Index: src/attach.c ================================================================== --- src/attach.c +++ src/attach.c @@ -246,10 +246,66 @@ db_multi_exec("INSERT OR IGNORE INTO unclustered VALUES(%d);", rid); } manifest_crosslink(rid, pAttach, MC_NONE); } + +/* +** Commit a new attachment into the repository +*/ +void attach_commit( + const char *zName, /* The filename of the attachment */ + const char *zTarget, /* The artifact uuid to attach to */ + const char *aContent, /* The content of the attachment */ + int szContent, /* The length of the attachment */ + int needModerator, /* Moderate the attachment? */ + const char *zComment /* The comment for the attachment */ +){ + Blob content; + Blob manifest; + Blob cksum; + char *zUUID; + char *zDate; + int rid; + int i, n; + int addCompress = 0; + Manifest *pManifest; + + db_begin_transaction(); + blob_init(&content, aContent, szContent); + pManifest = manifest_parse(&content, 0, 0); + manifest_destroy(pManifest); + blob_init(&content, aContent, szContent); + if( pManifest ){ + blob_compress(&content, &content); + addCompress = 1; + } + rid = content_put_ex(&content, 0, 0, 0, needModerator); + zUUID = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); + blob_zero(&manifest); + for(i=n=0; zName[i]; i++){ + if( zName[i]=='/' || zName[i]=='\\' ) n = i+1; + } + zName += n; + if( zName[0]==0 ) zName = "unknown"; + blob_appendf(&manifest, "A %F%s %F %s\n", + zName, addCompress ? ".gz" : "", zTarget, zUUID); + while( fossil_isspace(zComment[0]) ) zComment++; + n = strlen(zComment); + while( n>0 && fossil_isspace(zComment[n-1]) ){ n--; } + if( n>0 ){ + blob_appendf(&manifest, "C %#F\n", n, zComment); + } + zDate = date_in_standard_format("now"); + blob_appendf(&manifest, "D %s\n", zDate); + blob_appendf(&manifest, "U %F\n", login_name()); + md5sum_blob(&manifest, &cksum); + blob_appendf(&manifest, "Z %b\n", &cksum); + attach_put(&manifest, rid, needModerator); + assert( blob_is_reset(&manifest) ); + db_end_transaction(0); +} /* ** WEBPAGE: attachadd ** Add a new attachment. ** @@ -300,11 +356,11 @@ zTechNote = db_text(0, "SELECT substr(tagname,7) FROM tag" " WHERE tagname GLOB 'event-%q*'", zTechNote); if( zTechNote==0) fossil_redirect_home(); } zTarget = zTechNote; - zTargetType = mprintf("Tech Note %h", + zTargetType = mprintf("Tech Note %S", zTechNote, zTechNote); }else{ if( g.perm.ApndTkt==0 || g.perm.Attach==0 ){ login_needed(g.anon.ApndTkt && g.anon.Attach); @@ -322,59 +378,14 @@ if( zFrom==0 ) zFrom = mprintf("%s/home", g.zTop); if( P("cancel") ){ cgi_redirect(zFrom); } if( P("ok") && szContent>0 && (goodCaptcha = captcha_is_correct()) ){ - Blob content; - Blob manifest; - Blob cksum; - char *zUUID; - const char *zComment; - char *zDate; - int rid; - int i, n; - int addCompress = 0; - Manifest *pManifest; - int needModerator; - - db_begin_transaction(); - blob_init(&content, aContent, szContent); - pManifest = manifest_parse(&content, 0, 0); - manifest_destroy(pManifest); - blob_init(&content, aContent, szContent); - if( pManifest ){ - blob_compress(&content, &content); - addCompress = 1; - } - needModerator = - (zTkt!=0 && ticket_need_moderation(0)) || - (zPage!=0 && wiki_need_moderation(0)); - rid = content_put_ex(&content, 0, 0, 0, needModerator); - zUUID = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); - blob_zero(&manifest); - for(i=n=0; zName[i]; i++){ - if( zName[i]=='/' || zName[i]=='\\' ) n = i; - } - zName += n; - if( zName[0]==0 ) zName = "unknown"; - blob_appendf(&manifest, "A %F%s %F %s\n", - zName, addCompress ? ".gz" : "", zTarget, zUUID); - zComment = PD("comment", ""); - while( fossil_isspace(zComment[0]) ) zComment++; - n = strlen(zComment); - while( n>0 && fossil_isspace(zComment[n-1]) ){ n--; } - if( n>0 ){ - blob_appendf(&manifest, "C %#F\n", n, zComment); - } - zDate = date_in_standard_format("now"); - blob_appendf(&manifest, "D %s\n", zDate); - blob_appendf(&manifest, "U %F\n", login_name()); - md5sum_blob(&manifest, &cksum); - blob_appendf(&manifest, "Z %b\n", &cksum); - attach_put(&manifest, rid, needModerator); - assert( blob_is_reset(&manifest) ); - db_end_transaction(0); + int needModerator = (zTkt!=0 && ticket_need_moderation(0)) || + (zPage!=0 && wiki_need_moderation(0)); + const char *zComment = PD("comment", ""); + attach_commit(zName, zTarget, aContent, szContent, needModerator, zComment); cgi_redirect(zFrom); } style_header("Add Attachment"); if( !goodCaptcha ){ @

Error: Incorrect security code.

@@ -670,5 +681,106 @@ @ } db_finalize(&q); } + +/* +** COMMAND: attachment* +** +** Usage: %fossil attachment add ?PAGENAME? FILENAME ?OPTIONS? +** +** Add an attachment to an existing wiki page or tech note. +** +** Options: +** -t|--technote DATETIME Specifies the timestamp of +** the technote to which the attachment +** is to be made. The attachment will be +** to the most recently modified tech note +** with the specified timestamp. +** -t|--technote TECHNOTE-ID Specifies the technote to be +** updated by its technote id. +** +** One of PAGENAME, DATETIME or TECHNOTE-ID must be specified. +*/ +void attachment_cmd(void){ + int n; + db_find_and_open_repository(0, 0); + if( g.argc<3 ){ + goto attachment_cmd_usage; + } + n = strlen(g.argv[2]); + if( n==0 ){ + goto attachment_cmd_usage; + } + + if( strncmp(g.argv[2],"add",n)==0 ){ + const char *zPageName; /* Name of the wiki page to attach to */ + const char *zFile; /* Name of the file to be attached */ + const char *zETime; /* The name of the technote to attach to */ + Manifest *pWiki = 0; /* Parsed wiki page content */ + char *zBody = 0; /* Wiki page content */ + int rid; + const char *zTarget; /* Target of the attachment */ + Blob content; /* The content of the attachment */ + zETime = find_option("technote","t",1); + if( !zETime ){ + if( g.argc!=5 ){ + usage("add PAGENAME FILENAME"); + } + zPageName = g.argv[3]; + rid = db_int(0, "SELECT x.rid FROM tag t, tagxref x" + " WHERE x.tagid=t.tagid AND t.tagname='wiki-%q'" + " ORDER BY x.mtime DESC LIMIT 1", + zPageName + ); + if( (pWiki = manifest_get(rid, CFTYPE_WIKI, 0))!=0 ){ + zBody = pWiki->zWiki; + } + if( zBody==0 ){ + fossil_fatal("wiki page [%s] not found",zPageName); + } + zTarget = zPageName; + zFile = g.argv[4]; + }else{ + if( g.argc!=4 ){ + usage("add FILENAME --technote DATETIME|TECHNOTE-ID"); + } + rid = wiki_technote_to_rid(zETime); + if( rid<0 ){ + fossil_fatal("ambiguous tech note id: %s", zETime); + } + if( (pWiki = manifest_get(rid, CFTYPE_EVENT, 0))!=0 ){ + zBody = pWiki->zWiki; + } + if( zBody==0 ){ + fossil_fatal("technote [%s] not found",zETime); + } + zTarget = db_text(0, + "SELECT substr(tagname,7) FROM tag WHERE tagid=(SELECT tagid FROM event WHERE objid='%d')", + rid + ); + zFile = g.argv[3]; + } + blob_read_from_file(&content, zFile); + user_select(); + attach_commit( + zFile, /* The filename of the attachment */ + zTarget, /* The artifact uuid to attach to */ + blob_buffer(&content), /* The content of the attachment */ + blob_size(&content), /* The length of the attachment */ + 0, /* No need to moderate the attachment */ + "" /* Empty attachment comment */ + ); + if( !zETime ){ + fossil_print("Attached %s to wiki page %s.\n", zFile, zPageName); + }else{ + fossil_print("Attached %s to tech note %s.\n", zFile, zETime); + } + }else{ + goto attachment_cmd_usage; + } + return; + +attachment_cmd_usage: + usage("add ?PAGENAME? FILENAME [-t|--technote DATETIME ]"); +} Index: src/event.c ================================================================== --- src/event.c +++ src/event.c @@ -540,44 +540,26 @@ style_footer(); } /* ** Add a new tech note to the repository. The timestamp is -** given by the zETime parameter. isNew must be true to create +** given by the zETime parameter. rid must be zero to create ** a new page. If no previous page with the name zPageName exists ** and isNew is false, then this routine throws an error. */ void event_cmd_commit( char *zETime, /* timestamp */ - int isNew, /* true to create a new page */ + int rid, /* Artifact id of the tech note */ Blob *pContent, /* content of the new page */ const char *zMimeType, /* mimetype of the content */ const char *zComment, /* comment to go on the timeline */ const char *zTags, /* tags */ const char *zClr /* background color */ ){ - int rid; /* Artifact id of the tech note */ const char *zId; /* id of the tech note */ - rid = db_int(0, "SELECT objid FROM event" - " WHERE datetime(mtime)=datetime('%q') AND type = 'e'" - " LIMIT 1", - zETime - ); - if( rid==0 && !isNew ){ -#ifdef FOSSIL_ENABLE_JSON - g.json.resultCode = FSL_JSON_E_RESOURCE_NOT_FOUND; -#endif - fossil_fatal("no such tech note: %s", zETime); - } - if( rid!=0 && isNew ){ -#ifdef FOSSIL_ENABLE_JSON - g.json.resultCode = FSL_JSON_E_RESOURCE_ALREADY_EXISTS; -#endif - fossil_fatal("tech note %s already exists", zETime); - } - - if ( isNew ){ + + if ( rid==0 ){ zId = db_text(0, "SELECT lower(hex(randomblob(20)))"); }else{ zId = db_text(0, "SELECT substr(tagname,7) FROM tag" " WHERE tagid=(SELECT tagid FROM event WHERE objid='%d')", Index: src/info.c ================================================================== --- src/info.c +++ src/info.c @@ -1330,10 +1330,11 @@ const char *zDate = db_column_text(&q, 0); const char *zUser = db_column_text(&q, 1); const char *zCom = db_column_text(&q, 2); const char *zType = db_column_text(&q, 3); const char *zUuid = db_column_text(&q, 4); + int eventTagId = db_column_int(&q, 5); if( cnt>0 ){ @ Also } if( zType[0]=='w' ){ @ Wiki edit @@ -1343,17 +1344,21 @@ objType |= OBJTYPE_TICKET; }else if( zType[0]=='c' ){ @ Manifest of check-in objType |= OBJTYPE_CHECKIN; }else if( zType[0]=='e' ){ - @ Instance of technote - objType |= OBJTYPE_EVENT; - hyperlink_to_event_tagid(db_column_int(&q, 5)); + if( eventTagId != 0) { + @ Instance of technote + objType |= OBJTYPE_EVENT; + hyperlink_to_event_tagid(db_column_int(&q, 5)); + }else{ + @ Attachment to technote + } }else{ @ Tag referencing } - if( zType[0]!='e' ){ + if( zType[0]!='e' || eventTagId == 0){ hyperlink_to_uuid(zUuid); } @ - %!W(zCom) by hyperlink_to_user(zUser,zDate," on"); hyperlink_to_date(zDate, "."); @@ -1383,14 +1388,32 @@ }else{ @ Attachment "%h(zFilename)" to } objType |= OBJTYPE_ATTACHMENT; if( strlen(zTarget)==UUID_SIZE && validate16(zTarget,UUID_SIZE) ){ - if( g.perm.Hyperlink && g.anon.RdTkt ){ - @ ticket [%z(href("%R/tktview?name=%!S",zTarget))%S(zTarget)] + if ( db_exists("SELECT 1 FROM tag WHERE tagname='tkt-%q'", + zTarget) + ){ + if( g.perm.Hyperlink && g.anon.RdTkt ){ + @ ticket [%z(href("%R/tktview?name=%!S",zTarget))%S(zTarget)] + }else{ + @ ticket [%S(zTarget)] + } + }else if( db_exists("SELECT 1 FROM tag WHERE tagname='event-%q'", + zTarget) + ){ + if( g.perm.Hyperlink && g.anon.RdWiki ){ + @ tech note [%z(href("%R/technote/%h",zTarget))%S(zTarget)] + }else{ + @ tech note [%S(zTarget)] + } }else{ - @ ticket [%S(zTarget)] + if( g.perm.Hyperlink && g.anon.RdWiki ){ + @ wiki page [%z(href("%R/wiki?name=%t",zTarget))%h(zTarget)] + }else{ + @ wiki page [%h(zTarget)] + } } }else{ if( g.perm.Hyperlink && g.anon.RdWiki ){ @ wiki page [%z(href("%R/wiki?name=%t",zTarget))%h(zTarget)] }else{ Index: src/json_wiki.c ================================================================== --- src/json_wiki.c +++ src/json_wiki.c @@ -374,12 +374,13 @@ if(contentLen){ blob_append(&content, cson_string_cstr(jstr),contentLen); } zMimeType = json_find_option_cstr("mimetype","mimetype","M"); + zMimeType = wiki_filter_mimetypes(zMimeType); - wiki_cmd_commit(zPageName, 0==rid, &content, zMimeType, 0); + wiki_cmd_commit(zPageName, rid, &content, zMimeType, 0); blob_reset(&content); /* Our return value here has a race condition: if this operation is called concurrently for the same wiki page via two requests, payV could reflect the results of the other save operation. Index: src/manifest.c ================================================================== --- src/manifest.c +++ src/manifest.c @@ -2066,15 +2066,17 @@ const char isAdd = (zSrc && zSrc[0]) ? 1 : 0; char *zComment; if( isAdd ){ zComment = mprintf( "Add attachment [/artifact/%!S|%h] to" - " tech note [/technote/%h|%.10h]", + " tech note [/technote/%!S|%S]", zSrc, zName, zTarget, zTarget); }else{ - zComment = mprintf("Delete attachment \"%h\" from tech note [%.10h]", - zName, zTarget); + zComment = mprintf( + "Delete attachment \"%h\" from" + " tech note [/technote/%!S|%S]", + zName, zTarget, zTarget); } db_multi_exec("UPDATE event SET comment=%Q, type='e'" " WHERE objid=%Q", zComment, zAttachId); fossil_free(zComment); @@ -2162,15 +2164,18 @@ p->zAttachName, p->zAttachTarget); } }else if( 'e' == attachToType ){ if( isAdd ){ zComment = mprintf( - "Add attachment [/artifact/%!S|%h] to tech note [/technote/%h|%.10h]", + "Add attachment [/artifact/%!S|%h] to tech note [/technote/%!S|%S]", p->zAttachSrc, p->zAttachName, p->zAttachTarget, p->zAttachTarget); }else{ - zComment = mprintf("Delete attachment \"%h\" from tech note [%.10h]", - p->zAttachName, p->zAttachTarget); + zComment = mprintf( + "Delete attachment \"/artifact/%!S|%h\" from" + " tech note [/technote/%!S|%S]", + p->zAttachName, p->zAttachName, + p->zAttachTarget,p->zAttachTarget); } }else{ if( isAdd ){ zComment = mprintf( "Add attachment [/artifact/%!S|%h] to ticket [%!S|%S]", Index: src/wiki.c ================================================================== --- src/wiki.c +++ src/wiki.c @@ -122,20 +122,35 @@ static int is_sandbox(const char *zPagename){ return fossil_stricmp(zPagename,"sandbox")==0 || fossil_stricmp(zPagename,"sand box")==0; } +/* +** Formal, common and short names for the various wiki styles. +*/ +static const char *const azStyles[] = { + "text/x-fossil-wiki", "Fossil Wiki", "wiki", + "text/x-markdown", "Markdown", "markdown", + "text/plain", "Plain Text", "plain" +}; + /* ** Only allow certain mimetypes through. ** All others become "text/x-fossil-wiki" */ const char *wiki_filter_mimetypes(const char *zMimetype){ - if( zMimetype!=0 && - ( fossil_strcmp(zMimetype, "text/x-markdown")==0 - || fossil_strcmp(zMimetype, "text/plain")==0 ) - ){ - return zMimetype; + if( zMimetype!=0 ){ + int i; + for(i=0; i - for(i=0; i%s(azStyles[i+1]) }else{ @ } @@ -1068,47 +1074,26 @@ style_footer(); } /* ** Add a new wiki page to the repository. The page name is -** given by the zPageName parameter. isNew must be true to create -** a new page. If no previous page with the name zPageName exists -** and isNew is false, then this routine throws an error. +** given by the zPageName parameter. rid must be zero to create +** a new page otherwise the page identified by rid is updated. ** ** The content of the new page is given by the blob pContent. ** ** zMimeType specifies the N-card for the wiki page. If it is 0, ** empty, or "text/x-fossil-wiki" (the default format) then it is ** ignored. */ -int wiki_cmd_commit(const char *zPageName, int isNew, Blob *pContent, +int wiki_cmd_commit(const char *zPageName, int rid, Blob *pContent, const char *zMimeType, int localUser){ Blob wiki; /* Wiki page content */ Blob cksum; /* wiki checksum */ - int rid; /* artifact ID of parent page */ char *zDate; /* timestamp */ char *zUuid; /* uuid for rid */ - rid = db_int(0, - "SELECT x.rid FROM tag t, tagxref x" - " WHERE x.tagid=t.tagid AND t.tagname='wiki-%q'" - " ORDER BY x.mtime DESC LIMIT 1", - zPageName - ); - if( rid==0 && !isNew ){ -#ifdef FOSSIL_ENABLE_JSON - g.json.resultCode = FSL_JSON_E_RESOURCE_NOT_FOUND; -#endif - fossil_fatal("no such wiki page: %s", zPageName); - } - if( rid!=0 && isNew ){ -#ifdef FOSSIL_ENABLE_JSON - g.json.resultCode = FSL_JSON_E_RESOURCE_ALREADY_EXISTS; -#endif - fossil_fatal("wiki page %s already exists", zPageName); - } - blob_zero(&wiki); zDate = date_in_standard_format("now"); blob_appendf(&wiki, "D %s\n", zDate); free(zDate); blob_appendf(&wiki, "L %F\n", zPageName ); @@ -1133,47 +1118,112 @@ db_begin_transaction(); wiki_put(&wiki, 0, wiki_need_moderation(localUser)); db_end_transaction(0); return 1; } + +/* +** Determine the rid for a tech note given either its id or its +** timestamp. Returns 0 if there is no such item and -1 if the details +** are ambiguous and could refer to multiple items. +*/ +int wiki_technote_to_rid(const char *zETime) { + int rid=0; /* Artifact ID of the tech note */ + int nETime = strlen(zETime); + Stmt q; + if( nETime>=4 && nETime<=UUID_SIZE && validate16(zETime, nETime) ){ + char zUuid[UUID_SIZE+1]; + memcpy(zUuid, zETime, nETime+1); + canonical16(zUuid, nETime); + db_prepare(&q, + "SELECT e.objid" + " FROM event e, tag t" + " WHERE e.type='e' AND e.tagid IS NOT NULL AND t.tagid=e.tagid" + " AND t.tagname GLOB 'event-%q*'", + zUuid + ); + if( db_step(&q)==SQLITE_ROW ){ + rid = db_column_int(&q, 0); + if( db_step(&q)==SQLITE_ROW ) rid = -1; + } + db_finalize(&q); + } + if (!rid) { + if (strlen(zETime)>4) { + rid = db_int(0, "SELECT objid" + " FROM event" + " WHERE datetime(mtime)=datetime('%q')" + " AND type='e'" + " AND tagid IS NOT NULL" + " ORDER BY objid DESC LIMIT 1", + zETime); + } + } + return rid; +} /* ** COMMAND: wiki* ** ** Usage: %fossil wiki (export|create|commit|list) WikiName ** ** Run various subcommands to work with wiki entries or tech notes. ** -** %fossil wiki export ?PAGENAME? ?FILE? [-t|--technote DATETIME ] +** %fossil wiki export PAGENAME ?FILE? +** %fossil wiki export ?FILE? -t|--technote DATETIME|TECHNOTE-ID ** -** Sends the latest version of either the PAGENAME wiki entry -** or the DATETIME tech note to the given file or standard -** output. One of PAGENAME or DATETIME must be specified. +** Sends the latest version of either a wiki page or of a tech note +** to the given file or standard output. +** If PAGENAME is provided, the wiki page will be output. For +** a tech note either DATETIME or TECHNOTE-ID must be specified. If +** DATETIME is used, the most recently modified tech note with that +** DATETIME will be sent. ** ** %fossil wiki (create|commit) PAGENAME ?FILE? ?OPTIONS? ** ** Create a new or commit changes to an existing wiki page or -** technote from FILE or from standard input. +** technote from FILE or from standard input. PAGENAME is the +** name of the wiki entry or the timeline comment of the +** technote. ** ** Options: -** -M|--mimetype TEXT-FORMAT The mimetype of the update defaulting -** to the type used by the previous version -** of the page or text/x-fossil-wiki. -** -t|--technote DATETIME Specifies the timestamp of the technote -** to be created or updated. +** -M|--mimetype TEXT-FORMAT The mime type of the update. +** Defaults to the type used by +** the previous version of the +** page, or text/x-fossil-wiki. +** Valid values are: text/x-fossil-wiki, +** text/markdown and text/plain. fossil, +** markdown or plain can be specified as +** synonyms of these values. +** -t|--technote DATETIME Specifies the timestamp of +** the technote to be created or +** updated. When updating a tech note +** the most recently modified tech note +** with the specified timestamp will be +** updated. +** -t|--technote TECHNOTE-ID Specifies the technote to be +** updated by its technote id. ** --technote-tags TAGS The set of tags for a technote. -** --technote-bgcolor COLOR The color used for the technote on the -** timeline. +** --technote-bgcolor COLOR The color used for the technote +** on the timeline. ** -** %fossil wiki list ?--technote? -** %fossil wiki ls ?--technote? +** %fossil wiki list ?OPTIONS? +** %fossil wiki ls ?OPTIONS? ** ** Lists all wiki entries, one per line, ordered -** case-insensitively by name. The --technote flag -** specifies that technotes will be listed instead of -** the wiki entries, which will be listed in order -** timestamp. +** case-insensitively by name. +** +** Options: +** -t|--technote Technotes will be listed instead of +** pages. The technotes will be in order +** of timestamp with the most recent +** first. +** -s|--show-technote-ids The id of the tech note will be listed +** along side the timestamp. The tech note +** id will be the first word on each line. +** This option only applies if the +** --technote option is also specified. ** */ void wiki_cmd(void){ int n; db_find_and_open_repository(0, 0); @@ -1213,22 +1263,21 @@ fossil_fatal("wiki page [%s] not found",zPageName); } zFile = (g.argc==4) ? "-" : g.argv[4]; }else{ if( (g.argc!=3) && (g.argc!=4) ){ - usage("export ?FILE? --technote DATETIME"); + usage("export ?FILE? --technote DATETIME|TECHNOTE-ID"); } - rid = db_int(0, "SELECT objid FROM event" - " WHERE datetime(mtime)=datetime('%q') AND type='e'" - " ORDER BY mtime DESC LIMIT 1", - zETime - ); + rid = wiki_technote_to_rid(zETime); + if (rid == -1) { + fossil_fatal("ambiguous tech note id: %s", zETime); + } if( (pWiki = manifest_get(rid, CFTYPE_EVENT, 0))!=0 ){ zBody = pWiki->zWiki; } if( zBody==0 ){ - fossil_fatal("technote not found"); + fossil_fatal("technote [%s] not found",zETime); } zFile = (g.argc==3) ? "-" : g.argv[3]; } for(i=strlen(zBody); i>0 && fossil_isspace(zBody[i-1]); i--){} zBody[i] = 0; @@ -1270,43 +1319,58 @@ if(rid>0 && (pWiki = manifest_get(rid, CFTYPE_WIKI, 0))!=0 && (pWiki->zMimetype && *pWiki->zMimetype)){ zMimeType = pWiki->zMimetype; } }else{ - rid = db_int(0, "SELECT objid FROM event" - " WHERE datetime(mtime)=datetime('%q') AND type='e'" - " ORDER BY mtime DESC LIMIT 1", - zPageName - ); + rid = wiki_technote_to_rid(zETime); if(rid>0 && (pWiki = manifest_get(rid, CFTYPE_EVENT, 0))!=0 && (pWiki->zMimetype && *pWiki->zMimetype)){ zMimeType = pWiki->zMimetype; } } + }else{ + zMimeType = wiki_filter_mimetypes(zMimeType); + } + if( g.argv[2][1]=='r' && rid>0 ){ + if ( !zETime ){ + fossil_fatal("wiki page %s already exists", zPageName); + }else{ + /* Creating a tech note with same timestamp is permitted + and should create a new tech note */ + rid = 0; + } + }else if( g.argv[2][1]=='o' && rid == 0 ){ + if ( !zETime ){ + fossil_fatal("no such wiki page: %s", zPageName); + }else{ + fossil_fatal("no such tech note: %s", zETime); + } } + if( !zETime ){ + wiki_cmd_commit(zPageName, rid, &content, zMimeType, 1); if( g.argv[2][1]=='r' ){ - wiki_cmd_commit(zPageName, 1, &content, zMimeType, 1); fossil_print("Created new wiki page %s.\n", zPageName); }else{ - wiki_cmd_commit(zPageName, 0, &content, zMimeType, 1); fossil_print("Updated wiki page %s.\n", zPageName); } }else{ - char *zMETime; /* Normalized, mutable version of zETime */ - zMETime = db_text(0, "SELECT coalesce(datetime(%Q),datetime('now'))", - zETime); - if( g.argv[2][1]=='r' ){ - event_cmd_commit(zMETime, 1, &content, zMimeType, zPageName, - zTags, zClr); - fossil_print("Created new tech note %s.\n", zMETime); - }else{ - event_cmd_commit(zMETime, 0, &content, zMimeType, zPageName, - zTags, zClr); - fossil_print("Updated tech note %s.\n", zMETime); - } - free(zMETime); + if( rid != -1 ){ + char *zMETime; /* Normalized, mutable version of zETime */ + zMETime = db_text(0, "SELECT coalesce(datetime(%Q),datetime('now'))", + zETime); + event_cmd_commit(zMETime, rid, &content, zMimeType, zPageName, + zTags, zClr); + if( g.argv[2][1]=='r' ){ + fossil_print("Created new tech note %s.\n", zMETime); + }else{ + fossil_print("Updated tech note %s.\n", zMETime); + } + free(zMETime); + }else{ + fossil_fatal("ambiguous tech note id: %s", zETime); + } } manifest_destroy(pWiki); blob_reset(&content); }else if( strncmp(g.argv[2],"delete",n)==0 ){ if( g.argc!=5 ){ @@ -1314,23 +1378,35 @@ } fossil_fatal("delete not yet implemented."); }else if(( strncmp(g.argv[2],"list",n)==0 ) || ( strncmp(g.argv[2],"ls",n)==0 )){ Stmt q; + int showIds = 0; + if ( !find_option("technote","t",0) ){ db_prepare(&q, "SELECT substr(tagname, 6) FROM tag WHERE tagname GLOB 'wiki-*'" " ORDER BY lower(tagname) /*sort*/" ); }else{ + showIds = find_option("show-technote-ids","s",0)!=0; db_prepare(&q, - "SELECT datetime(mtime) FROM event WHERE type='e'" - " ORDER BY mtime /*sort*/" + "SELECT datetime(e.mtime), substr(t.tagname,7)" + " FROM event e, tag t" + " WHERE e.type='e'" + " AND e.tagid IS NOT NULL" + " AND t.tagid=e.tagid" + " ORDER BY e.mtime DESC /*sort*/" ); } + while( db_step(&q)==SQLITE_ROW ){ const char *zName = db_column_text(&q, 0); + if (showIds) { + const char *zUuid = db_column_text(&q, 1); + fossil_print("%s ",zUuid); + } fossil_print( "%s\n",zName); } db_finalize(&q); }else{ goto wiki_cmd_usage; ADDED test/wiki.test Index: test/wiki.test ================================================================== --- /dev/null +++ test/wiki.test @@ -0,0 +1,399 @@ +# +# Copyright (c) 2016 D. Richard Hipp +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the Simplified BSD License (also +# known as the "2-Clause License" or "FreeBSD License".) +# +# This program is distributed in the hope that it will be useful, +# but without any warranty; without even the implied warranty of +# merchantability or fitness for a particular purpose. +# +# Author contact information: +# drh@hwaci.com +# http://www.hwaci.com/drh/ +# +############################################################################ +# +# Test wiki and attachment command Support +# + +test_setup + +# Return true if two files are similar (i.e. not only compress trailing spaces +# from a line, but remove any final LF from the file as well) +proc similar_file {a b} { + set x [read_file $a] + regsub -all { +\n} $x \n x + regsub -all {\n$} $x {} x + set y [read_file $b] + regsub -all { +\n} $y \n y + regsub -all {\n$} $y {} y + return [expr {$x==$y}] +} + +# Return the mime type in the manifest for a given wiki page +# Defaults to "error: some text" if the manifest can't be located and +# "text/x-fossil-wiki" (the default mimetype for rendering) +# if the N card is omitted in the manifest. +# Note: Makes fossil calls, so $CODE and $RESULT will be corrupted +proc get_mime_type {name} { + global CODE RESULT + fossil http << "GET /wiki?name=$name" + if {$CODE != 0} { + return error: /wiki?name=$name $CODE $RESULT" + } + set CODE [regexp {href="/info/([0-9a-f]+)"} $RESULT match info] + if {$CODE == 0} { + return "error: No info link found for wiki page $name" + } + fossil http << "GET /artifact/$info" + if {$CODE != 0} { + return "error: /artifact/$info $CODE $RESULT" + } + set CODE [regexp {
(.*)
} $RESULT match pre] + if {$CODE == 0} { + return "error: No pre block in /artifact/$info" + } + set CODE [regexp -line {^N (.*)$} $pre match mimetype] + if {$CODE == 0} { + return "text/x-fossil-wiki" + } + return $mimetype +} + + +############################################################################### +# Initially there should be no wiki entries +fossil wiki list +test wiki-0 {[normalize_result] eq {}} + +############################################################################### +# Adding an entry should add it to the wiki list +write_file f1 "first wiki note" +fossil wiki create tcltest f1 +test wiki-1 {$CODE == 0} +fossil wiki list +test wiki-2 {[normalize_result] eq {tcltest}} + +############################################################################### +# Trying to add the same entry should fail +fossil wiki create tcltest f1 -expectError +test wiki-3 {$CODE != 0} + +############################################################################### +# exporting the wiki page should give back similar text +fossil wiki export tcltest a1 +test wiki-4 {[similar_file f1 a1]} + +############################################################################### +# commiting a change to an existing page should replace the page on export +write_file f2 "second version of the page" +fossil wiki commit tcltest f2 +test wiki-5 {$CODE == 0} +fossil wiki export tcltest a2 +test wiki-6 {[similar_file f2 a2]} + +############################################################################### +# But we shouldn't be able to update non-existant pages +fossil wiki commit doesntexist f1 -expectError +test wiki-7 {$CODE != 0} + +############################################################################### +# There shouldn't be any tech notes at this point +fossil wiki list --technote +test wiki-8 {[normalize_result] eq {}} + +############################################################################### +# Creating a tech note with a specified timestamp should add a technote +write_file f3 "A technote" +fossil wiki create technote f3 --technote {2016-01-01 12:34} +test wiki-9 {$CODE == 0} +fossil wiki list --technote +test wiki-10 {[normalize_result] eq {2016-01-01 12:34:00}} +fossil wiki list --technote --show-technote-ids +set technotelist [split $RESULT "\n"] +set veryfirsttechnoteid [lindex [split [lindex $technotelist 0]] 0] + +############################################################################### +# exporting that technote should give back similar text +fossil wiki export a3 --technote {2016-01-01 12:34:00} +test wiki-11 {[similar_file f3 a3]} + +############################################################################### +# Trying to add a technote with the same timestamp should succeed and create a +# second tech note +fossil wiki create 2ndnote f3 -technote {2016-01-01 12:34} +test wiki-13 {$CODE == 0} +fossil wiki list --technote +set technotelist [split $RESULT "\n"] +test wiki-13.1 {[llength $technotelist] == 2} + +############################################################################### +# commiting a change to an existing technote should replace the page on export +# (this should update the tech note from wiki-13 as that the most recently +# updated one, that should also be the one exported by the export command) +write_file f4 "technote 2nd variant" +fossil wiki commit technote f4 --technote {2016-01-01 12:34} +test wiki-14 {$CODE == 0} +fossil wiki export a4 --technote {2016-01-01 12:34} +test wiki-15 {[similar_file f4 a4]} +# Also check that the tech note with the same timestamp, but modified less +# recently still has its original text +fossil wiki export a4.1 --technote $veryfirsttechnoteid +test wiki-15.1 {[similar_file f3 a4.1]} + +############################################################################### +# But we shouldn't be able to update non-existant pages +fossil wiki commit doesntexist f1 -expectError +test wiki-16 {$CODE != 0} + +############################################################################### +# Check specifying tags for a technote is OK +write_file f5 "technote with tags" +fossil wiki create {tagged technote} f5 --technote {2016-01-02 12:34} --technote-tags {A B} +test wiki-17 {$CODE == 0} +write_file f5.1 "editted and tagged technote" +fossil wiki commit {tagged technote} f5 --technote {2016-01-02 12:34} --technote-tags {C D} +test wiki-18 {$CODE == 0} + +############################################################################### +# Check specifying a bgcolor for a technote is OK +write_file f6 "bgcolored technote" +fossil wiki create bgcolor f6 --technote {2016-01-03 12:34} --technote-bgcolor red +test wiki-19 {$CODE == 0} +write_file f6.1 "editted technote with a background color" +fossil wiki commit bgcolor f6.1 --technote {2016-01-03 12:34} --technote-bgcolor yellow +test wiki-20 {$CODE == 0} + +############################################################################### +# Test adding an attachment to both a non-existant (should fail) and existing wiki page +write_file fa "This is a file to be attached" +fossil attachment add doesntexist fa -expectError +test wiki-21 {$CODE != 0} +fossil attachment add tcltest fa +test wiki-22 {$CODE == 0} + +############################################################################### +# Test adding an attachment to both a non-existant (should fail) and existing tech note +fossil attachment add fa --technote {2016-07-22 12:00} -expectError +test wiki-23 {$CODE != 0} +fossil attachment add fa --technote {2016-01-03 12:34} +test wiki-24 {$CODE == 0} + +############################################################################### +# Check that a wiki page with an attachment can be updated +fossil wiki commit tcltest f1 +test wiki-25 {$CODE == 0} + +############################################################################### +# Check that a technote with an attachment can be updated +fossil wiki commit technote f6 --technote {2016-01-03 12:34} +test wiki-26 {$CODE == 0} +fossil wiki commit technote f6 --technote {2016-01-03 12:34} --technote-tags {E F} +test wiki-27 {$CODE == 0} +fossil wiki commit technote f6 --technote {2016-01-03 12:34} --technote-bgcolor blue +test wiki-28 {$CODE == 0} + +############################################################################### +# Check longest form of timestamp for the technote +write_file f7 "Different timestamps" +fossil wiki create technotenow f7 --technote {2016-01-04 12:34:56+00:00} +test wiki-29 {$CODE == 0} + +############################################################################### +# Check a technote appears on the timeline +write_file f8 "Contents of a 'unique' tech note" +fossil wiki create {Unique technote} f8 --technote {2016-01-05 01:02:03} +fossil timeline +test wiki-30 {[string match *Unique*technote* $RESULT]} + +############################################################################### +# Check for a collision between an attachment and a note, this was a +# bug that resulted from some code treating the attachment entry as if it +# were a technote when it isn't really. +# +# First, wait for the top of the next second so the attachment +# happens at a known time, then add an attachment to an existing note +# and a new note immediately after. + +set t0 [clock seconds] +while {$t0 == [clock seconds]} { + after 100 +} +set t1 [clock format [clock seconds] -gmt 1 -format "%Y-%m-%d %H:%M:%S"] +write_file f9 "Timestamp: $t1" +fossil attachment add f9 --technote {2016-01-05 01:02:03} +test wiki-31 {$CODE == 0} +fossil wiki create {Attachment collision} f9 --technote now +test wiki-32 {$CODE == 0} +# +# Now waste time until the next second so that the remaining tests +# don't have to worry about a potential collision +set t0 [clock seconds] +while {$t0 == [clock seconds]} { + after 100 +} + +############################################################################### +# Check a technote with no timestamp cannot be created, but that +# "now" is a valid stamp. +set t2 [clock format [clock seconds] -gmt 1 -format "%Y-%m-%d %H:%M:%S"] +write_file f10 "Even unstampted notes are delivered.\nStamped $t2" +fossil wiki create "Unstamped Note" f10 --technote -expectError +test wiki-33 {$CODE != 0} +fossil wiki create "Unstamped Note" f10 --technote now +test wiki-34 {$CODE == 0} +fossil wiki list -t +test wiki-35 {[string match "*$t2*" $RESULT]} + +############################################################################### +# Check an attachment to it in the same second works. +write_file f11 "Time Stamp was $t2" +fossil attachment add f11 --technote $t2 +test wiki-36 {$CODE == 0} +fossil timeline +test wiki-36-1 {$CODE == 0} +fossil wiki list -t +test wiki-36-2 {$CODE == 0} + +############################################################################### +# Check that we have the expected number of tech notes on the list (and not +# extra ones from other events (such as the attachments) - 8 tech notes +# expected created by tests 9, 13, 17, 19, 29, 31, 32 and 34 +fossil wiki list --technote +set technotelist [split $RESULT "\n"] +test wiki-37 {[llength $technotelist] == 8} + +############################################################################### +# Check that using the show-technote-ids shows the same tech notes in the same +# order (with the technote id as the first word of the line) +fossil wiki list --technote --show-technote-ids +set technoteidlist [split $RESULT "\n"] +test wiki-38 {[llength $technotelist] == 8} +for {set i 0} {$i < [llength $technotelist]} {incr i} { + set match "???????????????????????????????????????? " + append match [lindex $technotelist $i] + test "wiki-39-$i" {[string match $match [lindex $technoteidlist $i]]} +} + +############################################################################### +# Create new tech note with a old timestamp so that it is oldest and then check that +# the contents of the oldest tech note (by tech note id, both full and short) match up +write_file f12 "A really old tech note" +fossil wiki create {Old tech note} f12 --technote {2001-07-07 09:08:07} +fossil wiki list --technote --show-technote-ids +set technotelist [split $RESULT "\n"] +set anoldtechnoteid [lindex [split [lindex $technotelist [llength $technotelist]-1]] 0] +fossil wiki export a12 --technote $anoldtechnoteid +test wiki-40 {[similar_file f12 a12]} + +############################################################################### +# Also check that we can specify a prefix of the tech note id (note: with +# 9 items in the tech note at this point there is a chance of a collision. +# However with a 20 character prefix the chance of the collision is +# approximately 1 in 10^22 so this test ignores that possibility.) +fossil wiki export a12.1 --technote [string range $anoldtechnoteid 0 20] +test wiki-41 {[similar_file f12 a12.1]} + +############################################################################### +# Now we need to force a collision in the first four characters of the tech +# note id if we don't already have one so we can check we get an error if the +# tech note id is ambiguous +set idcounts [dict create] +set maxcount 0 +fossil wiki list --technote --show-technote-ids +set technotelist [split $RESULT "\n"] +for {set i 0} {$i < [llength $technotelist]} {incr i} { + set fullid [lindex $technotelist $i] + set id [string range $fullid 0 3] + dict incr idcounts $id + if {[dict get $idcounts $id] > $maxcount} { + set maxid $id + incr maxcount + } +} +# get i so that, as a julian date, it is in the 1800s, i.e., older than +# any other tech note, but after 1 AD +set i 2400000 +while {$maxcount < 2} { + # keep getting older + incr i -1 + write_file f13 "A tech note with timestamp of jday=$i" + fossil wiki create "timestamp of $i" f13 --technote "$i" + fossil wiki list --technote --show-technote-ids + set technotelist [split $RESULT "\n"] + set oldesttechnoteid [lindex [split [lindex $technotelist [llength $technotelist]-1]] 0] + set id [string range $oldesttechnoteid 0 3] + dict incr idcounts $id + if {[dict get $idcounts $id] > $maxcount} { + set maxid $id + incr maxcount + } +} +# Save the duplicate id for this and later tests +set duplicateid $maxid +fossil wiki export a13 --technote $duplicateid -expectError +test wiki-42 {$CODE != 0} + +############################################################################### +# Check we can update technote by its id +write_file f14 "Updated text for the really old tech note" +fossil wiki commit {Old tech note} f14 --technote $anoldtechnoteid +fossil wiki export a14 --technote $anoldtechnoteid +test wiki-43 {[similar_file f14 a14]} + +############################################################################### +# Check we can add attachments to a technote by its id +fossil attachment add fa --technote $anoldtechnoteid +test wiki-44 {$CODE == 0} + +############################################################################### +# Also check that we can specify a prefix of the tech note id +write_file f15 "Updated text for the really old tech note specified by its id" +fossil wiki commit {Old tech note} f15 --technote [string range $anoldtechnoteid 0 20] +fossil wiki export a15 --technote $anoldtechnoteid +test wiki-45 {[similar_file f15 a15]} + +############################################################################### +# Check we can add attachments to a technote by a prefix of its id +fossil attachment add fa --technote [string range $anoldtechnoteid 0 20] +test wiki-46 {$CODE == 0} + +############################################################################### +# And we get an error for the ambiguous tech note id +fossil wiki commit {Old tech note} f15 --technote $duplicateid -expectError +test wiki-47 {$CODE != 0} +fossil attachment add fa --technote $duplicateid -expectError +test wiki-48 {$CODE != 0} + +############################################################################### +# Check the default mimetype is text/x-fossil-wiki +test wiki-49 {[get_mime_type tcltest] == "text/x-fossil-wiki"} + +############################################################################### +# Check long form of the mimetypes are recorded correctly +fossil wiki create tcltest-x-fossil f1 -mimetype text/x-fossil-wiki +test wiki-50 {[get_mime_type tcltest-x-fossil] == "text/x-fossil-wiki"} +fossil wiki create tcltest-x-markdown f1 -mimetype text/x-markdown +test wiki-51 {[get_mime_type tcltest-x-markdown] == "text/x-markdown"} +fossil wiki create tcltest-plain f1 -mimetype text/plain +test wiki-52 {[get_mime_type tcltest-plain] == "text/plain"} +fossil wiki create tcltest-x-random f1 -mimetype text/x-random +test wiki-53 {[get_mime_type tcltest-x-random] == "text/x-fossil-wiki"} + +############################################################################### +# Check short form of the mimetypes are recorded correctly +fossil wiki create tcltest-x-fossil-short f1 -mimetype wiki +test wiki-54 {[get_mime_type tcltest-x-fossil-short] == "text/x-fossil-wiki"} +fossil wiki create tcltest-x-markdown-short f1 -mimetype markdown +test wiki-55 {[get_mime_type tcltest-x-markdown-short] == "text/x-markdown"} +fossil wiki create tcltest-plain-short f1 -mimetype plain +test wiki-56 {[get_mime_type tcltest-plain-short] == "text/plain"} +fossil wiki create tcltest-x-random-short f1 -mimetype random +test wiki-57 {[get_mime_type tcltest-x-random-short] == "text/x-fossil-wiki"} + + +############################################################################### +test_cleanup +