Index: src/backlink.c ================================================================== --- src/backlink.c +++ src/backlink.c @@ -132,11 +132,11 @@ " ELSE null END FROM backlink" ); style_table_sorter(); @ - @ + @ @ while( db_step(&q)==SQLITE_ROW ){ const char *zTarget = db_column_text(&q, 0); int srctype = db_column_int(&q, 1); int srcid = db_column_int(&q, 2); Index: src/blob.c ================================================================== --- src/blob.c +++ src/blob.c @@ -53,10 +53,15 @@ /* ** The buffer holding the blob data */ #define blob_buffer(X) ((X)->aData) +/* +** Number of elements that fits into the current blob's size +*/ +#define blob_count(X,elType) (blob_size(X)/sizeof(elType)) + /* ** Append blob contents to another */ #define blob_appendb(dest, src) \ blob_append((dest), blob_buffer(src), blob_size(src)) @@ -927,10 +932,29 @@ } void blobarray_reset(Blob *aBlob, int n){ int i; for(i=0; i ins.edit { background-color: #c0c0ff; text-decoration: none; font-weight: bold; } - +body.tkt div.content li > table.udiff { + margin-left: 1.5em; + margin-top: 0.5em; +} +body.tkt div.content ol.tkt-changes > li:target > p > span { + border-bottom: 3px solid gold; +} +body.tkt div.content ol.tkt-changes > li:target > ol { + border-left: 1px solid gold; +} span.modpending { color: #b03800; font-style: italic; } Index: src/info.c ================================================================== --- src/info.c +++ src/info.c @@ -2783,11 +2783,11 @@ "SELECT title FROM ticket WHERE tkt_uuid=%Q", zTktName) : 0; style_set_current_feature("tinfo"); style_header("Ticket Change Details"); style_submenu_element("Raw", "%R/artifact/%s", zUuid); - style_submenu_element("History", "%R/tkthistory/%s", zTktName); + style_submenu_element("History", "%R/tkthistory/%s#%S", zTktName,zUuid); style_submenu_element("Page", "%R/tktview/%t", zTktName); style_submenu_element("Timeline", "%R/tkttimeline/%t", zTktName); if( P("plaintext") ){ style_submenu_element("Formatted", "%R/info/%s", zUuid); }else{ @@ -2828,11 +2828,11 @@ @ } @
Changes
@

- ticket_output_change_artifact(pTktChng, 0, 1); + ticket_output_change_artifact(pTktChng, 0, 1, 0); manifest_destroy(pTktChng); style_finish_page(); } Index: src/tkt.c ================================================================== --- src/tkt.c +++ src/tkt.c @@ -30,22 +30,27 @@ static int nField = 0; static struct tktFieldInfo { char *zName; /* Name of the database field */ char *zValue; /* Value to store */ char *zAppend; /* Value to append */ + char *zBsln; /* "baseline for $zName" if that field exists*/ unsigned mUsed; /* 01: TICKET 02: TICKETCHNG */ } *aField; #define USEDBY_TICKET 01 #define USEDBY_TICKETCHNG 02 #define USEDBY_BOTH 03 +#define JCARD_ASSIGN ('=') +#define JCARD_APPEND ('+') +#define JCARD_PRIVATE ('p') static u8 haveTicket = 0; /* True if the TICKET table exists */ static u8 haveTicketCTime = 0; /* True if TICKET.TKT_CTIME exists */ static u8 haveTicketChng = 0; /* True if the TICKETCHNG table exists */ static u8 haveTicketChngRid = 0; /* True if TICKETCHNG.TKT_RID exists */ static u8 haveTicketChngUser = 0;/* True if TICKETCHNG.TKT_USER exists */ static u8 useTicketGenMt = 0; /* use generated TICKET.MIMETYPE */ static u8 useTicketChngGenMt = 0;/* use generated TICKETCHNG.MIMETYPE */ +static int nTicketBslns = 0; /* number of valid "baseline for ..." */ /* ** Compare two entries in aField[] for sorting purposes */ @@ -73,31 +78,56 @@ ** The haveTicket and haveTicketChng variables are set to 1 if the TICKET and ** TICKETCHANGE tables exist, respectively. */ static void getAllTicketFields(void){ Stmt q; - int i, noRegularMimetype; + int i, noRegularMimetype, nBaselines; static int once = 0; if( once ) return; once = 1; + nBaselines = 0; db_prepare(&q, "PRAGMA table_info(ticket)"); while( db_step(&q)==SQLITE_ROW ){ const char *zFieldName = db_column_text(&q, 1); haveTicket = 1; if( memcmp(zFieldName,"tkt_",4)==0 ){ if( strcmp(zFieldName, "tkt_ctime")==0 ) haveTicketCTime = 1; continue; + } + if( memcmp(zFieldName,"baseline for ",13)==0 ){ + if( strcmp(db_column_text(&q,2),"INTEGER")==0 ){ + nBaselines++; + } + continue; } if( strchr(zFieldName,' ')!=0 ) continue; if( nField%10==0 ){ aField = fossil_realloc(aField, sizeof(aField[0])*(nField+10) ); } + aField[nField].zBsln = 0; aField[nField].zName = mprintf("%s", zFieldName); aField[nField].mUsed = USEDBY_TICKET; nField++; } db_finalize(&q); + if( nBaselines ){ + db_prepare(&q, "SELECT 1 FROM pragma_table_info('ticket') " + "WHERE type = 'INTEGER' AND name = :n"); + for(i=0; inField; i++){ const char * const zName = p->aField[i].zName; const char * const zBaseName = zName[0]=='+' ? zName+1 : zName; j = fieldId(zBaseName); if( j<0 ) continue; @@ -244,12 +292,16 @@ aUsed[j] = 1; if( aField[j].mUsed & USEDBY_TICKET ){ if( zName[0]=='+' ){ blob_append_sql(&sql1,", \"%w\"=coalesce(\"%w\",'') || %Q", zBaseName, zBaseName, p->aField[i].zValue); + /* when appending keep "baseline for ..." unchanged */ }else{ blob_append_sql(&sql1,", \"%w\"=%Q", zBaseName, p->aField[i].zValue); + if( aField[j].zBsln ){ + blob_append_sql(&sql1,", \"%w\"=%d", aField[j].zBsln, rid); + } } } if( aField[j].mUsed & USEDBY_TICKETCHNG ){ blob_append_sql(&sql2, ",\"%w\"", zBaseName); blob_append_sql(&sql3, ",%Q", p->aField[i].zValue); @@ -327,24 +379,37 @@ } blob_reset(&sql2); blob_reset(&sql3); fossil_free(aUsed); if( rid>0 ){ /* extract backlinks */ - int bReplace = 1, mimetype; for(i=0; inField; i++){ const char *zName = p->aField[i].zName; const char *zBaseName = zName[0]=='+' ? zName+1 : zName; j = fieldId(zBaseName); if( j<0 ) continue; if( aField[j].mUsed & USEDBY_TICKETCHNG ){ - mimetype = mimetype_tktchng; + backlink_extract(p->aField[i].zValue, mimetype_tktchng, + rid, BKLNK_TICKET, p->rDate, + /* existing backlinks must have been + * already deleted by the caller */ 0 ); }else{ - mimetype = mimetype_tkt; + /* update field's data with the most recent values */ + Blob *cards = fields + j; + struct jCardInfo card = { + fossil_strdup(p->aField[i].zValue), + mimetype_tkt, rid, p->rDate + }; + if( blob_size(cards) && zName[0]!='+' ){ + struct jCardInfo *x = (struct jCardInfo *)blob_buffer(cards); + struct jCardInfo *end = x + blob_count(cards,struct jCardInfo); + for(; x!=end; x++){ + fossil_free( x->zValue ); + } + blob_truncate(cards,0); + } + blob_append(cards, (const char*)(&card), sizeof(card)); } - backlink_extract(p->aField[i].zValue, mimetype, rid, BKLNK_TICKET, - p->rDate, bReplace); - bReplace = 0; } } return tktid; } @@ -377,12 +442,13 @@ void ticket_rebuild_entry(const char *zTktUuid){ char *zTag = mprintf("tkt-%s", zTktUuid); int tagid = tag_findid(zTag, 1); Stmt q; Manifest *pTicket; - int tktid; + int tktid, i; int createFlag = 1; + Blob *fields; /* array of blobs; each blob holds array of jCardInfo */ fossil_free(zTag); getAllTicketFields(); if( haveTicket==0 ) return; tktid = db_int(0, "SELECT tkt_id FROM ticket WHERE tkt_uuid=%Q", zTktUuid); @@ -390,22 +456,41 @@ if( haveTicketChng ){ db_multi_exec("DELETE FROM ticketchng WHERE tkt_id=%d;", tktid); } db_multi_exec("DELETE FROM ticket WHERE tkt_id=%d", tktid); tktid = 0; + fields = blobarray_new( nField ); + db_multi_exec("DELETE FROM backlink WHERE srctype=%d AND srcid IN " + "(SELECT rid FROM tagxref WHERE tagid=%d)",BKLNK_TICKET, tagid); db_prepare(&q, "SELECT rid FROM tagxref WHERE tagid=%d ORDER BY mtime",tagid); while( db_step(&q)==SQLITE_ROW ){ int rid = db_column_int(&q, 0); pTicket = manifest_get(rid, CFTYPE_TICKET, 0); if( pTicket ){ - tktid = ticket_insert(pTicket, rid, tktid); + tktid = ticket_insert(pTicket, rid, tktid, fields); manifest_ticket_event(rid, pTicket, createFlag, tagid); manifest_destroy(pTicket); } createFlag = 0; } db_finalize(&q); + /* Extract backlinks from the most recent values of TICKET fields */ + for(i=0; izValue ); + backlink_extract(x->zValue,x->mimetype, + x->rid,BKLNK_TICKET,x->mtime,0); + fossil_free( x->zValue ); + } + } + blob_truncate(cards,0); + } + blobarray_delete(fields,nField); } /* ** Create the TH1 interpreter and load the "common" code. @@ -738,23 +823,41 @@ return TH_OK; } /* ** Write a ticket into the repository. +** Upon reassignment of fields try to delta-compress an artifact against +** all artifacts that are referenced in the corresponding zBsln fields. */ static int ticket_put( Blob *pTicket, /* The text of the ticket change record */ const char *zTktId, /* The ticket to which this change is applied */ + const char *aUsed, /* Indicators for fields' modifications */ int needMod /* True if moderation is needed */ ){ int result; int rid; manifest_crosslink_begin(); rid = content_put_ex(pTicket, 0, 0, 0, needMod); if( rid==0 ){ fossil_fatal("trouble committing ticket: %s", g.zErrMsg); } + if( nTicketBslns ){ + int i, s, buf[8], nSrc=0, *aSrc=&(buf[0]); + if( nTicketBslns > count(buf) ){ + aSrc = (int*)fossil_malloc(sizeof(int)*nTicketBslns); + } + for(i=0; i 0 ) aSrc[nSrc++] = s; + } + } + if( nSrc ) content_deltify(rid, aSrc, nSrc, 0); + if( aSrc!=&(buf[0]) ) fossil_free( aSrc ); + } if( needMod ){ moderation_table_create(); db_multi_exec( "INSERT INTO modreq(objid, tktid) VALUES(%d,%Q)", rid, zTktId @@ -787,14 +890,14 @@ void *pUuid, int argc, const char **argv, int *argl ){ - char *zDate; + char *zDate, *aUsed; const char *zUuid; int i; - int nJ = 0; + int nJ = 0, rc = TH_OK; Blob tktchng, cksum; int needMod; login_verify_csrf_secret(); if( !captcha_is_correct(0) ){ @@ -804,15 +907,17 @@ zUuid = (const char *)pUuid; blob_zero(&tktchng); zDate = date_in_standard_format("now"); blob_appendf(&tktchng, "D %s\n", zDate); free(zDate); + aUsed = fossil_malloc_zero( nField ); for(i=0; iTicket artifact that would have been submitted:

