Changes On Branch ticket-enhancements
Not logged in

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Changes In Branch ticket-enhancements Excluding Merge-Ins

This is equivalent to a diff from fe453a4893 to 7575b52e15

2012-11-27
16:26
Enhancements to ticket processing. There are now two tables: TICKET and TICKETCHNG. There is one row in TICKETCHNG for each ticket artifact. Fields from ticket artifacts go into either or both of TICKET and TICKETCHNG, whichever contain matching column names. Default ticket edit and viewing scripts are updated to use TICKETCHNG. The TH1 scripti... check-in: 4f8c8975bc user: drh tags: trunk
15:32
Fix some HTML markup irregularities. Improvements to the default ticket viewer. Closed-Leaf check-in: 7575b52e15 user: drh tags: ticket-enhancements
2012-11-26
21:30
Fix a string-quoting error in the previous commit. check-in: 3c8195c876 user: drh tags: ticket-enhancements
2012-11-23
19:33
some unnecessary spacing check-in: d13143eb3b user: jan.nijtmans tags: trunk
15:57
All markup of the form ... with an options "links" or "links=BOOLEAN" attribute. Improved TH1 tracing and error reporting capabilities. Improved documentation on how reports work. check-in: 23c75abde4 user: drh tags: ticket-enhancements
11:29
merge trunk "filename contains illegal characters" is now a warning check-in: d3bee356ba user: jan.nijtmans tags: ticket-d17d6e5b17
10:35
Disallow invalid unicode characters Closed-Leaf check-in: 9242c09ff9 user: jan.nijtmans tags: invalid-unicode
01:50
When db_open_config() is called with the useAttach parameter set to non-zero, it may need to close and reopen the database using ATTACH if that was not done previously. check-in: fe453a4893 user: drh tags: trunk
2012-11-22
23:35
Be consistent about display of check-in comments as either text/plain or text/x-fossil-wiki. When the user configures text/plain, use that format everywhere. check-in: 2c6fa9c3b0 user: drh tags: trunk
10:16
Modify db_open_config() and associated routines to make their internal state more consistent and discoverable. Closed-Leaf check-in: 52a6868700 user: mistachkin tags: dbOpenConfig

Changes to src/attach.c.

   528    528         output_text_with_line_numbers(z, zLn);
   529    529       }else{
   530    530         @ <pre>
   531    531         @ %h(z)
   532    532         @ </pre>
   533    533       }
   534    534     }else if( strncmp(zMime, "image/", 6)==0 ){
   535         -    @ <img src="%R/raw?name=%s(zSrc)&m=%s(zMime)"></img>
          535  +    @ <img src="%R/raw/%S(zSrc)?m=%s(zMime)"></img>
          536  +    style_submenu_element("Image", "Image", "%R/raw/%S?m=%s", zSrc, zMime);
   536    537     }else{
   537    538       int sz = db_int(0, "SELECT size FROM blob WHERE rid=%d", ridSrc);
   538    539       @ <i>(file is %d(sz) bytes of binary data)</i>
   539    540     }
   540    541     @ </blockquote>
   541    542     manifest_destroy(pAttach);
   542    543     blob_reset(&attach);

Changes to src/cgi.c.

   781    781     }else{ /* generic error message */
   782    782         json_err( FSL_JSON_E_INVALID_REQUEST, NULL, 1 );
   783    783     }
   784    784     fossil_exit( g.isHTTP ? 0 : 1);
   785    785   }
   786    786   #endif /* FOSSIL_ENABLE_JSON */
   787    787   
          788  +/*
          789  +** Log HTTP traffic to a file.  Begin the log on first use.  Close the log
          790  +** when the argument is NULL.
          791  +*/
          792  +void cgi_trace(const char *z){
          793  +  static FILE *pLog = 0;
          794  +  if( g.fHttpTrace==0 ) return;
          795  +  if( z==0 ){
          796  +    if( pLog ) fclose(pLog);
          797  +    pLog = 0;
          798  +    return;
          799  +  }
          800  +  if( pLog==0 ){
          801  +    char zFile[50];
          802  +    unsigned r;
          803  +    sqlite3_randomness(sizeof(r), &r);
          804  +    sqlite3_snprintf(sizeof(zFile), zFile, "httplog-%08x.txt", r);
          805  +    pLog = fossil_fopen(zFile, "wb");
          806  +    if( pLog ){
          807  +      fprintf(stderr, "# open log on %s\n", zFile);
          808  +    }else{
          809  +      fprintf(stderr, "# failed to open %s\n", zFile);
          810  +      return;
          811  +    }
          812  +  }
          813  +  fputs(z, pLog);
          814  +}
          815  +
   788    816   
   789    817   /*
   790    818   ** Initialize the query parameter database.  Information is pulled from
   791    819   ** the QUERY_STRING environment variable (if it exists), from standard
   792    820   ** input if there is POST data, and from HTTP_COOKIE.
   793    821   */
   794    822   void cgi_init(void){
................................................................................
   823    851     if( len>0 && zType ){
   824    852       blob_zero(&g.cgiIn);
   825    853       if( fossil_strcmp(zType,"application/x-www-form-urlencoded")==0 
   826    854            || strncmp(zType,"multipart/form-data",19)==0 ){
   827    855         z = fossil_malloc( len+1 );
   828    856         len = fread(z, 1, len, g.httpIn);
   829    857         z[len] = 0;
          858  +      cgi_trace(z);
   830    859         if( zType[0]=='a' ){
   831    860           add_param_list(z, '&');
   832    861         }else{
   833    862           process_multipart_form_data(z, len);
   834    863         }
   835    864       }else if( fossil_strcmp(zType, "application/x-fossil")==0 ){
   836    865         blob_read_from_channel(&g.cgiIn, g.httpIn, len);
................................................................................
  1143   1172     struct sockaddr_in remoteName;
  1144   1173     socklen_t size = sizeof(struct sockaddr_in);
  1145   1174     char zLine[2000];     /* A single line of input. */
  1146   1175     g.fullHttpReply = 1;
  1147   1176     if( fgets(zLine, sizeof(zLine),g.httpIn)==0 ){
  1148   1177       malformed_request();
  1149   1178     }
         1179  +  cgi_trace(zLine);
  1150   1180     zToken = extract_token(zLine, &z);
  1151   1181     if( zToken==0 ){
  1152   1182       malformed_request();
  1153   1183     }
  1154   1184     if( fossil_strcmp(zToken,"GET")!=0 && fossil_strcmp(zToken,"POST")!=0
  1155   1185         && fossil_strcmp(zToken,"HEAD")!=0 ){
  1156   1186       malformed_request();
................................................................................
  1179   1209    
  1180   1210     /* Get all the optional fields that follow the first line.
  1181   1211     */
  1182   1212     while( fgets(zLine,sizeof(zLine),g.httpIn) ){
  1183   1213       char *zFieldName;
  1184   1214       char *zVal;
  1185   1215   
         1216  +    cgi_trace(zLine);
  1186   1217       zFieldName = extract_token(zLine,&zVal);
  1187   1218       if( zFieldName==0 || *zFieldName==0 ) break;
  1188   1219       while( fossil_isspace(*zVal) ){ zVal++; }
  1189   1220       i = strlen(zVal);
  1190   1221       while( i>0 && fossil_isspace(zVal[i-1]) ){ i--; }
  1191   1222       zVal[i] = 0;
  1192   1223       for(i=0; zFieldName[i]; i++){
................................................................................
  1210   1241       }else if( fossil_strcmp(zFieldName,"referer:")==0 ){
  1211   1242         cgi_setenv("HTTP_REFERER", zVal);
  1212   1243   #endif
  1213   1244       }else if( fossil_strcmp(zFieldName,"user-agent:")==0 ){
  1214   1245         cgi_setenv("HTTP_USER_AGENT", zVal);
  1215   1246       }
  1216   1247     }
  1217         -
  1218   1248     cgi_init();
         1249  +  cgi_trace(0);
  1219   1250   }
  1220   1251   
  1221   1252   #if INTERFACE
  1222   1253   /* 
  1223   1254   ** Bitmap values for the flags parameter to cgi_http_server().
  1224   1255   */
  1225   1256   #define HTTP_SERVER_LOCALHOST      0x0001     /* Bind to 127.0.0.1 only */

Changes to src/db.c.

  1310   1310         " VALUES('server-code', lower(hex(randomblob(20))),now());"
  1311   1311         "INSERT INTO config(name,value,mtime)"
  1312   1312         " VALUES('project-code', lower(hex(randomblob(20))),now());"
  1313   1313       );
  1314   1314     }
  1315   1315     if( !db_is_global("autosync") ) db_set_int("autosync", 1, 0);
  1316   1316     if( !db_is_global("localauth") ) db_set_int("localauth", 0, 0);
         1317  +  if( !db_is_global("timeline-plaintext") ){
         1318  +    db_set_int("timeline-plaintext", 1, 0);
         1319  +  }
  1317   1320     db_create_default_users(0, zDefaultUser);
  1318   1321     user_select();
  1319   1322   
  1320   1323     if( zTemplate ){
  1321   1324       /*
  1322   1325       ** Copy all settings from the supplied template repository.
  1323   1326       */

Changes to src/info.c.

  1109   1109         @ [annotate]</a>
  1110   1110       }
  1111   1111       cnt++;
  1112   1112       if( pDownloadName && blob_size(pDownloadName)==0 ){
  1113   1113         blob_append(pDownloadName, zName, -1);
  1114   1114       }
  1115   1115     }
  1116         -  @ </ul></ul>
         1116  +  @ </ul>
  1117   1117     free(prevName);
  1118   1118     db_finalize(&q);
  1119   1119     db_prepare(&q,
  1120   1120       "SELECT substr(tagname, 6, 10000), datetime(event.mtime),"
  1121   1121       "       coalesce(event.euser, event.user)"
  1122   1122       "  FROM tagxref, tag, event"
  1123   1123       " WHERE tagxref.rid=%d"
................................................................................
  1652   1652           output_text_with_line_numbers(z, zLn);
  1653   1653         }else{
  1654   1654           @ <pre>
  1655   1655           @ %h(z)
  1656   1656           @ </pre>
  1657   1657         }
  1658   1658       }else if( strncmp(zMime, "image/", 6)==0 ){
  1659         -      @ <img src="%s(g.zTop)/raw?name=%s(zUuid)&m=%s(zMime)"></img>
         1659  +      @ <img src="%R/raw/%S(zUuid)?m=%s(zMime)" />
         1660  +      style_submenu_element("Image", "Image",
         1661  +                            "%R/raw/%S?m=%s", zUuid, zMime);
  1660   1662       }else{
  1661   1663         @ <i>(file is %d(blob_size(&content)) bytes of binary data)</i>
  1662   1664       }
  1663   1665       @ </blockquote>
  1664   1666     }
  1665   1667     style_footer();
  1666   1668   }
