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);
+ @
+ @
if( zSrc==0 || zSrc[0]==0 ){
- @
- @ Delete attachment "%h(zFile)"
+ @ Delete attachment "%h(zFile)"
}else{
- @
- @
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 ){
@
- @ Ticket change
- @ [%z(href("%R/artifact/%!S",zChngUuid))%S(zChngUuid)]
+ @
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);
}
+ @
}
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*/
@ - \
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))
- @
- }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.