@
%h(blob_str(&tktchng))
@
Moderation would be %h(zNeedMod).
@ @
- return TH_OK; }else{ if( g.thTrace ){ Th_Trace("submit_ticket {\n
\n%h\n
\n" "}
\n", blob_str(&tktchng)); } - ticket_put(&tktchng, zUuid, needMod); + ticket_put(&tktchng, zUuid, aUsed, needMod); + rc = ticket_change(zUuid); } - return ticket_change(zUuid); + finish: + fossil_free( aUsed ); + return rc; } /* ** WEBPAGE: tktnew @@ -1141,17 +1250,21 @@ ** By default, the artifacts are decoded and formatted. Text fields ** are formatted as text/plain, since in the general case Fossil does ** not have knowledge of the encoding. If the "raw" query parameter ** is present, then the undecoded and unformatted text of each artifact ** is displayed. +** +** Reassignments of a field of the TICKET table that has a corresponding +** "baseline for ..." companion are rendered as unified diffs. */ void tkthistory_page(void){ Stmt q; char *zTitle; const char *zUuid; int tagid; int nChng = 0; + Blob *aLastVal = 0; /* holds the last rendered value for each field */ login_check_credentials(); if( !g.perm.Hyperlink || !g.perm.RdTkt ){ login_needed(g.anon.Hyperlink && g.anon.RdTkt); return; @@ -1177,10 +1290,14 @@ } if( P("raw")!=0 ){ @

Raw Artifacts Associated With Ticket %h(zUuid)

}else{ @

Artifacts Associated With Ticket %h(zUuid)

+ getAllTicketFields(); + if( nTicketBslns ){ + aLastVal = blobarray_new(nField); + } } db_prepare(&q, "SELECT datetime(mtime,toLocal()), objid, uuid, NULL, NULL, NULL" " FROM event, blob" " WHERE objid IN (SELECT rid FROM tagxref WHERE tagid=%d)" @@ -1198,33 +1315,33 @@ const char *zDate = db_column_text(&q, 0); int rid = db_column_int(&q, 1); const char *zChngUuid = db_column_text(&q, 2); const char *zFile = db_column_text(&q, 4); if( nChng==0 ){ - @
    + @
      } if( zFile!=0 ){ const char *zSrc = db_column_text(&q, 3); const char *zUser = db_column_text(&q, 5); + @ + @
    1. if( zSrc==0 || zSrc[0]==0 ){ - @ - @

    2. Delete attachment "%h(zFile)" + @ Delete attachment "%h(zFile)" }else{ - @ - @

    3. Add attachment + @ Add attachment @ "%z(href("%R/artifact/%!S",zSrc))%s(zFile)" } - @ [%z(href("%R/artifact/%!S",zChngUuid))%S(zChngUuid)] + @ [%z(href("%R/artifact/%!S",zChngUuid))%S(zChngUuid)] @ (rid %d(rid)) by hyperlink_to_user(zUser,zDate," on"); hyperlink_to_date(zDate, ".

      "); }else{ pTicket = manifest_get(rid, CFTYPE_TICKET, 0); if( pTicket ){ @ - @
    4. Ticket change - @ [%z(href("%R/artifact/%!S",zChngUuid))%S(zChngUuid)] + @

    5. Ticket change + @ [%z(href("%R/artifact/%!S",zChngUuid))%S(zChngUuid)] @ (rid %d(rid)) by hyperlink_to_user(pTicket->zUser,zDate," on"); hyperlink_to_date(zDate, ":"); @

      if( P("raw")!=0 ){ @@ -1233,21 +1350,23 @@ @
                 @ %h(blob_str(&c))
                 @ 
      blob_reset(&c); }else{ - ticket_output_change_artifact(pTicket, "a", nChng); + ticket_output_change_artifact(pTicket, "a", nChng, aLastVal); } } manifest_destroy(pTicket); } + @
    6. } db_finalize(&q); if( nChng ){ @
    } style_finish_page(); + if( aLastVal ) blobarray_delete(aLastVal, nField); } /* ** Return TRUE if the given BLOB contains a newline character. */ @@ -1261,48 +1380,95 @@ } /* ** The pTkt object is a ticket change artifact. Output a detailed ** description of this object. +** +** If `aLastVal` is not NULL then render selected fields as unified diffs +** and update corresponding elements of that array with values from `pTkt`. */ void ticket_output_change_artifact( Manifest *pTkt, /* Parsed artifact for the ticket change */ const char *zListType, /* Which type of list */ - int n /* Which ticket change is this */ + int n, /* Which ticket change is this */ + Blob *aLastVal /* Array of the latest values for the diffs */ ){ int i; if( zListType==0 ) zListType = "1"; getAllTicketFields(); @
      for(i=0; inField; i++){ - Blob val; - const char *z, *zX; - int id; - z = pTkt->aField[i].zName; - blob_set(&val, pTkt->aField[i].zValue); - zX = z[0]=='+' ? z+1 : z; - id = fieldId(zX); + const char *z = pTkt->aField[i].zName; + const char *zX = z[0]=='+' ? z+1 : z; + const int id = fieldId(zX); + const char *zValue = pTkt->aField[i].zValue; + const size_t nValue = strlen(zValue); + const int bLong = nValue>50 || memchr(zValue,'\n',nValue)!=NULL; + /* zValue is long enough to justify a
      */ + const int bCanDiff = aLastVal && id>=0 && aField[id].zBsln; + /* preliminary flag for rendering via unified diff */ + int bAppend = 0; /* zValue is being appended to a TICKET's field */ + int bRegular = 0; /* prev value of a TICKET's field is being superseded*/ @
    1. \ if( id<0 ){ @ Untracked field %h(zX): }else if( aField[id].mUsed==USEDBY_TICKETCHNG ){ @ %h(zX): }else if( n==0 ){ @ %h(zX) initialized to: }else if( z[0]=='+' && (aField[id].mUsed&USEDBY_TICKET)!=0 ){ @ Appended to %h(zX): - }else{ - @ %h(zX) changed to: - } - if( blob_size(&val)>50 || contains_newline(&val) ){ - @
      -      @ %h(blob_str(&val))
      -      @ 
    2. - }else{ - @ "%h(blob_str(&val))" - } - blob_reset(&val); + bAppend = 1; + }else{ + if( !bCanDiff ){ + @ %h(zX) changed to: \ + } + bRegular = 1; + } + if( bCanDiff ){ + Blob *prev = aLastVal+id; + Blob val = BLOB_INITIALIZER; + if( nValue ){ + blob_init(&val, zValue, nValue+1); + val.nUsed--; /* makes blob_str() faster */ + } + if( bRegular && nValue && blob_buffer(prev) && blob_size(prev) ){ + Blob d = BLOB_INITIALIZER; + DiffConfig DCfg; + construct_diff_flags(1, &DCfg); + DCfg.diffFlags |= DIFF_HTML | DIFF_LINENO; + text_diff(prev, &val, &d, &DCfg); + @ %h(zX) changed as: + @ %s(blob_str(&d)) + @ + blob_reset(&d); + }else{ + if( bRegular ){ + @ %h(zX) changed to: + } + if( bLong ){ + @
      +          @ %h(zValue)
      +          @ 
      + }else{ + @ "%h(zValue)" + } + } + if( blob_buffer(prev) && blob_size(prev) && !bAppend ){ + blob_truncate(prev,0); + } + if( nValue ) blob_appendb(prev, &val); + blob_reset(&val); + }else{ + if( bLong ){ + @
      +        @ %h(zValue)
      +        @ 
      + }else{ + @ "%h(zValue)" + } + } } @
    } /* @@ -1460,10 +1626,11 @@ }else{ /* add a new ticket or update an existing ticket */ enum { set,add,history,err } eCmd = err; int i = 0; Blob tktchng, cksum; + char *aUsed; /* get command type (set/add) and get uuid, if needed for set */ if( strncmp(g.argv[2],"set",n)==0 || strncmp(g.argv[2],"change",n)==0 || strncmp(g.argv[2],"history",n)==0 ){ if( strncmp(g.argv[2],"history",n)==0 ){ @@ -1602,10 +1769,11 @@ }else{ aField[j].zValue = zFValue; } } } + aUsed = fossil_malloc_zero( nField ); /* now add the needed artifacts to the repository */ blob_zero(&tktchng); /* add the time to the ticket manifest */ blob_appendf(&tktchng, "D %s\n", zDate); @@ -1615,34 +1783,39 @@ char *zPfx; if( aField[i].zAppend && aField[i].zAppend[0] ){ zPfx = " +"; zValue = aField[i].zAppend; + aUsed[i] = JCARD_APPEND; }else if( aField[i].zValue && aField[i].zValue[0] ){ zPfx = " "; zValue = aField[i].zValue; + aUsed[i] = JCARD_ASSIGN; }else{ continue; } if( memcmp(aField[i].zName, "private_", 8)==0 ){ zValue = db_conceal(zValue, strlen(zValue)); blob_appendf(&tktchng, "J%s%s %s\n", zPfx, aField[i].zName, zValue); + aUsed[i] = JCARD_PRIVATE; }else{ blob_appendf(&tktchng, "J%s%s %#F\n", zPfx, aField[i].zName, strlen(zValue), zValue); } } blob_appendf(&tktchng, "K %s\n", zTktUuid); blob_appendf(&tktchng, "U %F\n", zUser); md5sum_blob(&tktchng, &cksum); blob_appendf(&tktchng, "Z %b\n", &cksum); - if( ticket_put(&tktchng, zTktUuid, ticket_need_moderation(1))==0 ){ + if( ticket_put(&tktchng, zTktUuid, aUsed, + ticket_need_moderation(1) )==0 ){ fossil_fatal("%s", g.zErrMsg); }else{ fossil_print("ticket %s succeeded for %s\n", (eCmd==set?"set":"add"),zTktUuid); } + fossil_free( aUsed ); } } } Index: src/wiki.c ================================================================== --- src/wiki.c +++ src/wiki.c @@ -1867,13 +1867,11 @@ style_header("Changes To %s", pW1->zWikiTitle); blob_zero(&d); construct_diff_flags(1, &DCfg); DCfg.diffFlags |= DIFF_HTML | DIFF_LINENO; text_diff(&w2, &w1, &d, &DCfg); - @
       @ %s(blob_str(&d))
    -  @ 
       manifest_destroy(pW1);
       manifest_destroy(pW2);
       style_finish_page();
     }
     
    
    Index: www/changes.wiki
    ==================================================================
    --- www/changes.wiki
    +++ www/changes.wiki
    @@ -14,10 +14,13 @@
       *  Rebuilt [/file/Dockerfile | the stock Dockerfile] to create a "from scratch"
          Busybox based container image via an Alpine Linux intermediary
       *  Added [/doc/trunk/www/containers.md | a new document] describing how to
          customize, use, and run that container.
       *  Added "by hour of day" report to [/reports?view=byhour|the /reports page].
    +  *  Improved correctness, usability, and efficiency for the case
    +     [/timeline?r=deltify-tkt-blobs|when values in a TICKET's column
    +     tend to be long and volatile].
     
     

    Changes for version 2.19 (2022-07-21)

    * On file listing pages, sort filenames using the "uintnocase" collating sequence, so that filenames that contains embedded integers sort in numeric order even if they contain a different number of digits.
Source Target mtime
Target Source mtime