................................................................................
  1709   1711       }
  1710   1712     }
  1711   1713     style_header("Ticket Change Details");
  1712   1714     style_submenu_element("Raw", "Raw", "%R/artifact/%S", zUuid);
  1713   1715     style_submenu_element("History", "History", "%R/tkthistory/%s", zTktName);
  1714   1716     style_submenu_element("Page", "Page", "%R/tktview/%t", zTktName);
  1715   1717     style_submenu_element("Timeline", "Timeline", "%R/tkttimeline/%t", zTktName);
         1718  +  if( P("plaintext") ){
         1719  +    style_submenu_element("Formatted", "Formatted", "%R/info/%S", zUuid);
         1720  +  }else{
         1721  +    style_submenu_element("Plaintext", "Plaintext",
         1722  +                          "%R/info/%S?plaintext", zUuid);
         1723  +  }
  1716   1724   
  1717   1725     @ <div class="section">Overview</div>
  1718   1726     @ <p><table class="label-value">
  1719   1727     @ <tr><th>Artifact&nbsp;ID:</th>
  1720   1728     @ <td>%z(href("%R/artifact/%s",zUuid))%s(zUuid)</a>
  1721   1729     if( g.perm.Setup ){
  1722   1730       @ (%d(rid))
................................................................................
  1745   1753       @ <input type="submit" value="Submit">
  1746   1754       @ </form>
  1747   1755       @ </blockquote>
  1748   1756     }
  1749   1757   
  1750   1758     @ <div class="section">Changes</div>
  1751   1759     @ <p>
  1752         -  ticket_output_change_artifact(pTktChng);
         1760  +  ticket_output_change_artifact(pTktChng, 0);
  1753   1761     manifest_destroy(pTktChng);
  1754   1762     style_footer();
  1755   1763   }
  1756   1764   
  1757   1765   
  1758   1766   /*
  1759   1767   ** WEBPAGE: info

Changes to src/main.c.

  1630   1630     fossil_binary_mode(g.httpIn);
  1631   1631     g.cgiOutput = 1;
  1632   1632     blob_read_from_file(&config, zFile);
  1633   1633     while( blob_line(&config, &line) ){
  1634   1634       if( !blob_token(&line, &key) ) continue;
  1635   1635       if( blob_buffer(&key)[0]=='#' ) continue;
  1636   1636       if( blob_eq(&key, "debug:") && blob_token(&line, &value) ){
  1637         -      g.fDebug = fossil_fopen(blob_str(&value), "a");
         1637  +      g.fDebug = fossil_fopen(blob_str(&value), "ab");
  1638   1638         blob_reset(&value);
  1639   1639         continue;
  1640   1640       }
  1641   1641       if( blob_eq(&key, "HOME:") && blob_token(&line, &value) ){
  1642   1642         cgi_setenv("HOME", blob_str(&value));
  1643   1643         blob_reset(&value);
  1644   1644         continue;
................................................................................
  1848   1848   /*
  1849   1849   ** Note that the following command is used by ssh:// processing.
  1850   1850   **
  1851   1851   ** COMMAND: test-http
  1852   1852   ** Works like the http command but gives setup permission to all users.
  1853   1853   */
  1854   1854   void cmd_test_http(void){
         1855  +  g.thTrace = find_option("th-trace", 0, 0)!=0;
         1856  +  if( g.thTrace ){
         1857  +    blob_zero(&g.thLog);
         1858  +  }
  1855   1859     login_set_capabilities("sx", 0);
  1856   1860     g.useLocalauth = 1;
  1857   1861     cgi_set_parameter("REMOTE_ADDR", "127.0.0.1");
  1858   1862     g.httpIn = stdin;
  1859   1863     g.httpOut = stdout;
  1860   1864     find_server_repository(0);
  1861   1865     g.cgiOutput = 1;

Changes to src/manifest.c.

  1550   1550     if( !isNew ){
  1551   1551       for(i=0; i<pManifest->nField; i++){
  1552   1552         if( fossil_strcmp(pManifest->aField[i].zName, zStatusColumn)==0 ){
  1553   1553           zNewStatus = pManifest->aField[i].zValue;
  1554   1554         }
  1555   1555       }
  1556   1556       if( zNewStatus ){
  1557         -      blob_appendf(&comment, "%h ticket [%.10s]: <i>%s</i>",
         1557  +      blob_appendf(&comment, "%h ticket [%.10s]: <i>%h</i>",
  1558   1558            zNewStatus, pManifest->zTicketUuid, zTitle
  1559   1559         );
  1560   1560         if( pManifest->nField>1 ){
  1561   1561           blob_appendf(&comment, " plus %d other change%s",
  1562   1562             pManifest->nField-1, pManifest->nField==2 ? "" : "s");
  1563   1563         }
  1564   1564         blob_appendf(&brief, "%h ticket [%.10s].",
  1565   1565                      zNewStatus, pManifest->zTicketUuid);
  1566   1566       }else{
  1567   1567         zNewStatus = db_text("unknown", 
  1568   1568            "SELECT %s FROM ticket WHERE tkt_uuid='%s'",
  1569   1569            zStatusColumn, pManifest->zTicketUuid
  1570   1570         );
  1571         -      blob_appendf(&comment, "Ticket [%.10s] <i>%s</i> status still %h with "
         1571  +      blob_appendf(&comment, "Ticket [%.10s] <i>%h</i> status still %h with "
  1572   1572              "%d other change%s",
  1573   1573              pManifest->zTicketUuid, zTitle, zNewStatus, pManifest->nField,
  1574   1574              pManifest->nField==1 ? "" : "s"
  1575   1575         );
  1576   1576         free(zNewStatus);
  1577   1577         blob_appendf(&brief, "Ticket [%.10s]: %d change%s",
  1578   1578              pManifest->zTicketUuid, pManifest->nField,
................................................................................
  1866   1866          p->zAttachTarget, p->zAttachName
  1867   1867       );
  1868   1868       if( strlen(p->zAttachTarget)!=UUID_SIZE
  1869   1869        || !validate16(p->zAttachTarget, UUID_SIZE) 
  1870   1870       ){
  1871   1871         char *zComment;
  1872   1872         if( p->zAttachSrc && p->zAttachSrc[0] ){
  1873         -        zComment = mprintf("Add attachment \"%h\" to wiki page [%h]",
  1874         -             p->zAttachName, p->zAttachTarget);
         1873  +        zComment = mprintf(
         1874  +             "Add attachment [%R/artifact/%S|%h] to wiki page [%h]",
         1875  +             p->zAttachSrc, p->zAttachName, p->zAttachTarget);
  1875   1876         }else{
  1876   1877           zComment = mprintf("Delete attachment \"%h\" from wiki page [%h]",
  1877   1878                p->zAttachName, p->zAttachTarget);
  1878   1879         }
  1879   1880         db_multi_exec(
  1880   1881           "REPLACE INTO event(type,mtime,objid,user,comment)"
  1881   1882           "VALUES('w',%.17g,%d,%Q,%Q)",
  1882   1883           p->rDate, rid, p->zUser, zComment
  1883   1884         );
  1884   1885         free(zComment);
  1885   1886       }else{
  1886   1887         char *zComment;
  1887   1888         if( p->zAttachSrc && p->zAttachSrc[0] ){
  1888         -        zComment = mprintf("Add attachment \"%h\" to ticket [%.10s]",
  1889         -             p->zAttachName, p->zAttachTarget);
         1889  +        zComment = mprintf(
         1890  +             "Add attachment [%R/artifact/%S|%h] to ticket [%S]",
         1891  +             p->zAttachSrc, p->zAttachName, p->zAttachTarget);
  1890   1892         }else{
  1891   1893           zComment = mprintf("Delete attachment \"%h\" from ticket [%.10s]",
  1892   1894                p->zAttachName, p->zAttachTarget);
  1893   1895         }
  1894   1896         db_multi_exec(
  1895   1897           "REPLACE INTO event(type,mtime,objid,user,comment)"
  1896   1898           "VALUES('t',%.17g,%d,%Q,%Q)",

Changes to src/report.c.

   486    486     @
   487    487     @ <li><p>If a column of the result set is named "#" then that column
   488    488     @ is assumed to hold a ticket number.  A hyperlink will be created from
   489    489     @ that column to a detailed view of the ticket.</p></li>
   490    490     @
   491    491     @ <li><p>If a column of the result set is named "bgcolor" then the content
   492    492     @ of that column determines the background color of the row.</p></li>
          493  +  @
          494  +  @ <li><p>The text of all columns prior to the first column whose name begins
          495  +  @ with underscore ("_") is shown character-for-character as it appears in
          496  +  @ the database.  In other words, it is assumed to have a mimetype of
          497  +  @ text/plain.
   493    498     @
   494    499     @ <li><p>The first column whose name begins with underscore ("_") and all
   495         -  @ subsequent columns are shown on their own rows in the table.  This might
   496         -  @ be useful for displaying the description of tickets.
          500  +  @ subsequent columns are shown on their own rows in the table and with
          501  +  @ wiki formatting.  In other words, such rows are shown with a mimetype
          502  +  @ of text/x-fossil-wiki.  This is recommended for the "description" field
          503  +  @ of tickets.
   497    504     @ </p></li>
   498    505     @
   499    506     @ <li><p>The query can join other tables in the database besides TICKET.
   500    507     @ </p></li>
   501    508     @ </ul>
   502    509     @
   503    510     @ <h3>Examples</h3>
................................................................................
   587    594     @    owner AS 'By',
   588    595     @    subsystem AS 'Subsys',
   589    596     @    sdate(changetime) AS 'Changed',
   590    597     @    assignedto AS 'Assigned',
   591    598     @    severity AS 'Svr',
   592    599     @    priority AS 'Pri',
   593    600     @    title AS 'Title',
   594         -  @    description AS '_Description',   -- When the column name begins with '_'
   595         -  @    remarks AS '_Remarks'            -- the data is shown on a separate row.
          601  +  @    description AS '_Description',  -- When the column name begins with '_'
          602  +  @    remarks AS '_Remarks'           -- content is rendered as wiki
   596    603     @  FROM ticket
   597    604     @ </pre></blockquote>
   598    605     @
   599    606     @ <p>Or, to see part of the description on the same row, use the
   600    607     @ <b>wiki()</b> function with some string manipulation. Using the
   601    608     @ <b>tkt()</b> function on the ticket number will also generate a linked
   602    609     @ field, but without the extra <i>edit</i> column:
................................................................................
   617    624   struct GenerateHTML {
   618    625     int rn;          /* Report number */
   619    626     int nCount;      /* Row number */
   620    627     int nCol;        /* Number of columns */
   621    628     int isMultirow;  /* True if multiple table rows per query result row */
   622    629     int iNewRow;     /* Index of first column that goes on separate row */
   623    630     int iBg;         /* Index of column that defines background color */
          631  +  int wikiFlags;   /* Flags passed into wiki_convert() */
          632  +  const char *zWikiStart;    /* HTML before display of multi-line wiki */
          633  +  const char *zWikiEnd;      /* HTML after display of multi-line wiki */
   624    634   };
   625    635   
   626    636   /*
   627    637   ** The callback function for db_query
   628    638   */
   629    639   static int generate_html(
   630    640     void *pUser,     /* Pointer to output state */
................................................................................
   661    671         if( g.perm.Write && azName[i][0]=='#' ){
   662    672           pState->nCol++;
   663    673         }
   664    674         if( !pState->isMultirow ){
   665    675           if( azName[i][0]=='_' ){
   666    676             pState->isMultirow = 1;
   667    677             pState->iNewRow = i;
          678  +          pState->wikiFlags = WIKI_NOBADLINKS;
          679  +          pState->zWikiStart = "";
          680  +          pState->zWikiEnd = "";
          681  +          if( P("plaintext") ){
          682  +            pState->wikiFlags |= WIKI_LINKSONLY;
          683  +            pState->zWikiStart = "<pre class='verbatim'>";
          684  +            pState->zWikiEnd = "</pre>";
          685  +            style_submenu_element("Formatted", "Formatted",
          686  +                                  "%R/rptview?rn=%d", pState->rn);
          687  +          }else{
          688  +            style_submenu_element("Plaintext", "Plaintext",
          689  +                                  "%R/rptview?rn=%d&plaintext", pState->rn);
          690  +          }
   668    691           }else{
   669    692             pState->nCol++;
   670    693           }
   671    694         }
   672    695       }
   673    696   
   674    697       /* The first time this routine is called, output a table header
................................................................................
   726    749       if( pState->iNewRow>=0 && i>=pState->iNewRow ){
   727    750         if( zTid && g.perm.Write ){
   728    751           @ <td valign="top">%z(href("%R/tktedit/%h",zTid))edit</a></td>
   729    752           zTid = 0;
   730    753         }
   731    754         if( zData[0] ){
   732    755           Blob content;
   733         -        @ </tr><tr style="background-color:%h(zBg)"><td colspan=%d(pState->nCol)>
          756  +        @ </tr>
          757  +        @ <tr style="background-color:%h(zBg)"><td colspan=%d(pState->nCol)>
          758  +        @ %s(pState->zWikiStart)
   734    759           blob_init(&content, zData, -1);
   735         -        wiki_convert(&content, 0, WIKI_NOBADLINKS);
          760  +        wiki_convert(&content, 0, pState->wikiFlags);
   736    761           blob_reset(&content);
          762  +        @ %s(pState->zWikiEnd)
   737    763         }
   738    764       }else if( azName[i][0]=='#' ){
   739    765         zTid = zData;
   740    766         @ <td valign="top">%z(href("%R/tktview?name=%h",zData))%h(zData)</a></td>
   741    767       }else if( zData[0]==0 ){
   742    768         @ <td valign="top">&nbsp;</td>
   743    769       }else{

Changes to src/schema.c.

   395    395   @   severity TEXT,
   396    396   @   foundin TEXT,
   397    397   @   private_contact TEXT,
   398    398   @   resolution TEXT,
   399    399   @   title TEXT,
   400    400   @   comment TEXT
   401    401   @ );
          402  +@ CREATE TABLE ticketchng(
          403  +@   -- Do not change any column that begins with tkt_
          404  +@   tkt_id INTEGER REFERENCES ticket,
          405  +@   tkt_mtime DATE,
          406  +@   -- Add as many fields as required below this line
          407  +@   login TEXT,
          408  +@   username TEXT,
          409  +@   mimetype TEXT,
          410  +@   icomment TEXT
          411  +@ );
          412  +@ CREATE INDEX ticketchng_idx1 ON ticketchng(tkt_id, tkt_mtime);
   402    413   ;
   403    414   
   404    415   /*
   405    416   ** Predefined tagid values
   406    417   */
   407    418   #if INTERFACE
   408    419   # define TAG_BGCOLOR    1     /* Set the background color for display */

Changes to src/th.c.

  1146   1146     if( !pValue->zData ){
  1147   1147       Th_ErrorMessage(interp, "no such variable:", zVar, nVar);
  1148   1148       return TH_ERROR;
  1149   1149     }
  1150   1150   
  1151   1151     return Th_SetResult(interp, pValue->zData, pValue->nData);
  1152   1152   }
         1153  +
         1154  +/*
         1155  +** Return true if variable (zVar, nVar) exists.
         1156  +*/
         1157  +int Th_ExistsVar(Th_Interp *interp, const char *zVar, int nVar){
         1158  +  return thFindValue(interp, zVar, nVar, 0, 0)!=0;
         1159  +}
  1153   1160   
  1154   1161   /*
  1155   1162   ** String (zVar, nVar) must contain the name of a scalar variable or
  1156   1163   ** array member. If the variable does not exist it is created. The
  1157   1164   ** variable is set to the value supplied in string (zValue, nValue).
  1158   1165   **
  1159   1166   ** If (zVar, nVar) refers to an existing array, TH_ERROR is returned

Changes to src/th.h.

    47     47   */
    48     48   int Th_Expr(Th_Interp *interp, const char *, int);
    49     49   
    50     50   /* 
    51     51   ** Access TH variables in the current stack frame. If the variable name
    52     52   ** begins with "::", the lookup is in the top level (global) frame. 
    53     53   */
           54  +int Th_ExistsVar(Th_Interp *, const char *, int);
    54     55   int Th_GetVar(Th_Interp *, const char *, int);
    55     56   int Th_SetVar(Th_Interp *, const char *, int, const char *, int);
    56     57   int Th_LinkVar(Th_Interp *, const char *, int, int, const char *, int);
    57     58   int Th_UnsetVar(Th_Interp *, const char *, int);
    58     59   
    59     60   typedef int (*Th_CommandProc)(Th_Interp *, void *, int, const char **, int *);
    60     61   

Changes to src/th_lang.c.

   815    815     Th_SetResult(interp, zByte, nByte);
   816    816     Th_Free(interp, zByte);
   817    817     return TH_OK;
   818    818   }
   819    819   
   820    820   /*
   821    821   ** TH Syntax:
          822  +**
          823  +**   string trim STRING
          824  +**   string trimleft STRING
          825  +**   string trimright STRING
          826  +*/
          827  +static int string_trim_command(
          828  +  Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl
          829  +){
          830  +  int n;
          831  +  const char *z;
          832  +
          833  +  if( argc!=3 ){
          834  +    return Th_WrongNumArgs(interp, "string trim string");
          835  +  }
          836  +  z = argv[2];
          837  +  n = argl[2];
          838  +  if( argl[1]<5 || argv[1][4]=='l' ){
          839  +    while( n && th_isspace(z[0]) ){ z++; n--; }
          840  +  }
          841  +  if( argl[1]<5 || argv[1][4]=='r' ){
          842  +    while( n && th_isspace(z[n-1]) ){ n--; }
          843  +  }
          844  +  Th_SetResult(interp, z, n);
          845  +  return TH_OK;
          846  +}
          847  +
          848  +/*
          849  +** TH Syntax:
   822    850   **
   823    851   **   info exists VAR
   824    852   */
   825    853   static int info_exists_command(
   826    854     Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl
   827    855   ){
   828    856     int rc;
   829    857   
   830    858     if( argc!=3 ){
   831    859       return Th_WrongNumArgs(interp, "info exists var");
   832    860     }
   833         -  rc = Th_GetVar(interp, argv[2], argl[2]);
   834         -  Th_SetResultInt(interp, rc?0:1);
          861  +  rc = Th_ExistsVar(interp, argv[2], argl[2]);
          862  +  Th_SetResultInt(interp, rc);
   835    863     return TH_OK;
   836    864   }
   837    865   
   838    866   /*
   839    867   ** TH Syntax:
   840    868   **
   841    869   **   unset VAR
................................................................................
   895    923       { "compare", string_compare_command },
   896    924       { "first",   string_first_command },
   897    925       { "is",      string_is_command },
   898    926       { "last",    string_last_command },
   899    927       { "length",  string_length_command },
   900    928       { "range",   string_range_command },
   901    929       { "repeat",  string_repeat_command },
          930  +    { "trim",      string_trim_command },
          931  +    { "trimleft",  string_trim_command },
          932  +    { "trimright", string_trim_command },
   902    933       { 0, 0 }
   903    934     };
   904    935     return Th_CallSubCommand(interp, ctx, argc, argv, argl, aSub);
   905    936   }
   906    937   
   907    938   /*
   908    939   ** TH Syntax:

Changes to src/th_main.c.

    16     16   *******************************************************************************
    17     17   **
    18     18   ** This file contains an interface between the TH scripting language
    19     19   ** (an independent project) and fossil.
    20     20   */
    21     21   #include "config.h"
    22     22   #include "th_main.h"
           23  +#include "sqlite3.h"
    23     24   
    24     25   /*
    25     26   ** Global variable counting the number of outstanding calls to malloc()
    26     27   ** made by the th1 implementation. This is used to catch memory leaks
    27     28   ** in the interpreter. Obviously, it also means th1 is not threadsafe.
    28     29   */
    29     30   static int nOutstandingMalloc = 0;
................................................................................
    70     71   static int enableOutputCmd(
    71     72     Th_Interp *interp, 
    72     73     void *p, 
    73     74     int argc, 
    74     75     const char **argv, 
    75     76     int *argl
    76     77   ){
    77         -  if( argc!=2 ){
    78         -    return Th_WrongNumArgs(interp, "enable_output BOOLEAN");
           78  +  int rc;
           79  +  if( argc<2 || argc>3 ){
           80  +    return Th_WrongNumArgs(interp, "enable_output [LABEL] BOOLEAN");
    79     81     }
    80         -  return Th_ToInt(interp, argv[1], argl[1], &enableOutput);
           82  +  rc = Th_ToInt(interp, argv[argc-1], argl[argc-1], &enableOutput);
           83  +  if( g.thTrace ){
           84  +    Th_Trace("enable_output {%.*s} -> %d<br>\n", argl[1],argv[1],enableOutput);
           85  +  }
           86  +  return rc;
    81     87   }
    82     88   
    83     89   /*
    84     90   ** Return a name for a TH1 return code.
    85     91   */
    86     92   const char *Th_ReturnCodeName(int rc){
    87     93     static char zRc[32];
................................................................................
   117    123         fflush(stdout);
   118    124       }
   119    125       if( encode ) free((char*)z);
   120    126     }
   121    127   }
   122    128   
   123    129   static void sendError(const char *z, int n, int forceCgi){
          130  +  int savedEnable = enableOutput;
          131  +  enableOutput = 1;
   124    132     if( forceCgi || g.cgiOutput ){
   125    133       sendText("<hr><p class=\"thmainError\">", -1, 0);
   126    134     }
   127    135     sendText("ERROR: ", -1, 0);
   128    136     sendText((char*)z, n, 1);
   129    137     sendText(forceCgi || g.cgiOutput ? "</p>" : "\n", -1, 0);
          138  +  enableOutput = savedEnable;
   130    139   }
   131    140   
   132    141   /*
   133    142   ** TH command:     puts STRING
   134    143   ** TH command:     html STRING
   135    144   **
   136    145   ** Output STRING escaped for HTML (html) or unchanged (puts).  
................................................................................
   565    574     }
   566    575     sqlite3_randomness(n, aRand);
   567    576     encode16(aRand, zOut, n);
   568    577     Th_SetResult(interp, (const char *)zOut, -1);
   569    578     return TH_OK;
   570    579   }
   571    580   
          581  +/*
          582  +** TH1 command:     query SQL CODE
          583  +**
          584  +** Run the SQL query given by the SQL argument.  For each row in the result
          585  +** set, run CODE.
          586  +**
          587  +** In SQL, parameters such as $var are filled in using the value of variable
          588  +** "var".  Result values are stored in variables with the column name prior
          589  +** to each invocation of CODE.
          590  +*/
          591  +static int queryCmd(
          592  +  Th_Interp *interp,
          593  +  void *p, 
          594  +  int argc, 
          595  +  const char **argv, 
          596  +  int *argl
          597  +){
          598  +  sqlite3_stmt *pStmt;
          599  +  int rc;
          600  +  const char *zSql;
          601  +  int nSql;
          602  +  const char *zTail;
          603  +  int n, i;
          604  +  int res = TH_OK;
          605  +  int nVar;
          606  +
          607  +  if( argc!=3 ){
          608  +    return Th_WrongNumArgs(interp, "query SQL CODE");
          609  +  }
          610  +  if( g.db==0 ){
          611  +    Th_ErrorMessage(interp, "database is not open", 0, 0);
          612  +    return TH_ERROR;
          613  +  }
          614  +  zSql = argv[1];
          615  +  nSql = argl[1];
          616  +  while( res==TH_OK && nSql>0 ){
          617  +    rc = sqlite3_prepare_v2(g.db, argv[1], argl[1], &pStmt, &zTail);
          618  +    if( rc!=0 ){
          619  +      Th_ErrorMessage(interp, "SQL error: ", sqlite3_errmsg(g.db), -1);
          620  +      return TH_ERROR;
          621  +    }
          622  +    n = (int)(zTail - zSql);
          623  +    zSql += n;
          624  +    nSql -= n;
          625  +    if( pStmt==0 ) continue;
          626  +    nVar = sqlite3_bind_parameter_count(pStmt);
          627  +    for(i=1; i<=nVar; i++){
          628  +      const char *zVar = sqlite3_bind_parameter_name(pStmt, i);
          629  +      int szVar = zVar ? th_strlen(zVar) : 0;
          630  +      if( szVar>1 && zVar[0]=='$'
          631  +       && Th_GetVar(interp, zVar+1, szVar-1)==TH_OK ){
          632  +        int nVal;
          633  +        const char *zVal = Th_GetResult(interp, &nVal);
          634  +        sqlite3_bind_text(pStmt, i, zVal, nVal, SQLITE_TRANSIENT);
          635  +      }
          636  +    }
          637  +    while( res==TH_OK && sqlite3_step(pStmt)==SQLITE_ROW ){
          638  +      int nCol = sqlite3_column_count(pStmt);
          639  +      for(i=0; i<nCol; i++){
          640  +        const char *zCol = sqlite3_column_name(pStmt, i);
          641  +        int szCol = th_strlen(zCol);
          642  +        const char *zVal = (const char*)sqlite3_column_text(pStmt, i);
          643  +        int szVal = sqlite3_column_bytes(pStmt, i);
          644  +        Th_SetVar(interp, zCol, szCol, zVal, szVal);
          645  +      }
          646  +      res = Th_Eval(interp, 0, argv[2], argl[2]);
          647  +      if( res==TH_BREAK || res==TH_CONTINUE ) res = TH_OK;
          648  +    }
          649  +    rc = sqlite3_finalize(pStmt);
          650  +    if( rc!=SQLITE_OK ){
          651  +      Th_ErrorMessage(interp, "SQL error: ", sqlite3_errmsg(g.db), -1);
          652  +      return TH_ERROR;
          653  +    }
          654  +  } 
          655  +  return res;
          656  +}
   572    657   
   573    658   /*
   574    659   ** Make sure the interpreter has been initialized.  Initialize it if
   575    660   ** it has not been already.
   576    661   **
   577    662   ** The interpreter is stored in the g.interp global variable.
   578    663   */
................................................................................
   591    676       {"enable_output", enableOutputCmd,      0},
   592    677       {"hascap",        hascapCmd,            0},
   593    678       {"hasfeature",    hasfeatureCmd,        0},
   594    679       {"html",          putsCmd,              (void*)&aFlags[0]},
   595    680       {"htmlize",       htmlizeCmd,           0},
   596    681       {"linecount",     linecntCmd,           0},
   597    682       {"puts",          putsCmd,              (void*)&aFlags[1]},
          683  +    {"query",         queryCmd,             0},
   598    684       {"randhex",       randhexCmd,           0},
   599    685       {"repository",    repositoryCmd,        0},
   600    686       {"stime",         stimeCmd,             0},
   601    687       {"utime",         utimeCmd,             0},
   602    688       {"wiki",          wikiCmd,              (void*)&aFlags[0]},
   603    689       {0, 0, 0}
   604    690     };
................................................................................
   799    885         i = 0;
   800    886         zResult = (char*)Th_GetResult(g.interp, &n);
   801    887         sendText((char*)zResult, n, encode);
   802    888       }else if( z[i]=='<' && isBeginScriptTag(&z[i]) ){
   803    889         sendText(z, i, 0);
   804    890         z += i+5;
   805    891         for(i=0; z[i] && (z[i]!='<' || !isEndScriptTag(&z[i])); i++){}
          892  +      if( g.thTrace ){
          893  +        Th_Trace("eval {<pre>%#h</pre>}<br>", i, z);
          894  +      }
   806    895         rc = Th_Eval(g.interp, 0, (const char*)z, i);
   807    896         if( rc!=TH_OK ) break;
   808    897         z += i;
   809    898         if( z[0] ){ z += 6; }
   810    899         i = 0;
   811    900       }else{
   812    901         i++;

Changes to src/timeline.c.

   342    342         }
   343    343       }else if( zType[0]=='e' && tagid ){
   344    344         hyperlink_to_event_tagid(tagid<0?-tagid:tagid);
   345    345       }else if( (tmFlags & TIMELINE_ARTID)!=0 ){
   346    346         hyperlink_to_uuid(zUuid);
   347    347       }
   348    348       db_column_blob(pQuery, commentColumn, &comment);
   349         -    if( mxWikiLen>0 && blob_size(&comment)>mxWikiLen ){
          349  +    if( zType[0]!='c' ){
          350  +      /* Comments for anything other than a check-in are generated by
          351  +      ** "fossil rebuild" and expect to be rendered as text/x-fossil-wiki */
          352  +      wiki_convert(&comment, 0, WIKI_INLINE);
          353  +    }else if( mxWikiLen>0 && blob_size(&comment)>mxWikiLen ){
   350    354         Blob truncated;
   351    355         blob_zero(&truncated);
   352    356         blob_append(&truncated, blob_buffer(&comment), mxWikiLen);
   353    357         blob_append(&truncated, "...", 3);
   354    358         @ %w(blob_str(&truncated))
   355    359         blob_reset(&truncated);
   356    360       }else{

Changes to src/tkt.c.

    24     24   
    25     25   /*
    26     26   ** The list of database user-defined fields in the TICKET table.
    27     27   ** The real table also contains some addition fields for internal
    28     28   ** used.  The internal-use fields begin with "tkt_".
    29     29   */
    30     30   static int nField = 0;
    31         -static char **azField = 0;    /* Names of database fields */
    32         -static char **azValue = 0;    /* Original values */
    33         -static char **azAppend = 0;   /* Value to be appended */
           31  +static struct tktFieldInfo {
           32  +  char *zName;             /* Name of the database field */
           33  +  char *zValue;            /* Value to store */
           34  +  char *zAppend;           /* Value to append */
           35  +  unsigned mUsed;          /* 01: TICKET  02: TICKETCHNG */
           36  +} *aField;
           37  +#define USEDBY_TICKET      01
           38  +#define USEDBY_TICKETCHNG  02
           39  +static int haveTicket = 0;     /* True if the TICKET table exists */
           40  +static int haveTicketChng = 0; /* True if the TICKETCHNG table exists */
    34     41   
    35     42   /*
    36         -** Compare two entries in azField for sorting purposes
           43  +** Compare two entries in aField[] for sorting purposes
    37     44   */
    38     45   static int nameCmpr(const void *a, const void *b){
    39         -  return fossil_strcmp(*(char**)a, *(char**)b);
           46  +  return fossil_strcmp(((const struct tktFieldInfo*)a)->zName,
           47  +                       ((const struct tktFieldInfo*)b)->zName);
    40     48   }
    41     49   
    42     50   /*
    43         -** Obtain a list of all fields of the TICKET table.  Put them 
    44         -** in sorted order in azField[].
           51  +** Return the index into aField[] of the given field name.
           52  +** Return -1 if zFieldName is not in aField[].
           53  +*/
           54  +static int fieldId(const char *zFieldName){
           55  +  int i;
           56  +  for(i=0; i<nField; i++){
           57  +    if( fossil_strcmp(aField[i].zName, zFieldName)==0 ) return i;
           58  +  }
           59  +  return -1;
           60  +}
           61  +
           62  +/*
           63  +** Obtain a list of all fields of the TICKET and TICKETCHNG tables.  Put them 
           64  +** in sorted order in aField[].
    45     65   **
    46         -** Also allocate space for azValue[] and azAppend[] and initialize
    47         -** all the values there to zero.
           66  +** The haveTicket and haveTicketChng variables are set to 1 if the TICKET and
           67  +** TICKETCHANGE tables exist, respectively.
    48     68   */
    49     69   static void getAllTicketFields(void){
    50     70     Stmt q;
    51     71     int i;
    52         -  if( nField>0 ) return;
           72  +  static int once = 0;
           73  +  if( once ) return;
           74  +  once = 1;
    53     75     db_prepare(&q, "PRAGMA table_info(ticket)");
    54     76     while( db_step(&q)==SQLITE_ROW ){
    55         -    const char *zField = db_column_text(&q, 1);
    56         -    if( strncmp(zField,"tkt_",4)==0 ) continue;
           77  +    const char *zFieldName = db_column_text(&q, 1);
           78  +    haveTicket = 1;
           79  +    if( memcmp(zFieldName,"tkt_",4)==0 ) continue;
           80  +    if( nField%10==0 ){
           81  +      aField = fossil_realloc(aField, sizeof(aField[0])*(nField+10) );
           82  +    }
           83  +    aField[nField].zName = mprintf("%s", zFieldName);
           84  +    aField[nField].mUsed = USEDBY_TICKET;
           85  +    nField++;
           86  +  }
           87  +  db_finalize(&q);
           88  +  db_prepare(&q, "PRAGMA table_info(ticketchng)");
           89  +  while( db_step(&q)==SQLITE_ROW ){
           90  +    const char *zFieldName = db_column_text(&q, 1);
           91  +    haveTicketChng = 1;
           92  +    if( memcmp(zFieldName,"tkt_",4)==0 ) continue;
           93  +    if( (i = fieldId(zFieldName))>=0 ){
           94  +      aField[i].mUsed |= USEDBY_TICKETCHNG;
           95  +      continue;
           96  +    }
    57     97       if( nField%10==0 ){
    58         -      azField = fossil_realloc(azField, sizeof(azField)*3*(nField+10) );
           98  +      aField = fossil_realloc(aField, sizeof(aField[0])*(nField+10) );
    59     99       }
    60         -    azField[nField] = mprintf("%s", zField);
          100  +    aField[nField].zName = mprintf("%s", zFieldName);
          101  +    aField[nField].mUsed = USEDBY_TICKETCHNG;
    61    102       nField++;
    62    103     }
    63    104     db_finalize(&q);
    64         -  qsort(azField, nField, sizeof(azField[0]), nameCmpr);
    65         -  azAppend = &azField[nField];
    66         -  memset(azAppend, 0, sizeof(azAppend[0])*nField);
    67         -  azValue = &azAppend[nField];
          105  +  qsort(aField, nField, sizeof(aField[0]), nameCmpr);
    68    106     for(i=0; i<nField; i++){
    69         -    azValue[i] = "";
          107  +    aField[i].zValue = "";
          108  +    aField[i].zAppend = 0;
    70    109     }
    71    110   }
    72    111   
    73         -/*
    74         -** Return the index into azField[] of the given field name.
    75         -** Return -1 if zField is not in azField[].
    76         -*/
    77         -static int fieldId(const char *zField){
    78         -  int i;
    79         -  for(i=0; i<nField; i++){
    80         -    if( fossil_strcmp(azField[i], zField)==0 ) return i;
    81         -  }
    82         -  return -1;
    83         -}
    84         -
    85    112   /*
    86    113   ** Query the database for all TICKET fields for the specific
    87    114   ** ticket whose name is given by the "name" CGI parameter.
    88    115   ** Load the values for all fields into the interpreter.
    89    116   **
    90    117   ** Only load those fields which do not already exist as
    91    118   ** variables.
................................................................................
   112    139         const char *zName = db_column_name(&q, i);
   113    140         char *zRevealed = 0;
   114    141         if( zVal==0 ){
   115    142           zVal = "";
   116    143         }else if( strncmp(zName, "private_", 8)==0 ){
   117    144           zVal = zRevealed = db_reveal(zVal);
   118    145         }
   119         -      for(j=0; j<nField; j++){
   120         -        if( fossil_strcmp(azField[j],zName)==0 ){
   121         -          azValue[j] = mprintf("%s", zVal);
   122         -          break;
   123         -        }
   124         -      }
   125         -      if( Th_Fetch(zName, &size)==0 ){
          146  +      if( (j = fieldId(zName))>=0 ){
          147  +        aField[j].zValue = mprintf("%s", zVal);
          148  +      }else if( memcmp(zName, "tkt_", 4)==0 && Th_Fetch(zName, &size)==0 ){
   126    149           Th_Store(zName, zVal);
   127    150         }
   128    151         free(zRevealed);
   129    152       }
   130         -  }else{
   131         -    db_finalize(&q);
   132         -    db_prepare(&q, "PRAGMA table_info(ticket)");
   133         -    if( Th_Fetch("tkt_uuid",&size)==0 ){
   134         -      Th_Store("tkt_uuid",zName);
   135         -    }
   136         -    while( db_step(&q)==SQLITE_ROW ){
   137         -      const char *zField = db_column_text(&q, 1);
   138         -      if( Th_Fetch(zField, &size)==0 ){
   139         -        Th_Store(zField, "");
   140         -      }
   141         -    }
   142         -    if( Th_Fetch("tkt_datetime",&size)==0 ){
   143         -      Th_Store("tkt_datetime","");
   144         -    }
   145    153     }
   146    154     db_finalize(&q);
          155  +  for(i=0; i<nField; i++){
          156  +    if( Th_Fetch(aField[i].zName, &size)==0 ){
          157  +      Th_Store(aField[i].zName, aField[i].zValue);
          158  +    }
          159  +  }
   147    160   }
   148    161   
   149    162   /*
   150    163   ** Transfer all CGI parameters to variables in the interpreter.
   151    164   */
   152    165   static void initializeVariablesFromCGI(void){
   153    166     int i;
................................................................................
   155    168   
   156    169     for(i=0; (z = cgi_parameter_name(i))!=0; i++){
   157    170       Th_Store(z, P(z));
   158    171     }
   159    172   }
   160    173   
   161    174   /*
   162         -** Update an entry of the TICKET table according to the information
   163         -** in the control file given in p.  Attempt to create the appropriate
   164         -** TICKET table entry if createFlag is true.  If createFlag is false,
   165         -** that means we already know the entry exists and so we can save the
   166         -** work of trying to create it.
          175  +** Update an entry of the TICKET and TICKETCHNG tables according to the
          176  +** information in the ticket artifact given in p.  Attempt to create
          177  +** the appropriate TICKET table entry if tktid is zero.  If tktid is nonzero
          178  +** then it will be the ROWID of an existing TICKET entry.
   167    179   **
   168         -** Return TRUE if a new TICKET entry was created and FALSE if an
   169         -** existing entry was revised.
          180  +** Parameter rid is the recordID for the ticket artifact in the BLOB table.
          181  +**
          182  +** Return the new rowid of the TICKET table entry.
   170    183   */
   171         -int ticket_insert(const Manifest *p, int createFlag, int rid){
   172         -  Blob sql;
          184  +static int ticket_insert(const Manifest *p, int rid, int tktid){
          185  +  Blob sql1, sql2, sql3;
   173    186     Stmt q;
   174         -  int i;
   175         -  int rc = 0;
          187  +  int i, j;
   176    188   
   177         -  getAllTicketFields();
   178         -  if( createFlag ){  
   179         -    db_multi_exec("INSERT OR IGNORE INTO ticket(tkt_uuid, tkt_mtime) "
          189  +  if( tktid==0 ){
          190  +    db_multi_exec("INSERT INTO ticket(tkt_uuid, tkt_mtime) "
   180    191                     "VALUES(%Q, 0)", p->zTicketUuid);
   181         -    rc = db_changes();
          192  +    tktid = db_last_insert_rowid();
   182    193     }
   183         -  blob_zero(&sql);
   184         -  blob_appendf(&sql, "UPDATE OR REPLACE ticket SET tkt_mtime=:mtime");
          194  +  blob_zero(&sql1);
          195  +  blob_zero(&sql2);
          196  +  blob_zero(&sql3);
          197  +  blob_appendf(&sql1, "UPDATE OR REPLACE ticket SET tkt_mtime=:mtime");
   185    198     for(i=0; i<p->nField; i++){
   186    199       const char *zName = p->aField[i].zName;
   187    200       if( zName[0]=='+' ){
   188    201         zName++;
   189         -      if( fieldId(zName)<0 ) continue;
   190         -      blob_appendf(&sql,", %s=coalesce(%s,'') || %Q",
   191         -                   zName, zName, p->aField[i].zValue);
          202  +      if( (j = fieldId(zName))<0 ) continue;
          203  +      if( aField[j].mUsed & USEDBY_TICKET ){
          204  +        blob_appendf(&sql1,", %s=coalesce(%s,'') || %Q",
          205  +                     zName, zName, p->aField[i].zValue);
          206  +      }
   192    207       }else{
   193         -      if( fieldId(zName)<0 ) continue;
   194         -      blob_appendf(&sql,", %s=%Q", zName, p->aField[i].zValue);
          208  +      if( (j = fieldId(zName))<0 ) continue;
          209  +      if( aField[j].mUsed & USEDBY_TICKET ){
          210  +        blob_appendf(&sql1,", %s=%Q", zName, p->aField[i].zValue);
          211  +      }
          212  +    }
          213  +    if( aField[j].mUsed & USEDBY_TICKETCHNG ){
          214  +      blob_appendf(&sql2, ",%s", zName);
          215  +      blob_appendf(&sql3, ",%Q", p->aField[i].zValue);
   195    216       }
   196    217       if( rid>0 ){
   197    218         wiki_extract_links(p->aField[i].zValue, rid, 1, p->rDate, i==0, 0);
   198    219       }
   199    220     }
   200         -  blob_appendf(&sql, " WHERE tkt_uuid='%s' AND tkt_mtime<:mtime",
   201         -                     p->zTicketUuid);
   202         -  db_prepare(&q, "%s", blob_str(&sql));
          221  +  blob_appendf(&sql1, " WHERE tkt_id=%d", tktid);
          222  +  db_prepare(&q, "%s", blob_str(&sql1));
   203    223     db_bind_double(&q, ":mtime", p->rDate);
   204    224     db_step(&q);
   205    225     db_finalize(&q);
   206         -  blob_reset(&sql);
   207         -  return rc;
          226  +  blob_reset(&sql1);
          227  +  if( blob_size(&sql2)>0 ){
          228  +    db_prepare(&q, "INSERT INTO ticketchng(tkt_id,tkt_mtime%s)"
          229  +                   "VALUES(%d,:mtime%s)",
          230  +                  blob_str(&sql2), tktid, blob_str(&sql3));
          231  +    db_bind_double(&q, ":mtime", p->rDate);
          232  +    db_step(&q);
          233  +    db_finalize(&q);
          234  +  }
          235  +  blob_reset(&sql2);
          236  +  blob_reset(&sql3);
          237  +  return tktid;
   208    238   }
   209    239   
   210    240   /*
   211    241   ** Rebuild an entire entry in the TICKET table
   212    242   */
   213    243   void ticket_rebuild_entry(const char *zTktUuid){
   214    244     char *zTag = mprintf("tkt-%s", zTktUuid);
   215    245     int tagid = tag_findid(zTag, 1);
   216    246     Stmt q;
   217    247     Manifest *pTicket;
          248  +  int tktid;
   218    249     int createFlag = 1;
   219    250   
   220         -  fossil_free(zTag);  
   221         -  db_multi_exec(
   222         -     "DELETE FROM ticket WHERE tkt_uuid=%Q", zTktUuid
   223         -  );
          251  +  fossil_free(zTag);
          252  +  getAllTicketFields();
          253  +  if( haveTicket==0 ) return;
          254  +  tktid = db_int(0, "SELECT tkt_id FROM ticket WHERE tkt_uuid=%Q", zTktUuid);
          255  +  if( haveTicketChng ){
          256  +    db_multi_exec("DELETE FROM ticketchng WHERE tkt_id=%d;", tktid);
          257  +  }
          258  +  db_multi_exec("DELETE FROM ticket WHERE tkt_id=%d", tktid);
          259  +  tktid = 0;
   224    260     db_prepare(&q, "SELECT rid FROM tagxref WHERE tagid=%d ORDER BY mtime",tagid);
   225    261     while( db_step(&q)==SQLITE_ROW ){
   226    262       int rid = db_column_int(&q, 0);
   227    263       pTicket = manifest_get(rid, CFTYPE_TICKET);
   228    264       if( pTicket ){
   229         -      ticket_insert(pTicket, createFlag, rid);
          265  +      tktid = ticket_insert(pTicket, rid, tktid);
   230    266         manifest_ticket_event(rid, pTicket, createFlag, tagid);
   231    267         manifest_destroy(pTicket);
   232    268       }
   233    269       createFlag = 0;
   234    270     }
   235    271     db_finalize(&q);
   236    272   }
   237    273   
   238    274   /*
   239         -** Create the subscript interpreter and load the "common" code.
          275  +** Create the TH1 interpreter and load the "common" code.
   240    276   */
   241    277   void ticket_init(void){
   242    278     const char *zConfig;
   243    279     Th_FossilInit(0, 0);
   244    280     zConfig = ticket_common_code();
   245    281     Th_Eval(g.interp, 0, zConfig, -1);
   246    282   }
   247    283   
   248    284   /*
   249         -** Create the subscript interpreter and load the "change" code.
          285  +** Create the TH1 interpreter and load the "change" code.
   250    286   */
   251    287   int ticket_change(void){
   252    288     const char *zConfig;
   253    289     Th_FossilInit(0, 0);
   254    290     zConfig = ticket_change_code();
   255    291     return Th_Eval(g.interp, 0, zConfig, -1);
   256    292   }
   257    293   
   258    294   /*
   259         -** Recreate the ticket table.
          295  +** Recreate the TICKET and TICKETCHNG tables.
   260    296   */
   261    297   void ticket_create_table(int separateConnection){
   262    298     const char *zSql;
   263    299   
   264         -  db_multi_exec("DROP TABLE IF EXISTS ticket;");
          300  +  db_multi_exec(
          301  +    "DROP TABLE IF EXISTS ticket;"
          302  +    "DROP TABLE IF EXISTS ticketchng;"
          303  +  );
   265    304     zSql = ticket_table_schema();
   266    305     if( separateConnection ){
          306  +    db_end_transaction(0);
   267    307       db_init_database(g.zRepositoryName, zSql, 0);
   268    308     }else{
   269    309       db_multi_exec("%s", zSql);
   270    310     }
   271    311   }
   272    312   
   273    313   /*
   274         -** Repopulate the ticket table
          314  +** Repopulate the TICKET and TICKETCHNG tables from scratch using all
          315  +** available ticket artifacts.
   275    316   */
   276    317   void ticket_rebuild(void){
   277    318     Stmt q;
   278    319     ticket_create_table(1);
   279    320     db_begin_transaction();
   280    321     db_prepare(&q,"SELECT tagname FROM tag WHERE tagname GLOB 'tkt-*'");
   281    322     while( db_step(&q)==SQLITE_ROW ){
................................................................................
   285    326       len = strlen(zName);
   286    327       if( len<20 || !validate16(zName, len) ) continue;
   287    328       ticket_rebuild_entry(zName);
   288    329     }
   289    330     db_finalize(&q);
   290    331     db_end_transaction(0);
   291    332   }
          333  +
          334  +/*
          335  +** For trouble-shooting purposes, render a dump of the aField[] table to
          336  +** the webpage currently under construction.
          337  +*/
          338  +static void showAllFields(void){
          339  +  int i;
          340  +  @ <font color="blue">
          341  +  @ <p>Database fields:</p><ul>
          342  +  for(i=0; i<nField; i++){
          343  +    @ <li>aField[%d(i)].zName = "%h(aField[i].zName)";
          344  +    @ originally = "%h(aField[i].zValue)";
          345  +    @ currently = "%h(PD(aField[i].zName,""))"";
          346  +    if( aField[i].zAppend ){
          347  +      @ zAppend = "%h(aField[i].zAppend)";
          348  +    }
          349  +    @ mUsed = %d(aField[i].mUsed);
          350  +  }
          351  +  @ </ul></font>
          352  +}
   292    353   
   293    354   /*
   294    355   ** WEBPAGE: tktview
   295    356   ** URL:  tktview?name=UUID
   296    357   **
   297    358   ** View a ticket.
   298    359   */
................................................................................
   320    381           "%s/tktnew", g.zTop);
   321    382     }
   322    383     if( g.perm.ApndTkt && g.perm.Attach ){
   323    384       style_submenu_element("Attach", "Add An Attachment",
   324    385           "%s/attachadd?tkt=%T&from=%s/tktview/%t",
   325    386           g.zTop, zUuid, g.zTop, zUuid);
   326    387     }
          388  +  if( P("plaintext") ){
          389  +    style_submenu_element("Formatted", "Formatted", "%R/tktview/%S", zUuid);
          390  +  }else{
          391  +    style_submenu_element("Plaintext", "Plaintext",
          392  +                          "%R/tktview/%S?plaintext", zUuid);
          393  +  }
   327    394     style_header("View Ticket");
   328    395     if( g.thTrace ) Th_Trace("BEGIN_TKTVIEW<br />\n", -1);
   329    396     ticket_init();
          397  +  initializeVariablesFromCGI();
          398  +  getAllTicketFields();
   330    399     initializeVariablesFromDb();
   331    400     zScript = ticket_viewpage_code();
          401  +  if( P("showfields")!=0 ) showAllFields();
   332    402     if( g.thTrace ) Th_Trace("BEGIN_TKTVIEW_SCRIPT<br />\n", -1);
   333    403     Th_Render(zScript);
   334    404     if( g.thTrace ) Th_Trace("END_TKTVIEW<br />\n", -1);
   335    405   
   336    406     zFullName = db_text(0, 
   337    407          "SELECT tkt_uuid FROM ticket"
   338    408          " WHERE tkt_uuid GLOB '%q*'", zUuid);
................................................................................
   364    434       return Th_WrongNumArgs(interp, "append_field FIELD STRING");
   365    435     }
   366    436     if( g.thTrace ){
   367    437       Th_Trace("append_field %#h {%#h}<br />\n",
   368    438                 argl[1], argv[1], argl[2], argv[2]);
   369    439     }
   370    440     for(idx=0; idx<nField; idx++){
   371         -    if( strncmp(azField[idx], argv[1], argl[1])==0
   372         -        && azField[idx][argl[1]]==0 ){
          441  +    if( memcmp(aField[idx].zName, argv[1], argl[1])==0
          442  +        && aField[idx].zName[argl[1]]==0 ){
   373    443         break;
   374    444       }
   375    445     }
   376    446     if( idx>=nField ){
   377    447       Th_ErrorMessage(g.interp, "no such TICKET column: ", argv[1], argl[1]);
   378    448       return TH_ERROR;
   379    449     }
   380         -  azAppend[idx] = mprintf("%.*s", argl[2], argv[2]);
          450  +  aField[idx].zAppend = mprintf("%.*s", argl[2], argv[2]);
   381    451     return TH_OK;
   382    452   }
   383    453   
   384    454   /*
   385    455   ** Write a ticket into the repository.
   386    456   */
   387    457   static void ticket_put(
................................................................................
   438    508     }
   439    509     zUuid = (const char *)pUuid;
   440    510     blob_zero(&tktchng);
   441    511     zDate = date_in_standard_format("now");
   442    512     blob_appendf(&tktchng, "D %s\n", zDate);
   443    513     free(zDate);
   444    514     for(i=0; i<nField; i++){
   445         -    if( azAppend[i] ){
   446         -      blob_appendf(&tktchng, "J +%s %z\n", azField[i],
   447         -                   fossilize(azAppend[i], -1));
          515  +    if( aField[i].zAppend ){
          516  +      blob_appendf(&tktchng, "J +%s %z\n", aField[i].zName,
          517  +                   fossilize(aField[i].zAppend, -1));
   448    518         ++nJ;
   449    519       }
   450    520     }
   451    521     for(i=0; i<nField; i++){
   452    522       const char *zValue;
   453    523       int nValue;
   454         -    if( azAppend[i] ) continue;
   455         -    zValue = Th_Fetch(azField[i], &nValue);
          524  +    if( aField[i].zAppend ) continue;
          525  +    zValue = Th_Fetch(aField[i].zName, &nValue);
   456    526       if( zValue ){
   457    527         while( nValue>0 && fossil_isspace(zValue[nValue-1]) ){ nValue--; }
   458         -      if( strncmp(zValue, azValue[i], nValue) || strlen(azValue[i])!=nValue ){
   459         -        if( strncmp(azField[i], "private_", 8)==0 ){
          528  +      if( ((aField[i].mUsed & USEDBY_TICKETCHNG)!=0 && nValue>0)
          529  +       || memcmp(zValue, aField[i].zValue, nValue)!=0
          530  +       || strlen(aField[i].zValue)!=nValue
          531  +      ){
          532  +        if( memcmp(aField[i].zName, "private_", 8)==0 ){
   460    533             zValue = db_conceal(zValue, nValue);
   461         -          blob_appendf(&tktchng, "J %s %s\n", azField[i], zValue);
          534  +          blob_appendf(&tktchng, "J %s %s\n", aField[i].zName, zValue);
   462    535           }else{
   463         -          blob_appendf(&tktchng, "J %s %#F\n", azField[i], nValue, zValue);
          536  +          blob_appendf(&tktchng, "J %s %#F\n", aField[i].zName, nValue, zValue);
   464    537           }
   465    538           nJ++;
   466    539         }
   467    540       }
   468    541     }
   469    542     if( *(char**)pUuid ){
   470    543       zUuid = db_text(0, 
................................................................................
   521    594     if( !g.perm.NewTkt ){ login_needed(); return; }
   522    595     if( P("cancel") ){
   523    596       cgi_redirect("home");
   524    597     }
   525    598     style_header("New Ticket");
   526    599     if( g.thTrace ) Th_Trace("BEGIN_TKTNEW<br />\n", -1);
   527    600     ticket_init();
          601  +  initializeVariablesFromCGI();
   528    602     getAllTicketFields();
   529    603     initializeVariablesFromDb();
   530         -  initializeVariablesFromCGI();
          604  +  if( g.zPath[0]=='d' ) showAllFields();
   531    605     form_begin(0, "%R/%s", g.zPath);
   532    606     login_insert_csrf_secret();
   533    607     if( P("date_override") && g.perm.Setup ){
   534    608       @ <input type="hidden" name="date_override" value="%h(P("date_override"))">
   535    609     }
   536         -  @ </p>
   537    610     zScript = ticket_newpage_code();
   538    611     Th_Store("login", g.zLogin ? g.zLogin : "nobody");
   539    612     Th_Store("date", db_text(0, "SELECT datetime('now')"));
   540    613     Th_CreateCommand(g.interp, "submit_ticket", submitTicketCmd,
   541    614                      (void*)&zNewUuid, 0);
   542    615     if( g.thTrace ) Th_Trace("BEGIN_TKTNEW_SCRIPT<br />\n", -1);
   543    616     if( Th_Render(zScript)==TH_RETURN && !g.thTrace && zNewUuid ){
................................................................................
   594    667       return;
   595    668     }
   596    669     if( g.thTrace ) Th_Trace("BEGIN_TKTEDIT<br />\n", -1);
   597    670     ticket_init();
   598    671     getAllTicketFields();
   599    672     initializeVariablesFromCGI();
   600    673     initializeVariablesFromDb();
          674  +  if( g.zPath[0]=='d' ) showAllFields();
   601    675     form_begin(0, "%R/%s", g.zPath);
   602    676     @ <input type="hidden" name="name" value="%s(zName)" />
   603    677     login_insert_csrf_secret();
   604         -  @ </p>
   605    678     zScript = ticket_editpage_code();
   606    679     Th_Store("login", g.zLogin ? g.zLogin : "nobody");
   607    680     Th_Store("date", db_text(0, "SELECT datetime('now')"));
   608    681     Th_CreateCommand(g.interp, "append_field", appendRemarkCmd, 0, 0);
   609    682     Th_CreateCommand(g.interp, "submit_ticket", submitTicketCmd, (void*)&zName,0);
   610    683     if( g.thTrace ) Th_Trace("BEGIN_TKTEDIT_SCRIPT<br />\n", -1);
   611    684     if( Th_Render(zScript)==TH_RETURN && !g.thTrace && zName ){
................................................................................
   633    706       rc = sqlite3_exec(db, zSchema, 0, 0, &zErr);
   634    707       if( rc!=SQLITE_OK ){
   635    708         sqlite3_close(db);
   636    709         return zErr;
   637    710       }
   638    711       rc = sqlite3_exec(db, "SELECT tkt_id, tkt_uuid, tkt_mtime FROM ticket",
   639    712                         0, 0, 0);
   640         -    sqlite3_close(db);
   641    713       if( rc!=SQLITE_OK ){
   642         -      zErr = mprintf("schema fails to define a valid ticket table "
   643         -                     "containing all required fields");
   644         -      return zErr;
          714  +      zErr = mprintf("schema fails to define valid a TICKET "
          715  +                     "table containing all required fields");
          716  +    }else{
          717  +      rc = sqlite3_exec(db, "SELECT tkt_id, tkt_mtime FROM ticketchng", 0,0,0);
          718  +      if( rc!=SQLITE_OK ){
          719  +        zErr = mprintf("schema fails to define valid a TICKETCHNG "
          720  +                       "table containing all required fields");
          721  +      }
   645    722       }
          723  +    sqlite3_close(db);
   646    724     }
   647         -  return 0;
          725  +  return zErr;
   648    726   }
   649    727   
   650    728   /*
   651    729   ** WEBPAGE: tkttimeline
   652    730   ** URL: /tkttimeline?name=TICKETUUID&y=TYPE
   653    731   **
   654    732   ** Show the change history for a single ticket in timeline format.
................................................................................
   732    810   ** Show the complete change history for a single ticket
   733    811   */
   734    812   void tkthistory_page(void){
   735    813     Stmt q;
   736    814     char *zTitle;
   737    815     const char *zUuid;
   738    816     int tagid;
          817  +  int nChng = 0;
   739    818   
   740    819     login_check_credentials();
   741    820     if( !g.perm.Hyperlink || !g.perm.RdTkt ){ login_needed(); return; }
   742    821     zUuid = PD("name","");
   743    822     zTitle = mprintf("History Of Ticket %h", zUuid);
   744    823     style_submenu_element("Status", "Status",
   745    824       "%s/info/%s", g.zTop, zUuid);
   746    825     style_submenu_element("Check-ins", "Check-ins",
   747    826       "%s/tkttimeline?name=%s&y=ci", g.zTop, zUuid);
   748    827     style_submenu_element("Timeline", "Timeline",
   749    828       "%s/tkttimeline?name=%s", g.zTop, zUuid);
          829  +  if( P("plaintext")!=0 ){
          830  +    style_submenu_element("Formatted", "Formatted",
          831  +                          "%R/tkthistory/%S", zUuid);
          832  +  }else{
          833  +    style_submenu_element("Plaintext", "Plaintext",
          834  +                          "%R/tkthistory/%S?plaintext", zUuid);
          835  +  }
   750    836     style_header(zTitle);
   751    837     free(zTitle);
   752    838   
   753    839     tagid = db_int(0, "SELECT tagid FROM tag WHERE tagname GLOB 'tkt-%q*'",zUuid);
   754    840     if( tagid==0 ){
   755    841       @ No such ticket: %h(zUuid)
   756    842       style_footer();
................................................................................
   762    848       " WHERE objid IN (SELECT rid FROM tagxref WHERE tagid=%d)"
   763    849       "   AND blob.rid=event.objid"
   764    850       " UNION "
   765    851       "SELECT datetime(mtime,'localtime'), attachid, uuid, src, filename, user"
   766    852       "  FROM attachment, blob"
   767    853       " WHERE target=(SELECT substr(tagname,5) FROM tag WHERE tagid=%d)"
   768    854       "   AND blob.rid=attachid"
   769         -    " ORDER BY 1 DESC",
          855  +    " ORDER BY 1",
   770    856       tagid, tagid
   771    857     );
   772    858     while( db_step(&q)==SQLITE_ROW ){
   773    859       Manifest *pTicket;
   774    860       char zShort[12];
   775    861       const char *zDate = db_column_text(&q, 0);
   776    862       int rid = db_column_int(&q, 1);
   777    863       const char *zChngUuid = db_column_text(&q, 2);
   778    864       const char *zFile = db_column_text(&q, 4);
   779    865       memcpy(zShort, zChngUuid, 10);
   780    866       zShort[10] = 0;
          867  +    if( nChng==0 ){
          868  +      @ <ol>
          869  +    }
          870  +    nChng++;
   781    871       if( zFile!=0 ){
   782    872         const char *zSrc = db_column_text(&q, 3);
   783    873         const char *zUser = db_column_text(&q, 5);
   784    874         if( zSrc==0 || zSrc[0]==0 ){
   785    875           @ 
   786         -        @ <p>Delete attachment "%h(zFile)"
          876  +        @ <li><p>Delete attachment "%h(zFile)"
   787    877         }else{
   788    878           @ 
   789         -        @ <p>Add attachment "%h(zFile)"
          879  +        @ <li><p>Add attachment
          880  +        @ "%z(href("%R/artifact/%S",zSrc))%h(zFile)</a>"
   790    881         }
   791    882         @ [%z(href("%R/artifact/%T",zChngUuid))%s(zShort)</a>]
   792    883         @ (rid %d(rid)) by
   793    884         hyperlink_to_user(zUser,zDate," on");
   794    885         hyperlink_to_date(zDate, ".</p>");
   795    886       }else{
   796    887         pTicket = manifest_get(rid, CFTYPE_TICKET);
   797    888         if( pTicket ){
   798    889           @
   799         -        @ <p>Ticket change
          890  +        @ <li><p>Ticket change
   800    891           @ [%z(href("%R/artifact/%T",zChngUuid))%s(zShort)</a>]
   801    892           @ (rid %d(rid)) by
   802    893           hyperlink_to_user(pTicket->zUser,zDate," on");
   803    894           hyperlink_to_date(zDate, ":");
   804    895           @ </p>
   805         -        ticket_output_change_artifact(pTicket);
          896  +        ticket_output_change_artifact(pTicket, "a");
   806    897         }
   807    898         manifest_destroy(pTicket);
   808    899       }
   809    900     }
   810    901     db_finalize(&q);
          902  +  if( nChng ){
          903  +    @ </ol>
          904  +  }
   811    905     style_footer();
   812    906   }
   813    907   
   814    908   /*
   815    909   ** Return TRUE if the given BLOB contains a newline character.
   816    910   */
   817    911   static int contains_newline(Blob *p){
................................................................................
   823    917     return 0;
   824    918   }
   825    919   
   826    920   /*
   827    921   ** The pTkt object is a ticket change artifact.  Output a detailed
   828    922   ** description of this object.
   829    923   */
   830         -void ticket_output_change_artifact(Manifest *pTkt){
          924  +void ticket_output_change_artifact(Manifest *pTkt, const char *zListType){
   831    925     int i;
   832         -  @ <ol>
          926  +  int wikiFlags = WIKI_NOBADLINKS;
          927  +  const char *zBlock = "<blockquote>";
          928  +  const char *zEnd = "</blockquote>";
          929  +  if( P("plaintext")!=0 ){
          930  +    wikiFlags |= WIKI_LINKSONLY;
          931  +    zBlock = "<blockquote><pre class='verbatim'>";
          932  +    zEnd = "</pre></blockquote>";
          933  +  }
          934  +  if( zListType==0 ) zListType = "1";
          935  +  @ <ol type="%s(zListType)">
   833    936     for(i=0; i<pTkt->nField; i++){
   834    937       Blob val;
   835    938       const char *z;
   836    939       z = pTkt->aField[i].zName;
   837    940       blob_set(&val, pTkt->aField[i].zValue);
   838    941       if( z[0]=='+' ){
   839         -      @ <li>Appended to %h(&z[1]):<blockquote>
   840         -      wiki_convert(&val, 0, WIKI_NOBADLINKS);
   841         -      @ </blockquote></li>
   842         -    }else if( blob_size(&val)<=50 && contains_newline(&val) ){
   843         -      @ <li>Change %h(z) to:<blockquote>
   844         -      wiki_convert(&val, 0, WIKI_NOBADLINKS);
   845         -      @ </blockquote></li>
          942  +      @ <li>Appended to %h(&z[1]):%s(zBlock)
          943  +      wiki_convert(&val, 0, wikiFlags);
          944  +      @ %s(zEnd)</li>
          945  +    }else if( blob_size(&val)>50 || contains_newline(&val) ){
          946  +      @ <li>Change %h(z) to:%s(zBlock)
          947  +      wiki_convert(&val, 0, wikiFlags);
          948  +      @ %s(zEnd)</li>
   846    949       }else{
   847    950         @ <li>Change %h(z) to "%h(blob_str(&val))"</li>
   848    951       }
   849    952       blob_reset(&val);
   850    953     }
   851    954     @ </ol>
   852    955   }
................................................................................
   964   1067         if( !strncmp(g.argv[3],"fields",n) ){
   965   1068           /* simply show all field names */
   966   1069           int i;
   967   1070   
   968   1071           /* read all available ticket fields */
   969   1072           getAllTicketFields();
   970   1073           for(i=0; i<nField; i++){
   971         -          printf("%s\n",azField[i]);
         1074  +          printf("%s\n",aField[i].zName);
   972   1075           }
   973   1076         }else if( !strncmp(g.argv[3],"reports",n) ){
   974   1077           rpt_list_reports();
   975   1078         }else{
   976   1079           fossil_fatal("unknown ticket list option '%s'!",g.argv[3]);
   977   1080         }
   978   1081       }
................................................................................
  1039   1142         if( eCmd==history ){
  1040   1143           Stmt q;
  1041   1144           int tagid;
  1042   1145   
  1043   1146           if ( i != g.argc ){
  1044   1147             fossil_fatal("no other parameters expected to %s!",g.argv[2]);
  1045   1148           }
  1046         -        tagid = db_int(0, "SELECT tagid FROM tag WHERE tagname GLOB 'tkt-%q*'",zTktUuid);
         1149  +        tagid = db_int(0, "SELECT tagid FROM tag WHERE tagname GLOB 'tkt-%q*'",
         1150  +                       zTktUuid);
  1047   1151           if( tagid==0 ){
  1048   1152             fossil_fatal("no such ticket %h", zTktUuid);
  1049   1153           }  
  1050   1154           db_prepare(&q,
  1051   1155             "SELECT datetime(mtime,'localtime'), objid, uuid, NULL, NULL, NULL"
  1052   1156             "  FROM event, blob"
  1053   1157             " WHERE objid IN (SELECT rid FROM tagxref WHERE tagid=%d)"
  1054   1158             "   AND blob.rid=event.objid"
  1055   1159             " UNION "
  1056         -          "SELECT datetime(mtime,'localtime'), attachid, uuid, src, filename, user"
         1160  +          "SELECT datetime(mtime,'localtime'), attachid, uuid, src, "
         1161  +          "       filename, user"
  1057   1162             "  FROM attachment, blob"
  1058   1163             " WHERE target=(SELECT substr(tagname,5) FROM tag WHERE tagid=%d)"
  1059   1164             "   AND blob.rid=attachid"
  1060   1165             " ORDER BY 1 DESC",
  1061   1166             tagid, tagid
  1062   1167           );
  1063   1168           while( db_step(&q)==SQLITE_ROW ){
................................................................................
  1069   1174             const char *zFile = db_column_text(&q, 4);
  1070   1175             memcpy(zShort, zChngUuid, 10);
  1071   1176             zShort[10] = 0;
  1072   1177             if( zFile!=0 ){
  1073   1178               const char *zSrc = db_column_text(&q, 3);
  1074   1179               const char *zUser = db_column_text(&q, 5);
  1075   1180               if( zSrc==0 || zSrc[0]==0 ){
  1076         -              fossil_print("Delete attachment %h\n", zFile);
         1181  +              fossil_print("Delete attachment %s\n", zFile);
  1077   1182               }else{
  1078         -              fossil_print("Add attachment %h\n", zFile);
         1183  +              fossil_print("Add attachment %s\n", zFile);
  1079   1184               }
  1080         -            fossil_print(" by %h on %h\n", zUser, zDate);
         1185  +            fossil_print(" by %s on %s\n", zUser, zDate);
  1081   1186             }else{
  1082   1187               pTicket = manifest_get(rid, CFTYPE_TICKET);
  1083   1188               if( pTicket ){
  1084   1189                 int i;
  1085   1190   
  1086         -              fossil_print("Ticket Change by %h on %h:\n", pTicket->zUser, zDate);
         1191  +              fossil_print("Ticket Change by %s on %s:\n",
         1192  +                           pTicket->zUser, zDate);
  1087   1193                 for(i=0; i<pTicket->nField; i++){
  1088   1194                   Blob val;
  1089   1195                   const char *z;
  1090   1196                   z = pTicket->aField[i].zName;
  1091   1197                   blob_set(&val, pTicket->aField[i].zValue);
  1092   1198                   if( z[0]=='+' ){
  1093   1199                     fossil_print("  Append to ");
................................................................................
  1112   1218           return;
  1113   1219         }
  1114   1220         /* read all given ticket field/value pairs from command line */
  1115   1221         if( i==g.argc ){
  1116   1222           fossil_fatal("empty %s command aborted!",g.argv[2]);
  1117   1223         }
  1118   1224         getAllTicketFields();
  1119         -      /* read commandline and assign fields in the azValue array */
         1225  +      /* read commandline and assign fields in the aField[].zValue array */
  1120   1226         while( i<g.argc ){
  1121   1227           char *zFName;
  1122   1228           char *zFValue;
  1123   1229           int j;
  1124   1230           int append = 0;
  1125   1231   
  1126   1232           zFName = g.argv[i++];
................................................................................
  1137   1243             zFName++;
  1138   1244           }
  1139   1245           j = fieldId(zFName);
  1140   1246           if( j == -1 ){
  1141   1247             fossil_fatal("unknown field name '%s'!",zFName);
  1142   1248           }else{
  1143   1249             if (append) {
  1144         -            azAppend[j] = zFValue;
         1250  +            aField[j].zAppend = zFValue;
  1145   1251             } else {
  1146         -            azValue[j] = zFValue;
         1252  +            aField[j].zValue = zFValue;
  1147   1253             }
  1148   1254           }
  1149   1255         }
  1150   1256   
  1151   1257         /* now add the needed artifacts to the repository */
  1152   1258         blob_zero(&tktchng);
  1153   1259         /* add the time to the ticket manifest */
  1154   1260         blob_appendf(&tktchng, "D %s\n", zDate);
  1155   1261         /* append defined elements */
  1156   1262         for(i=0; i<nField; i++){
  1157   1263           char *zValue = 0;
  1158   1264           char *zPfx;
  1159   1265   
  1160         -        if (azAppend[i] && azAppend[i][0] ){
         1266  +        if (aField[i].zAppend && aField[i].zAppend[0] ){
  1161   1267             zPfx = " +";
  1162         -          zValue = azAppend[i];
  1163         -        } else if( azValue[i] && azValue[i][0] ){
         1268  +          zValue = aField[i].zAppend;
         1269  +        } else if( aField[i].zValue && aField[i].zValue[0] ){
  1164   1270             zPfx = " ";
  1165         -          zValue = azValue[i];
         1271  +          zValue = aField[i].zValue;
  1166   1272           } else {
  1167   1273             continue;
  1168   1274           }
  1169         -        if( strncmp(azField[i], "private_", 8)==0 ){
         1275  +        if( memcmp(aField[i].zName, "private_", 8)==0 ){
  1170   1276             zValue = db_conceal(zValue, strlen(zValue));
  1171         -          blob_appendf(&tktchng, "J%s%s %s\n", zPfx, azField[i], zValue);
         1277  +          blob_appendf(&tktchng, "J%s%s %s\n", zPfx, aField[i].zName, zValue);
  1172   1278           }else{
  1173   1279             blob_appendf(&tktchng, "J%s%s %#F\n", zPfx,
  1174         -                       azField[i], strlen(zValue), zValue);
         1280  +                       aField[i].zName, strlen(zValue), zValue);
  1175   1281           }
  1176   1282         }
  1177   1283         blob_appendf(&tktchng, "K %s\n", zTktUuid);
  1178   1284         blob_appendf(&tktchng, "U %F\n", zUser);
  1179   1285         md5sum_blob(&tktchng, &cksum);
  1180   1286         blob_appendf(&tktchng, "Z %b\n", &cksum);
  1181   1287         ticket_put(&tktchng, zTktUuid, 0);
  1182   1288         printf("ticket %s succeeded for %s\n",
  1183   1289                (eCmd==set?"set":"add"),zTktUuid);
  1184   1290       }
  1185   1291     }
  1186   1292   }

Changes to src/tktsetup.c.

    65     65   /* @-comment: ** */
    66     66   static const char zDefaultTicketTable[] =
    67     67   @ CREATE TABLE ticket(
    68     68   @   -- Do not change any column that begins with tkt_
    69     69   @   tkt_id INTEGER PRIMARY KEY,
    70     70   @   tkt_uuid TEXT UNIQUE,
    71     71   @   tkt_mtime DATE,
    72         -@   -- Add as many field as required below this line
           72  +@   -- Add as many fields as required below this line
    73     73   @   type TEXT,
    74     74   @   status TEXT,
    75     75   @   subsystem TEXT,
    76     76   @   priority TEXT,
    77     77   @   severity TEXT,
    78     78   @   foundin TEXT,
    79     79   @   private_contact TEXT,
    80     80   @   resolution TEXT,
    81     81   @   title TEXT,
    82     82   @   comment TEXT
    83     83   @ );
           84  +@ CREATE TABLE ticketchng(
           85  +@   -- Do not change any column that begins with tkt_
           86  +@   tkt_id INTEGER REFERENCES ticket,
           87  +@   tkt_mtime DATE,
           88  +@   -- Add as many fields as required below this line
           89  +@   login TEXT,
           90  +@   username TEXT,
           91  +@   mimetype TEXT,
           92  +@   icomment TEXT
           93  +@ );
           94  +@ CREATE INDEX ticketchng_idx1 ON ticketchng(tkt_id, tkt_mtime);
    84     95   ;
    85     96   
    86     97   /*
    87     98   ** Return the ticket table definition
    88     99   */
    89    100   const char *ticket_table_schema(void){
    90    101     return db_get("ticket-table", (char*)zDefaultTicketTable);
................................................................................
   118    129       z = db_get(zDbField, (char*)zDfltValue);
   119    130     }
   120    131     style_header("Edit %s", zTitle);
   121    132     if( P("clear")!=0 ){
   122    133       login_verify_csrf_secret();
   123    134       db_unset(zDbField, 0);
   124    135       if( xRebuild ) xRebuild();
   125         -    z = zDfltValue;
          136  +    cgi_redirect("tktsetup");
   126    137     }else if( isSubmit ){
   127    138       char *zErr = 0;
   128    139       login_verify_csrf_secret();
   129    140       if( xText && (zErr = xText(z))!=0 ){
   130    141         @ <p class="tktsetupError">ERROR: %h(zErr)</p>
   131    142       }else{
   132    143         db_set(zDbField, z, 0);
................................................................................
   275    286       0,
   276    287       30
   277    288     );
   278    289   }
   279    290   
   280    291   static const char zDefaultNew[] =
   281    292   @ <th1>
          293  +@   if {![info exists mutype]} {set mutype {[links only]}}
   282    294   @   if {[info exists submit]} {
   283    295   @      set status Open
          296  +@      if {$mutype eq "HTML"} {
          297  +@        set mimetype "text/html"
          298  +@      } elseif {$mutype eq "Wiki"} {
          299  +@        set mimetype "text/x-fossil-wiki"
          300  +@      } elseif {$mutype eq {[links only]}} {
          301  +@        set mimetype "text/x-fossil-plain"
          302  +@      } else {
          303  +@        set mimetype "text/plain"
          304  +@      }
   284    305   @      submit_ticket
   285    306   @   }
   286    307   @ </th1>
   287    308   @ <h1 style="text-align: center;">Enter A New Ticket</h1>
   288    309   @ <table cellpadding="5">
   289    310   @ <tr>
   290         -@ <td colspan="2">
          311  +@ <td colspan="3">
   291    312   @ Enter a one-line summary of the ticket:<br />
   292    313   @ <input type="text" name="title" size="60" value="$<title>" />
   293    314   @ </td>
   294    315   @ </tr>
   295    316   @ 
   296    317   @ <tr>
   297         -@ <td style="text-align: center;">Type:
   298         -@ <th1>combobox type $type_choices 1</th1>
   299         -@ </td>
   300         -@ <td>What type of ticket is this?</td>
          318  +@ <td align="right">Type:</td>
          319  +@ <td align="left"><th1>combobox type $type_choices 1</th1></td>
          320  +@ <td align="left">What type of ticket is this?</td>
   301    321   @ </tr>
   302    322   @ 
   303    323   @ <tr>
   304         -@ <td style="text-align: center;">Version: 
          324  +@ <td align="right">Version:</td>
          325  +@ <td align="left">
   305    326   @ <input type="text" name="foundin" size="20" value="$<foundin>" />
   306    327   @ </td>
   307         -@ <td>In what version or build number do you observe the problem?</td>
          328  +@ <td align="left">In what version or build number do you observe
          329  +@ the problem?</td>
   308    330   @ </tr>
   309    331   @ 
   310    332   @ <tr>
   311         -@ <td style="text-align: center;">Severity:
   312         -@ <th1>combobox severity $severity_choices 1</th1>
   313         -@ </td>
   314         -@ <td>How debilitating is the problem?  How badly does the problem
          333  +@ <td align="right">Severity:</td>
          334  +@ <td align="left"><th1>combobox severity $severity_choices 1</th1></td>
          335  +@ <td align="left">How debilitating is the problem?  How badly does the problem
   315    336   @ affect the operation of the product?</td>
   316    337   @ </tr>
   317    338   @ 
   318    339   @ <tr>
   319         -@ <td style="text-align: center;">EMail:
   320         -@ <input type="text" name="private_contact" value="$<private_contact>" size="30" />
          340  +@ <td align="right">EMail:</td>
          341  +@ <td align="left">
          342  +@ <input type="text" name="private_contact" value="$<private_contact>"
          343  +@  size="30" />
   321    344   @ </td>
   322         -@ <td><span style="text-decoration: underline;">Not publicly visible</span>.
          345  +@ <td align="left"><u>Not publicly visible</u>
   323    346   @ Used by developers to contact you with questions.</td>
   324    347   @ </tr>
   325    348   @ 
   326    349   @ <tr>
   327         -@ <td colspan="2">
          350  +@ <td colspan="3">
   328    351   @ Enter a detailed description of the problem.
   329    352   @ For code defects, be sure to provide details on exactly how
   330    353   @ the problem can be reproduced.  Provide as much detail as
   331         -@ possible.
          354  +@ possible.  Format:
          355  +@ <th1>combobox mutype {Wiki HTML {Plain Text} {[links only]}} 1</th1>
   332    356   @ <br />
   333    357   @ <th1>set nline [linecount $comment 50 10]</th1>
   334         -@ <textarea name="comment" cols="80" rows="$nline"
   335         -@  wrap="virtual" class="wikiedit">$<comment></textarea><br />
   336         -@ <input type="submit" name="preview" value="Preview" /></td>
          358  +@ <textarea name="icomment" cols="80" rows="$nline"
          359  +@  wrap="virtual" class="wikiedit">$<icomment></textarea><br />
   337    360   @ </tr>
   338         -@
          361  +@ 
   339    362   @ <th1>enable_output [info exists preview]</th1>
   340         -@ <tr><td colspan="2">
          363  +@ <tr><td colspan="3">
   341    364   @ Description Preview:<br /><hr />
   342         -@ <th1>wiki $comment</th1>
   343         -@ <hr />
   344         -@ </td></tr>
          365  +@ <th1>
          366  +@ if {$mutype eq "Wiki"} {
          367  +@   wiki $icomment
          368  +@ } elseif {$mutype eq "Plain Text"} {
          369  +@   set r [randhex]
          370  +@   wiki "<verbatim-$r>[string trimright $icomment]\n</verbatim-$r>"
          371  +@ } elseif {$mutype eq {[links only]}} {
          372  +@   set r [randhex]
          373  +@   wiki "<verbatim-$r links>[string trimright $icomment]\n</verbatim-$r>"
          374  +@ } else {
          375  +@   wiki "<nowiki>$icomment\n</nowiki>"
          376  +@ }
          377  +@ </th1>
          378  +@ <hr /></td></tr>
          379  +@ <th1>enable_output 1</th1>
          380  +@ 
          381  +@ <tr>
          382  +@ <td><td align="left">
          383  +@ <input type="submit" name="preview" value="Preview" />
          384  +@ </td>
          385  +@ <td align="left">See how the description will appear after formatting.</td>
          386  +@ </tr>
          387  +@ 
          388  +@ <th1>enable_output [info exists preview]</th1>
          389  +@ <tr>
          390  +@ <td><td align="left">
          391  +@ <input type="submit" name="submit" value="Submit" />
          392  +@ </td>
          393  +@ <td align="left">After filling in the information above, press this 
          394  +@ button to create the new ticket</td>
          395  +@ </tr>
   345    396   @ <th1>enable_output 1</th1>
   346    397   @ 
   347    398   @ <tr>
   348         -@ <td style="text-align: center;">
   349         -@ <input type="submit" name="submit" value="Submit" />
   350         -@ </td>
   351         -@ <td>After filling in the information above, press this button to create
   352         -@ the new ticket</td>
   353         -@ </tr>
   354         -@ <tr>
   355         -@ <td style="text-align: center;">
          399  +@ <td><td align="left">
   356    400   @ <input type="submit" name="cancel" value="Cancel" />
   357    401   @ </td>
   358    402   @ <td>Abandon and forget this ticket</td>
   359    403   @ </tr>
   360    404   @ </table>
   361    405   ;
   362    406   
................................................................................
   385    429       40
   386    430     );
   387    431   }
   388    432   
   389    433   static const char zDefaultView[] =
   390    434   @ <table cellpadding="5">
   391    435   @ <tr><td class="tktDspLabel">Ticket&nbsp;UUID:</td>
   392         -@ <td class="tktDspValue" colspan="3">$<tkt_uuid></td></tr>
          436  +@ <th1>
          437  +@ if {[hascap s]} {
          438  +@   html "<td class='tktDspValue' colspan='3'>$tkt_uuid "
          439  +@   html "($tkt_id)</td></tr>\n"
          440  +@ } else {
          441  +@   html "<td class='tktDspValue' colspan='3'>$tkt_uuid</td></tr>\n"
          442  +@ }
          443  +@ </th1>
   393    444   @ <tr><td class="tktDspLabel">Title:</td>
   394    445   @ <td class="tktDspValue" colspan="3">
   395         -@ <th1>wiki $title</th1>
          446  +@ $<title>
   396    447   @ </td></tr>
   397    448   @ <tr><td class="tktDspLabel">Status:</td><td class="tktDspValue">
   398    449   @ $<status>
   399    450   @ </td>
   400    451   @ <td class="tktDspLabel">Type:</td><td class="tktDspValue">
   401    452   @ $<type>
   402    453   @ </td></tr>
................................................................................
   421    472   @   </td>
   422    473   @ <th1>enable_output 1</th1>
   423    474   @ </tr>
   424    475   @ <tr><td class="tktDspLabel">Version&nbsp;Found&nbsp;In:</td>
   425    476   @ <td colspan="3" valign="top" class="tktDspValue">
   426    477   @ $<foundin>
   427    478   @ </td></tr>
   428         -@ <tr><td>Description &amp; Comments:</td></tr>
   429         -@ <tr><td colspan="4" class="tktDspValue">
   430         -@ <th1>wiki $comment</th1>
   431         -@ </td></tr>
          479  +@ 
          480  +@ <th1>
          481  +@ if {[info exists comment] && [string length $comment]>10} {
          482  +@   html {
          483  +@     <tr><td class="tktDspLabel">Description:</td></tr>
          484  +@     <tr><td colspan="5" class="tktDspValue">
          485  +@   }
          486  +@   if {[info exists plaintext]} {
          487  +@     set r [randhex]
          488  +@     wiki "<verbatim-$r links>\n$comment\n</verbatim-$r>"
          489  +@   } else {
          490  +@     wiki $comment
          491  +@   }
          492  +@ }
          493  +@ set seenRow 0
          494  +@ set alwaysPlaintext [info exists plaintext]
          495  +@ query {SELECT datetime(tkt_mtime) AS xdate, login AS xlogin,
          496  +@               mimetype as xmimetype, icomment AS xcomment,
          497  +@               username AS xusername
          498  +@          FROM ticketchng
          499  +@         WHERE tkt_id=$tkt_id} {
          500  +@   if {$seenRow} {
          501  +@     html "<hr>\n"
          502  +@   } else {
          503  +@     html "<tr><td class='tktDspLabel'>User Comments:</td></tr>\n"
          504  +@     html "<tr><td colspan='5' class='tktDspValue'>\n"
          505  +@     set seenRow 1
          506  +@   }
          507  +@   html "[htmlize $xlogin]"
          508  +@   if {$xlogin ne $xusername && [string length $xusername]>0} {
          509  +@     html " (claiming to be [htmlize $xusername])"
          510  +@   }
          511  +@   html " added on $xdate:\n"
          512  +@   if {$alwaysPlaintext || $xmimetype eq "text/plain"} {
          513  +@     set r [randhex]
          514  +@     if {$xmimetype ne "text/plain"} {html "([htmlize $xmimetype])\n"}
          515  +@     wiki "<verbatim-$r>[string trimright $xcomment]</verbatim-$r>\n"
          516  +@   } elseif {$xmimetype eq "text/x-fossil-wiki"} {
          517  +@     wiki "<p>\n[string trimright $xcomment]\n</p>\n"
          518  +@   } elseif {$xmimetype eq "text/html"} {
          519  +@     wiki "<p><nowiki>\n[string trimright $xcomment]\n</nowiki>\n"
          520  +@   } else {
          521  +@     set r [randhex]
          522  +@     wiki "<verbatim-$r links>[string trimright $xcomment]</verbatim-$r>\n"
          523  +@   }
          524  +@ }
          525  +@ if {$seenRow} {html "</td></tr>\n"}
          526  +@ </th1>
   432    527   @ </table>
   433    528   ;
   434    529   
   435    530   
   436    531   /*
   437    532   ** Return the code used to generate the view ticket page
   438    533   */
................................................................................
   456    551       0,
   457    552       40
   458    553     );
   459    554   }
   460    555   
   461    556   static const char zDefaultEdit[] =
   462    557   @ <th1>
          558  +@   if {![info exists mutype]} {set mutype {[links only]}}
          559  +@   if {![info exists icomment]} {set icomment {}}
   463    560   @   if {![info exists username]} {set username $login}
   464    561   @   if {[info exists submit]} {
   465         -@     if {[info exists cmappnd]} {
   466         -@       if {[string length $cmappnd]>0} {
   467         -@         set ctxt "\n\n<hr /><i>[htmlize $login]"
   468         -@         if {$username ne $login} {
   469         -@           set ctxt "$ctxt claiming to be [htmlize $username]"
   470         -@         }
   471         -@         set ctxt "$ctxt added on [date] UTC:</i><br />\n$cmappnd"
   472         -@         append_field comment $ctxt
   473         -@       }
          562  +@     if {$mutype eq "Wiki"} {
          563  +@       set mimetype text/x-fossil-wiki
          564  +@     } elseif {$mutype eq "HTML"} {
          565  +@       set mimetype text/html
          566  +@     } elseif {$mutype eq {[links only]}} {
          567  +@       set mimetype text/x-fossil-plain
          568  +@     } else {
          569  +@       set mimetype text/plain
   474    570   @     }
   475    571   @     submit_ticket
   476    572   @   }
   477    573   @ </th1>
   478    574   @ <table cellpadding="5">
   479    575   @ <tr><td class="tktDspLabel">Title:</td><td>
   480    576   @ <input type="text" name="title" value="$<title>" size="60" />
   481    577   @ </td></tr>
          578  +@ 
   482    579   @ <tr><td class="tktDspLabel">Status:</td><td>
   483    580   @ <th1>combobox status $status_choices 1</th1>
   484    581   @ </td></tr>
          582  +@ 
   485    583   @ <tr><td class="tktDspLabel">Type:</td><td>
   486    584   @ <th1>combobox type $type_choices 1</th1>
   487    585   @ </td></tr>
          586  +@ 
   488    587   @ <tr><td class="tktDspLabel">Severity:</td><td>
   489    588   @ <th1>combobox severity $severity_choices 1</th1>
   490    589   @ </td></tr>
          590  +@ 
   491    591   @ <tr><td class="tktDspLabel">Priority:</td><td>
   492    592   @ <th1>combobox priority $priority_choices 1</th1>
   493    593   @ </td></tr>
          594  +@ 
   494    595   @ <tr><td class="tktDspLabel">Resolution:</td><td>
   495    596   @ <th1>combobox resolution $resolution_choices 1</th1>
   496    597   @ </td></tr>
          598  +@ 
   497    599   @ <tr><td class="tktDspLabel">Subsystem:</td><td>
   498    600   @ <th1>combobox subsystem $subsystem_choices 1</th1>
   499    601   @ </td></tr>
          602  +@ 
   500    603   @ <th1>enable_output [hascap e]</th1>
   501    604   @   <tr><td class="tktDspLabel">Contact:</td><td>
   502    605   @   <input type="text" name="private_contact" size="40"
   503    606   @    value="$<private_contact>" />
   504    607   @   </td></tr>
   505    608   @ <th1>enable_output 1</th1>
          609  +@ 
   506    610   @ <tr><td class="tktDspLabel">Version&nbsp;Found&nbsp;In:</td><td>
   507    611   @ <input type="text" name="foundin" size="50" value="$<foundin>" />
   508    612   @ </td></tr>
          613  +@ 
   509    614   @ <tr><td colspan="2">
   510         -@ <th1>
   511         -@   if {![info exists eall]} {set eall 0}
   512         -@   if {[info exists aonlybtn]} {set eall 0}
   513         -@   if {[info exists eallbtn]} {set eall 1}
   514         -@   if {![hascap w]} {set eall 0}
   515         -@   if {![info exists cmappnd]} {set cmappnd {}}
   516         -@   set nline [linecount $comment 15 10]
   517         -@   enable_output $eall
   518         -@ </th1>
   519         -@   Description And Comments:<br />
   520         -@   <textarea name="comment" cols="80" rows="$nline"
   521         -@    wrap="virtual" class="wikiedit">$<comment></textarea><br />
   522         -@   <input type="hidden" name="eall" value="1" />
   523         -@   <input type="submit" name="aonlybtn" value="Append Remark" />
   524         -@   <input type="submit" name="preview1btn" value="Preview" />
   525         -@ <th1>enable_output [expr {!$eall}]</th1>
   526         -@   Append Remark from 
          615  +@   Append Remark with format
          616  +@   <th1>combobox mutype {Wiki HTML {Plain Text} {[links only]}} 1</th1>
          617  +@   from
   527    618   @   <input type="text" name="username" value="$<username>" size="30" />:<br />
   528         -@   <textarea name="cmappnd" cols="80" rows="15"
   529         -@    wrap="virtual" class="wikiedit">$<cmappnd></textarea><br />
   530         -@ <th1>enable_output [expr {[hascap w] && !$eall}]</th1>
   531         -@   <input type="submit" name="eallbtn" value="Edit All" />
   532         -@ <th1>enable_output [expr {!$eall}]</th1>
   533         -@   <input type="submit" name="preview2btn" value="Preview" />
   534         -@ <th1>enable_output 1</th1>
          619  +@   <textarea name="icomment" cols="80" rows="15"
          620  +@    wrap="virtual" class="wikiedit">$<icomment></textarea>
   535    621   @ </td></tr>
   536         -@
   537         -@ <th1>enable_output [info exists preview1btn]</th1>
          622  +@ 
          623  +@ <th1>enable_output [info exists preview]</th1>
   538    624   @ <tr><td colspan="2">
   539         -@ Description Preview:<br /><hr />
   540         -@ <th1>wiki $comment</th1>
   541         -@ <hr />
   542         -@ </td></tr>
   543         -@ <th1>enable_output [info exists preview2btn]</th1>
   544         -@ <tr><td colspan="2">
   545         -@ Description Preview:<br /><hr />
   546         -@ <th1>wiki $cmappnd</th1>
   547         -@ <hr />
          625  +@ Description Preview:<br><hr>
          626  +@ <th1>
          627  +@ if {$mutype eq "Wiki"} {
          628  +@   wiki $icomment
          629  +@ } elseif {$mutype eq "Plain Text"} {
          630  +@   set r [randhex]
          631  +@   wiki "<verbatim-$r>\n[string trimright $icomment]\n</verbatim-$r>"
          632  +@ } elseif {$mutype eq {[links only]}} {
          633  +@   set r [randhex]
          634  +@   wiki "<verbatim-$r links>\n[string trimright $icomment]</verbatim-$r>"
          635  +@ } else {
          636  +@   wiki "<nowiki>\n[string trimright $icomment]\n</nowiki>"
          637  +@ }
          638  +@ </th1>
          639  +@ <hr>
   548    640   @ </td></tr>
   549    641   @ <th1>enable_output 1</th1>
   550         -@
   551         -@ <tr><td align="right"></td><td>
   552         -@ <input type="submit" name="submit" value="Submit Changes" />
          642  +@ 
          643  +@ <tr>
          644  +@ <td align="right">
          645  +@ <input type="submit" name="preview" value="Preview" />
          646  +@ </td>
          647  +@ <td align="left">See how the description will appear after formatting.</td>
          648  +@ </tr>
          649  +@ 
          650  +@ <th1>enable_output [info exists preview]</th1>
          651  +@ <tr>
          652  +@ <td align="right">
          653  +@ <input type="submit" name="submit" value="Submit" />
          654  +@ </td>
          655  +@ <td align="left">Apply the changes shown above</td>
          656  +@ </tr>
          657  +@ <th1>enable_output 1</th1>
          658  +@ 
          659  +@ <tr>
          660  +@ <td align="right">
   553    661   @ <input type="submit" name="cancel" value="Cancel" />
   554         -@ </td></tr>
          662  +@ </td>
          663  +@ <td>Abandon this edit</td>
          664  +@ </tr>
          665  +@ 
   555    666   @ </table>
   556    667   ;
   557    668   
   558    669   /*
   559    670   ** Return the code used to generate the edit ticket page
   560    671   */
   561    672   const char *ticket_editpage_code(void){

Changes to src/wikiformat.c.

    49     49   #define ATTR_COLSPAN            10
    50     50   #define ATTR_COMPACT            11
    51     51   #define ATTR_FACE               12
    52     52   #define ATTR_HEIGHT             13
    53     53   #define ATTR_HREF               14
    54     54   #define ATTR_HSPACE             15
    55     55   #define ATTR_ID                 16
    56         -#define ATTR_NAME               17
    57         -#define ATTR_ROWSPAN            18
    58         -#define ATTR_SIZE               19
    59         -#define ATTR_SRC                20
    60         -#define ATTR_START              21
    61         -#define ATTR_STYLE              22
    62         -#define ATTR_TARGET             23
    63         -#define ATTR_TYPE               24
    64         -#define ATTR_VALIGN             25
    65         -#define ATTR_VALUE              26
    66         -#define ATTR_VSPACE             27
    67         -#define ATTR_WIDTH              28
           56  +#define ATTR_LINKS              17
           57  +#define ATTR_NAME               18
           58  +#define ATTR_ROWSPAN            19
           59  +#define ATTR_SIZE               20
           60  +#define ATTR_SRC                21
           61  +#define ATTR_START              22
           62  +#define ATTR_STYLE              23
           63  +#define ATTR_TARGET             24
           64  +#define ATTR_TYPE               25
           65  +#define ATTR_VALIGN             26
           66  +#define ATTR_VALUE              27
           67  +#define ATTR_VSPACE             28
           68  +#define ATTR_WIDTH              29
    68     69   #define AMSK_ALIGN              0x00000001
    69     70   #define AMSK_ALT                0x00000002
    70     71   #define AMSK_BGCOLOR            0x00000004
    71     72   #define AMSK_BORDER             0x00000008
    72     73   #define AMSK_CELLPADDING        0x00000010
    73     74   #define AMSK_CELLSPACING        0x00000020
    74     75   #define AMSK_CLASS              0x00000040
................................................................................
    77     78   #define AMSK_COLSPAN            0x00000200
    78     79   #define AMSK_COMPACT            0x00000400
    79     80   #define AMSK_FACE               0x00000800
    80     81   #define AMSK_HEIGHT             0x00001000
    81     82   #define AMSK_HREF               0x00002000
    82     83   #define AMSK_HSPACE             0x00004000
    83     84   #define AMSK_ID                 0x00008000
    84         -#define AMSK_NAME               0x00010000
    85         -#define AMSK_ROWSPAN            0x00020000
    86         -#define AMSK_SIZE               0x00040000
    87         -#define AMSK_SRC                0x00080000
    88         -#define AMSK_START              0x00100000
    89         -#define AMSK_STYLE              0x00200000
    90         -#define AMSK_TARGET             0x00400000
    91         -#define AMSK_TYPE               0x00800000
    92         -#define AMSK_VALIGN             0x01000000
    93         -#define AMSK_VALUE              0x02000000
    94         -#define AMSK_VSPACE             0x04000000
    95         -#define AMSK_WIDTH              0x08000000
           85  +#define AMSK_LINKS              0x00010000
           86  +#define AMSK_NAME               0x00020000
           87  +#define AMSK_ROWSPAN            0x00040000
           88  +#define AMSK_SIZE               0x00080000
           89  +#define AMSK_SRC                0x00100000
           90  +#define AMSK_START              0x00200000
           91  +#define AMSK_STYLE              0x00400000
           92  +#define AMSK_TARGET             0x00800000
           93  +#define AMSK_TYPE               0x01000000
           94  +#define AMSK_VALIGN             0x02000000
           95  +#define AMSK_VALUE              0x04000000
           96  +#define AMSK_VSPACE             0x08000000
           97  +#define AMSK_WIDTH              0x10000000
    96     98   
    97     99   static const struct AllowedAttribute {
    98    100     const char *zName;
    99    101     unsigned int iMask;
   100    102   } aAttribute[] = {
   101    103     { 0, 0 },
   102    104     { "align",         AMSK_ALIGN,          },
................................................................................
   111    113     { "colspan",       AMSK_COLSPAN,        },
   112    114     { "compact",       AMSK_COMPACT,        },
   113    115     { "face",          AMSK_FACE,           },
   114    116     { "height",        AMSK_HEIGHT,         },
   115    117     { "href",          AMSK_HREF,           },
   116    118     { "hspace",        AMSK_HSPACE,         },
   117    119     { "id",            AMSK_ID,             },
          120  +  { "links",         AMSK_LINKS,          },
   118    121     { "name",          AMSK_NAME,           },
   119    122     { "rowspan",       AMSK_ROWSPAN,        },
   120    123     { "size",          AMSK_SIZE,           },
   121    124     { "src",           AMSK_SRC,            },
   122    125     { "start",         AMSK_START,          },
   123    126     { "style",         AMSK_STYLE,          },
   124    127     { "target",        AMSK_TARGET,         },
................................................................................
   436    439   */
   437    440   static int markupLength(const char *z){
   438    441     int n = 1;
   439    442     int inparen = 0;
   440    443     int c;
   441    444     if( z[n]=='/' ){ n++; }
   442    445     if( !fossil_isalpha(z[n]) ) return 0;
   443         -  while( fossil_isalnum(z[n]) ){ n++; }
          446  +  while( fossil_isalnum(z[n]) || z[n]=='-' ){ n++; }
   444    447     c = z[n];
   445    448     if( c=='/' && z[n+1]=='>' ){ return n+2; }
   446    449     if( c!='>' && !fossil_isspace(c) ) return 0;
   447    450     while( (c = z[n])!=0 && (c!='>' || inparen) ){
   448    451       if( c==inparen ){
   449    452         inparen = 0;
   450    453       }else if( inparen==0 && (c=='"' || c=='\'') ){
................................................................................
   748    751       if( j<sizeof(zTag)-1 ) zTag[j++] = fossil_tolower(z[i]);
   749    752       i++;
   750    753     }
   751    754     zTag[j] = 0;
   752    755     p->iCode = findTag(zTag);
   753    756     p->iType = aMarkup[p->iCode].iType;
   754    757     p->nAttr = 0;
          758  +  c = 0;
          759  +  if( z[i]=='-' ){
          760  +    p->aAttr[0].iACode = iACode = ATTR_ID;
          761  +    i++;
          762  +    p->aAttr[0].zValue = &z[i];
          763  +    while( fossil_isalnum(z[i]) ){ i++; }
          764  +    p->aAttr[0].cTerm = c = z[i];
          765  +    z[i++] = 0;
          766  +    p->nAttr = 1;
          767  +    if( c=='>' ) return;
          768  +  }
   755    769     while( fossil_isspace(z[i]) ){ i++; }
   756         -  while( p->nAttr<8 && fossil_isalpha(z[i]) ){
          770  +  while( c!='>' && p->nAttr<8 && fossil_isalpha(z[i]) ){
   757    771       int attrOk;    /* True to preserver attribute.  False to ignore it */
   758    772       j = 0;
   759    773       while( fossil_isalnum(z[i]) ){
   760    774         if( j<sizeof(zTag)-1 ) zTag[j++] = fossil_tolower(z[i]);
   761    775         i++;
   762    776       }
   763    777       zTag[j] = 0;
................................................................................
  1159   1173      || strncmp(zTarget, "https:", 6)==0
  1160   1174      || strncmp(zTarget, "ftp:", 4)==0
  1161   1175      || strncmp(zTarget, "mailto:", 7)==0
  1162   1176     ){
  1163   1177       blob_appendf(p->pOut, "<a href=\"%s\">", zTarget);
  1164   1178     }else if( zTarget[0]=='/' ){
  1165   1179       blob_appendf(p->pOut, "<a href=\"%s%h\">", g.zTop, zTarget);
  1166         -  }else if( zTarget[0]=='.' || zTarget[0]=='#' ){
         1180  +  }else if( zTarget[0]=='.'
         1181  +         && (zTarget[1]=='/' || (zTarget[1]=='.' && zTarget[2]=='/'))
         1182  +         && (p->state & WIKI_LINKSONLY)==0 ){
         1183  +    blob_appendf(p->pOut, "<a href=\"%h\">", zTarget);
         1184  +  }else if( zTarget[0]=='#' ){
  1167   1185       blob_appendf(p->pOut, "<a href=\"%h\">", zTarget);
  1168   1186     }else if( is_valid_uuid(zTarget) ){
  1169   1187       int isClosed = 0;
  1170   1188       if( is_ticket(zTarget, &isClosed) ){
  1171   1189         /* Special display processing for tickets.  Display the hyperlink
  1172   1190         ** as crossed out if the ticket is closed.
  1173   1191         */
................................................................................
  1431   1449         }
  1432   1450         case TOKEN_MARKUP: {
  1433   1451           const char *zId;
  1434   1452           int iDiv;
  1435   1453           parseMarkup(&markup, z);
  1436   1454   
  1437   1455           /* Markup of the form </div id=ID> where there is a matching
  1438         -        ** ID somewhere on the stack.  Exit the verbatim if were are in
  1439         -        ** it.  Pop the stack up to the matching <div>.  Discard the
  1440         -        ** </div>
         1456  +        ** ID somewhere on the stack.  Exit any contained verbatim.
         1457  +        ** Pop the stack up to the matching <div>.  Discard the </div>
  1441   1458           */
  1442   1459           if( markup.iCode==MARKUP_DIV && markup.endTag &&
  1443   1460                (zId = markupId(&markup))!=0 &&
  1444   1461                (iDiv = findTagWithId(p, MARKUP_DIV, zId))>=0
  1445   1462           ){
  1446   1463             if( p->inVerbatim ){
  1447   1464               p->inVerbatim = 0;
................................................................................
  1525   1542             p->zVerbatimId = 0;
  1526   1543             p->inVerbatim = 1;
  1527   1544             p->preVerbState = p->state;
  1528   1545             p->state &= ~ALLOW_WIKI;
  1529   1546             for(ii=0; ii<markup.nAttr; ii++){
  1530   1547               if( markup.aAttr[ii].iACode == ATTR_ID ){
  1531   1548                 p->zVerbatimId = markup.aAttr[ii].zValue;
  1532         -            }else if( markup.aAttr[ii].iACode == ATTR_TYPE ){
  1533         -              if( fossil_stricmp(markup.aAttr[ii].zValue, "allow-links")==0 ){
  1534         -                p->state |= ALLOW_LINKS;
  1535         -              }else{
  1536         -                blob_appendf(p->pOut, "<pre name='code' class='%s'>",
  1537         -                  markup.aAttr[ii].zValue);
  1538         -                vAttrDidAppend=1;
  1539         -              }
         1549  +            }else if( markup.aAttr[ii].iACode==ATTR_TYPE ){
         1550  +              blob_appendf(p->pOut, "<pre name='code' class='%s'>",
         1551  +                markup.aAttr[ii].zValue);
         1552  +              vAttrDidAppend=1;
         1553  +            }else if( markup.aAttr[ii].iACode==ATTR_LINKS
         1554  +                   && !is_false(markup.aAttr[ii].zValue) ){
         1555  +              p->state |= ALLOW_LINKS;
  1540   1556               }
  1541   1557             }
  1542   1558             if( !vAttrDidAppend ) {
  1543   1559               endAutoParagraph(p);
  1544   1560               blob_append(p->pOut, "<pre class='verbatim'>",-1);
  1545   1561             }
  1546   1562             p->wantAutoParagraph = 0;