Fossil SCM

Improve correctness, usability and efficiency for the case when values in a TICKET's column tend to be long and volatile. Owner of a repository may specify one or several TICKET's columns so that delta-compression is tried for the corresponding ticket change artifacts and the corresponding changes on the <tt>/tkthistory</tt> page are rendered via unified diffs. See details in the [/wiki?name=branch/deltify-tkt-blobs&p|associated wiki].

george 2022-10-28 17:08 trunk merge
Commit 872a3b232737c0e46891297ebf24cbc3924001457807ad7d4df640b6755850c7
+1 -1
--- src/backlink.c
+++ src/backlink.c
@@ -132,11 +132,11 @@
132132
" ELSE null END FROM backlink"
133133
);
134134
style_table_sorter();
135135
@ <table border="1" cellpadding="2" cellspacing="0" \
136136
@ class='sortable' data-column-types='ttt' data-init-sort='0'>
137
- @ <thead><tr><th> Source <th> Target <th> mtime </tr></thead>
137
+ @ <thead><tr><th> Target <th> Source <th> mtime </tr></thead>
138138
@ <tbody>
139139
while( db_step(&q)==SQLITE_ROW ){
140140
const char *zTarget = db_column_text(&q, 0);
141141
int srctype = db_column_int(&q, 1);
142142
int srcid = db_column_int(&q, 2);
143143
--- src/backlink.c
+++ src/backlink.c
@@ -132,11 +132,11 @@
132 " ELSE null END FROM backlink"
133 );
134 style_table_sorter();
135 @ <table border="1" cellpadding="2" cellspacing="0" \
136 @ class='sortable' data-column-types='ttt' data-init-sort='0'>
137 @ <thead><tr><th> Source <th> Target <th> mtime </tr></thead>
138 @ <tbody>
139 while( db_step(&q)==SQLITE_ROW ){
140 const char *zTarget = db_column_text(&q, 0);
141 int srctype = db_column_int(&q, 1);
142 int srcid = db_column_int(&q, 2);
143
--- src/backlink.c
+++ src/backlink.c
@@ -132,11 +132,11 @@
132 " ELSE null END FROM backlink"
133 );
134 style_table_sorter();
135 @ <table border="1" cellpadding="2" cellspacing="0" \
136 @ class='sortable' data-column-types='ttt' data-init-sort='0'>
137 @ <thead><tr><th> Target <th> Source <th> mtime </tr></thead>
138 @ <tbody>
139 while( db_step(&q)==SQLITE_ROW ){
140 const char *zTarget = db_column_text(&q, 0);
141 int srctype = db_column_int(&q, 1);
142 int srcid = db_column_int(&q, 2);
143
+24
--- src/blob.c
+++ src/blob.c
@@ -53,10 +53,15 @@
5353
/*
5454
** The buffer holding the blob data
5555
*/
5656
#define blob_buffer(X) ((X)->aData)
5757
58
+/*
59
+** Number of elements that fits into the current blob's size
60
+*/
61
+#define blob_count(X,elType) (blob_size(X)/sizeof(elType))
62
+
5863
/*
5964
** Append blob contents to another
6065
*/
6166
#define blob_appendb(dest, src) \
6267
blob_append((dest), blob_buffer(src), blob_size(src))
@@ -927,10 +932,29 @@
927932
}
928933
void blobarray_reset(Blob *aBlob, int n){
929934
int i;
930935
for(i=0; i<n; i++) blob_reset(&aBlob[i]);
931936
}
937
+/*
938
+** Allocate array of n blobs and initialize each element with `empty_blob`
939
+*/
940
+Blob* blobarray_new(int n){
941
+ int i;
942
+ Blob *aBlob = fossil_malloc(sizeof(Blob)*n);
943
+ for(i=0; i<n; i++) aBlob[i] = empty_blob;
944
+ return aBlob;
945
+}
946
+/*
947
+** Free array of n blobs some of which may be empty (have NULL buffer)
948
+*/
949
+void blobarray_delete(Blob *aBlob, int n){
950
+ int i;
951
+ for(i=0; i<n; i++){
952
+ if( blob_buffer(aBlob+i) ) blob_reset(aBlob+i);
953
+ }
954
+ fossil_free(aBlob);
955
+}
932956
933957
/*
934958
** Parse a blob into space-separated tokens. Store each token in
935959
** an element of the blobarray aToken[]. aToken[] is nToken elements in
936960
** size. Return the number of tokens seen.
937961
--- src/blob.c
+++ src/blob.c
@@ -53,10 +53,15 @@
53 /*
54 ** The buffer holding the blob data
55 */
56 #define blob_buffer(X) ((X)->aData)
57
 
 
 
 
 
58 /*
59 ** Append blob contents to another
60 */
61 #define blob_appendb(dest, src) \
62 blob_append((dest), blob_buffer(src), blob_size(src))
@@ -927,10 +932,29 @@
927 }
928 void blobarray_reset(Blob *aBlob, int n){
929 int i;
930 for(i=0; i<n; i++) blob_reset(&aBlob[i]);
931 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
932
933 /*
934 ** Parse a blob into space-separated tokens. Store each token in
935 ** an element of the blobarray aToken[]. aToken[] is nToken elements in
936 ** size. Return the number of tokens seen.
937
--- src/blob.c
+++ src/blob.c
@@ -53,10 +53,15 @@
53 /*
54 ** The buffer holding the blob data
55 */
56 #define blob_buffer(X) ((X)->aData)
57
58 /*
59 ** Number of elements that fits into the current blob's size
60 */
61 #define blob_count(X,elType) (blob_size(X)/sizeof(elType))
62
63 /*
64 ** Append blob contents to another
65 */
66 #define blob_appendb(dest, src) \
67 blob_append((dest), blob_buffer(src), blob_size(src))
@@ -927,10 +932,29 @@
932 }
933 void blobarray_reset(Blob *aBlob, int n){
934 int i;
935 for(i=0; i<n; i++) blob_reset(&aBlob[i]);
936 }
937 /*
938 ** Allocate array of n blobs and initialize each element with `empty_blob`
939 */
940 Blob* blobarray_new(int n){
941 int i;
942 Blob *aBlob = fossil_malloc(sizeof(Blob)*n);
943 for(i=0; i<n; i++) aBlob[i] = empty_blob;
944 return aBlob;
945 }
946 /*
947 ** Free array of n blobs some of which may be empty (have NULL buffer)
948 */
949 void blobarray_delete(Blob *aBlob, int n){
950 int i;
951 for(i=0; i<n; i++){
952 if( blob_buffer(aBlob+i) ) blob_reset(aBlob+i);
953 }
954 fossil_free(aBlob);
955 }
956
957 /*
958 ** Parse a blob into space-separated tokens. Store each token in
959 ** an element of the blobarray aToken[]. aToken[] is nToken elements in
960 ** size. Return the number of tokens seen.
961
+10 -1
--- src/default.css
+++ src/default.css
@@ -707,11 +707,20 @@
707707
td.difftxt ins > ins.edit {
708708
background-color: #c0c0ff;
709709
text-decoration: none;
710710
font-weight: bold;
711711
}
712
-
712
+body.tkt div.content li > table.udiff {
713
+ margin-left: 1.5em;
714
+ margin-top: 0.5em;
715
+}
716
+body.tkt div.content ol.tkt-changes > li:target > p > span {
717
+ border-bottom: 3px solid gold;
718
+}
719
+body.tkt div.content ol.tkt-changes > li:target > ol {
720
+ border-left: 1px solid gold;
721
+}
713722
714723
span.modpending {
715724
color: #b03800;
716725
font-style: italic;
717726
}
718727
--- src/default.css
+++ src/default.css
@@ -707,11 +707,20 @@
707 td.difftxt ins > ins.edit {
708 background-color: #c0c0ff;
709 text-decoration: none;
710 font-weight: bold;
711 }
712
 
 
 
 
 
 
 
 
 
713
714 span.modpending {
715 color: #b03800;
716 font-style: italic;
717 }
718
--- src/default.css
+++ src/default.css
@@ -707,11 +707,20 @@
707 td.difftxt ins > ins.edit {
708 background-color: #c0c0ff;
709 text-decoration: none;
710 font-weight: bold;
711 }
712 body.tkt div.content li > table.udiff {
713 margin-left: 1.5em;
714 margin-top: 0.5em;
715 }
716 body.tkt div.content ol.tkt-changes > li:target > p > span {
717 border-bottom: 3px solid gold;
718 }
719 body.tkt div.content ol.tkt-changes > li:target > ol {
720 border-left: 1px solid gold;
721 }
722
723 span.modpending {
724 color: #b03800;
725 font-style: italic;
726 }
727
+2 -2
--- src/info.c
+++ src/info.c
@@ -2783,11 +2783,11 @@
27832783
"SELECT title FROM ticket WHERE tkt_uuid=%Q", zTktName)
27842784
: 0;
27852785
style_set_current_feature("tinfo");
27862786
style_header("Ticket Change Details");
27872787
style_submenu_element("Raw", "%R/artifact/%s", zUuid);
2788
- style_submenu_element("History", "%R/tkthistory/%s", zTktName);
2788
+ style_submenu_element("History", "%R/tkthistory/%s#%S", zTktName,zUuid);
27892789
style_submenu_element("Page", "%R/tktview/%t", zTktName);
27902790
style_submenu_element("Timeline", "%R/tkttimeline/%t", zTktName);
27912791
if( P("plaintext") ){
27922792
style_submenu_element("Formatted", "%R/info/%s", zUuid);
27932793
}else{
@@ -2828,11 +2828,11 @@
28282828
@ </blockquote>
28292829
}
28302830
28312831
@ <div class="section">Changes</div>
28322832
@ <p>
2833
- ticket_output_change_artifact(pTktChng, 0, 1);
2833
+ ticket_output_change_artifact(pTktChng, 0, 1, 0);
28342834
manifest_destroy(pTktChng);
28352835
style_finish_page();
28362836
}
28372837
28382838
28392839
--- src/info.c
+++ src/info.c
@@ -2783,11 +2783,11 @@
2783 "SELECT title FROM ticket WHERE tkt_uuid=%Q", zTktName)
2784 : 0;
2785 style_set_current_feature("tinfo");
2786 style_header("Ticket Change Details");
2787 style_submenu_element("Raw", "%R/artifact/%s", zUuid);
2788 style_submenu_element("History", "%R/tkthistory/%s", zTktName);
2789 style_submenu_element("Page", "%R/tktview/%t", zTktName);
2790 style_submenu_element("Timeline", "%R/tkttimeline/%t", zTktName);
2791 if( P("plaintext") ){
2792 style_submenu_element("Formatted", "%R/info/%s", zUuid);
2793 }else{
@@ -2828,11 +2828,11 @@
2828 @ </blockquote>
2829 }
2830
2831 @ <div class="section">Changes</div>
2832 @ <p>
2833 ticket_output_change_artifact(pTktChng, 0, 1);
2834 manifest_destroy(pTktChng);
2835 style_finish_page();
2836 }
2837
2838
2839
--- src/info.c
+++ src/info.c
@@ -2783,11 +2783,11 @@
2783 "SELECT title FROM ticket WHERE tkt_uuid=%Q", zTktName)
2784 : 0;
2785 style_set_current_feature("tinfo");
2786 style_header("Ticket Change Details");
2787 style_submenu_element("Raw", "%R/artifact/%s", zUuid);
2788 style_submenu_element("History", "%R/tkthistory/%s#%S", zTktName,zUuid);
2789 style_submenu_element("Page", "%R/tktview/%t", zTktName);
2790 style_submenu_element("Timeline", "%R/tkttimeline/%t", zTktName);
2791 if( P("plaintext") ){
2792 style_submenu_element("Formatted", "%R/info/%s", zUuid);
2793 }else{
@@ -2828,11 +2828,11 @@
2828 @ </blockquote>
2829 }
2830
2831 @ <div class="section">Changes</div>
2832 @ <p>
2833 ticket_output_change_artifact(pTktChng, 0, 1, 0);
2834 manifest_destroy(pTktChng);
2835 style_finish_page();
2836 }
2837
2838
2839
+220 -47
--- src/tkt.c
+++ src/tkt.c
@@ -30,22 +30,27 @@
3030
static int nField = 0;
3131
static struct tktFieldInfo {
3232
char *zName; /* Name of the database field */
3333
char *zValue; /* Value to store */
3434
char *zAppend; /* Value to append */
35
+ char *zBsln; /* "baseline for $zName" if that field exists*/
3536
unsigned mUsed; /* 01: TICKET 02: TICKETCHNG */
3637
} *aField;
3738
#define USEDBY_TICKET 01
3839
#define USEDBY_TICKETCHNG 02
3940
#define USEDBY_BOTH 03
41
+#define JCARD_ASSIGN ('=')
42
+#define JCARD_APPEND ('+')
43
+#define JCARD_PRIVATE ('p')
4044
static u8 haveTicket = 0; /* True if the TICKET table exists */
4145
static u8 haveTicketCTime = 0; /* True if TICKET.TKT_CTIME exists */
4246
static u8 haveTicketChng = 0; /* True if the TICKETCHNG table exists */
4347
static u8 haveTicketChngRid = 0; /* True if TICKETCHNG.TKT_RID exists */
4448
static u8 haveTicketChngUser = 0;/* True if TICKETCHNG.TKT_USER exists */
4549
static u8 useTicketGenMt = 0; /* use generated TICKET.MIMETYPE */
4650
static u8 useTicketChngGenMt = 0;/* use generated TICKETCHNG.MIMETYPE */
51
+static int nTicketBslns = 0; /* number of valid "baseline for ..." */
4752
4853
4954
/*
5055
** Compare two entries in aField[] for sorting purposes
5156
*/
@@ -73,31 +78,56 @@
7378
** The haveTicket and haveTicketChng variables are set to 1 if the TICKET and
7479
** TICKETCHANGE tables exist, respectively.
7580
*/
7681
static void getAllTicketFields(void){
7782
Stmt q;
78
- int i, noRegularMimetype;
83
+ int i, noRegularMimetype, nBaselines;
7984
static int once = 0;
8085
if( once ) return;
8186
once = 1;
87
+ nBaselines = 0;
8288
db_prepare(&q, "PRAGMA table_info(ticket)");
8389
while( db_step(&q)==SQLITE_ROW ){
8490
const char *zFieldName = db_column_text(&q, 1);
8591
haveTicket = 1;
8692
if( memcmp(zFieldName,"tkt_",4)==0 ){
8793
if( strcmp(zFieldName, "tkt_ctime")==0 ) haveTicketCTime = 1;
8894
continue;
95
+ }
96
+ if( memcmp(zFieldName,"baseline for ",13)==0 ){
97
+ if( strcmp(db_column_text(&q,2),"INTEGER")==0 ){
98
+ nBaselines++;
99
+ }
100
+ continue;
89101
}
90102
if( strchr(zFieldName,' ')!=0 ) continue;
91103
if( nField%10==0 ){
92104
aField = fossil_realloc(aField, sizeof(aField[0])*(nField+10) );
93105
}
106
+ aField[nField].zBsln = 0;
94107
aField[nField].zName = mprintf("%s", zFieldName);
95108
aField[nField].mUsed = USEDBY_TICKET;
96109
nField++;
97110
}
98111
db_finalize(&q);
112
+ if( nBaselines ){
113
+ db_prepare(&q, "SELECT 1 FROM pragma_table_info('ticket') "
114
+ "WHERE type = 'INTEGER' AND name = :n");
115
+ for(i=0; i<nField && nBaselines!=0; i++){
116
+ char *zBsln = mprintf("baseline for %s",aField[i].zName);
117
+ db_bind_text(&q, ":n", zBsln);
118
+ if( db_step(&q)==SQLITE_ROW ){
119
+ aField[i].zBsln = zBsln;
120
+ nTicketBslns++;
121
+ nBaselines--;
122
+ }else{
123
+ free(zBsln);
124
+ }
125
+ db_reset(&q);
126
+ }
127
+ db_finalize(&q);
128
+ }
99129
db_prepare(&q, "PRAGMA table_info(ticketchng)");
100130
while( db_step(&q)==SQLITE_ROW ){
101131
const char *zFieldName = db_column_text(&q, 1);
102132
haveTicketChng = 1;
103133
if( memcmp(zFieldName,"tkt_",4)==0 ){
@@ -114,10 +144,11 @@
114144
continue;
115145
}
116146
if( nField%10==0 ){
117147
aField = fossil_realloc(aField, sizeof(aField[0])*(nField+10) );
118148
}
149
+ aField[nField].zBsln = 0;
119150
aField[nField].zName = mprintf("%s", zFieldName);
120151
aField[nField].mUsed = USEDBY_TICKETCHNG;
121152
nField++;
122153
}
123154
db_finalize(&q);
@@ -201,21 +232,39 @@
201232
for(i=0; (z = cgi_parameter_name(i))!=0; i++){
202233
Th_Store(z, P(z));
203234
}
204235
}
205236
237
+/*
238
+** Information about a single J-card
239
+*/
240
+struct jCardInfo {
241
+ char *zValue;
242
+ int mimetype;
243
+ int rid;
244
+ double mtime;
245
+};
246
+
206247
/*
207248
** Update an entry of the TICKET and TICKETCHNG tables according to the
208249
** information in the ticket artifact given in p. Attempt to create
209250
** the appropriate TICKET table entry if tktid is zero. If tktid is nonzero
210251
** then it will be the ROWID of an existing TICKET entry.
211252
**
212253
** Parameter rid is the recordID for the ticket artifact in the BLOB table.
254
+** Upon assignment of a field this rid is stored into a corresponding
255
+** zBsln integer column (provided that it is defined within TICKET table).
256
+**
257
+** If a field is USEDBY_TICKETCHNG table then back-references within it
258
+** are extracted and inserted into the BACKLINK table; otherwise
259
+** a corresponding blob in the `fields` array is updated so that the
260
+** caller could extract backlinks from the most recent field's values.
213261
**
214262
** Return the new rowid of the TICKET table entry.
215263
*/
216
-static int ticket_insert(const Manifest *p, const int rid, int tktid){
264
+static int ticket_insert(const Manifest *p, const int rid, int tktid,
265
+ Blob *fields){
217266
Blob sql1; /* update or replace TICKET ... */
218267
Blob sql2; /* list of TICKETCHNG's fields that are in the manifest */
219268
Blob sql3; /* list of values which correspond to the previous list */
220269
Stmt q;
221270
int i, j;
@@ -232,12 +281,11 @@
232281
blob_zero(&sql3);
233282
blob_append_sql(&sql1, "UPDATE OR REPLACE ticket SET tkt_mtime=:mtime");
234283
if( haveTicketCTime ){
235284
blob_append_sql(&sql1, ", tkt_ctime=coalesce(tkt_ctime,:mtime)");
236285
}
237
- aUsed = fossil_malloc( nField );
238
- memset(aUsed, 0, nField);
286
+ aUsed = fossil_malloc_zero( nField );
239287
for(i=0; i<p->nField; i++){
240288
const char * const zName = p->aField[i].zName;
241289
const char * const zBaseName = zName[0]=='+' ? zName+1 : zName;
242290
j = fieldId(zBaseName);
243291
if( j<0 ) continue;
@@ -244,12 +292,16 @@
244292
aUsed[j] = 1;
245293
if( aField[j].mUsed & USEDBY_TICKET ){
246294
if( zName[0]=='+' ){
247295
blob_append_sql(&sql1,", \"%w\"=coalesce(\"%w\",'') || %Q",
248296
zBaseName, zBaseName, p->aField[i].zValue);
297
+ /* when appending keep "baseline for ..." unchanged */
249298
}else{
250299
blob_append_sql(&sql1,", \"%w\"=%Q", zBaseName, p->aField[i].zValue);
300
+ if( aField[j].zBsln ){
301
+ blob_append_sql(&sql1,", \"%w\"=%d", aField[j].zBsln, rid);
302
+ }
251303
}
252304
}
253305
if( aField[j].mUsed & USEDBY_TICKETCHNG ){
254306
blob_append_sql(&sql2, ",\"%w\"", zBaseName);
255307
blob_append_sql(&sql3, ",%Q", p->aField[i].zValue);
@@ -327,24 +379,37 @@
327379
}
328380
blob_reset(&sql2);
329381
blob_reset(&sql3);
330382
fossil_free(aUsed);
331383
if( rid>0 ){ /* extract backlinks */
332
- int bReplace = 1, mimetype;
333384
for(i=0; i<p->nField; i++){
334385
const char *zName = p->aField[i].zName;
335386
const char *zBaseName = zName[0]=='+' ? zName+1 : zName;
336387
j = fieldId(zBaseName);
337388
if( j<0 ) continue;
338389
if( aField[j].mUsed & USEDBY_TICKETCHNG ){
339
- mimetype = mimetype_tktchng;
390
+ backlink_extract(p->aField[i].zValue, mimetype_tktchng,
391
+ rid, BKLNK_TICKET, p->rDate,
392
+ /* existing backlinks must have been
393
+ * already deleted by the caller */ 0 );
340394
}else{
341
- mimetype = mimetype_tkt;
395
+ /* update field's data with the most recent values */
396
+ Blob *cards = fields + j;
397
+ struct jCardInfo card = {
398
+ fossil_strdup(p->aField[i].zValue),
399
+ mimetype_tkt, rid, p->rDate
400
+ };
401
+ if( blob_size(cards) && zName[0]!='+' ){
402
+ struct jCardInfo *x = (struct jCardInfo *)blob_buffer(cards);
403
+ struct jCardInfo *end = x + blob_count(cards,struct jCardInfo);
404
+ for(; x!=end; x++){
405
+ fossil_free( x->zValue );
406
+ }
407
+ blob_truncate(cards,0);
408
+ }
409
+ blob_append(cards, (const char*)(&card), sizeof(card));
342410
}
343
- backlink_extract(p->aField[i].zValue, mimetype, rid, BKLNK_TICKET,
344
- p->rDate, bReplace);
345
- bReplace = 0;
346411
}
347412
}
348413
return tktid;
349414
}
350415
@@ -377,12 +442,13 @@
377442
void ticket_rebuild_entry(const char *zTktUuid){
378443
char *zTag = mprintf("tkt-%s", zTktUuid);
379444
int tagid = tag_findid(zTag, 1);
380445
Stmt q;
381446
Manifest *pTicket;
382
- int tktid;
447
+ int tktid, i;
383448
int createFlag = 1;
449
+ Blob *fields; /* array of blobs; each blob holds array of jCardInfo */
384450
385451
fossil_free(zTag);
386452
getAllTicketFields();
387453
if( haveTicket==0 ) return;
388454
tktid = db_int(0, "SELECT tkt_id FROM ticket WHERE tkt_uuid=%Q", zTktUuid);
@@ -390,22 +456,41 @@
390456
if( haveTicketChng ){
391457
db_multi_exec("DELETE FROM ticketchng WHERE tkt_id=%d;", tktid);
392458
}
393459
db_multi_exec("DELETE FROM ticket WHERE tkt_id=%d", tktid);
394460
tktid = 0;
461
+ fields = blobarray_new( nField );
462
+ db_multi_exec("DELETE FROM backlink WHERE srctype=%d AND srcid IN "
463
+ "(SELECT rid FROM tagxref WHERE tagid=%d)",BKLNK_TICKET, tagid);
395464
db_prepare(&q, "SELECT rid FROM tagxref WHERE tagid=%d ORDER BY mtime",tagid);
396465
while( db_step(&q)==SQLITE_ROW ){
397466
int rid = db_column_int(&q, 0);
398467
pTicket = manifest_get(rid, CFTYPE_TICKET, 0);
399468
if( pTicket ){
400
- tktid = ticket_insert(pTicket, rid, tktid);
469
+ tktid = ticket_insert(pTicket, rid, tktid, fields);
401470
manifest_ticket_event(rid, pTicket, createFlag, tagid);
402471
manifest_destroy(pTicket);
403472
}
404473
createFlag = 0;
405474
}
406475
db_finalize(&q);
476
+ /* Extract backlinks from the most recent values of TICKET fields */
477
+ for(i=0; i<nField; i++){
478
+ Blob *cards = fields + i;
479
+ if( blob_size(cards) ){
480
+ struct jCardInfo *x = (struct jCardInfo *)blob_buffer(cards);
481
+ struct jCardInfo *end = x + blob_count(cards,struct jCardInfo);
482
+ for(; x!=end; x++){
483
+ assert( x->zValue );
484
+ backlink_extract(x->zValue,x->mimetype,
485
+ x->rid,BKLNK_TICKET,x->mtime,0);
486
+ fossil_free( x->zValue );
487
+ }
488
+ }
489
+ blob_truncate(cards,0);
490
+ }
491
+ blobarray_delete(fields,nField);
407492
}
408493
409494
410495
/*
411496
** Create the TH1 interpreter and load the "common" code.
@@ -738,23 +823,41 @@
738823
return TH_OK;
739824
}
740825
741826
/*
742827
** Write a ticket into the repository.
828
+** Upon reassignment of fields try to delta-compress an artifact against
829
+** all artifacts that are referenced in the corresponding zBsln fields.
743830
*/
744831
static int ticket_put(
745832
Blob *pTicket, /* The text of the ticket change record */
746833
const char *zTktId, /* The ticket to which this change is applied */
834
+ const char *aUsed, /* Indicators for fields' modifications */
747835
int needMod /* True if moderation is needed */
748836
){
749837
int result;
750838
int rid;
751839
manifest_crosslink_begin();
752840
rid = content_put_ex(pTicket, 0, 0, 0, needMod);
753841
if( rid==0 ){
754842
fossil_fatal("trouble committing ticket: %s", g.zErrMsg);
755843
}
844
+ if( nTicketBslns ){
845
+ int i, s, buf[8], nSrc=0, *aSrc=&(buf[0]);
846
+ if( nTicketBslns > count(buf) ){
847
+ aSrc = (int*)fossil_malloc(sizeof(int)*nTicketBslns);
848
+ }
849
+ for(i=0; i<nField; i++){
850
+ if( aField[i].zBsln && aUsed[i]==JCARD_ASSIGN ){
851
+ s = db_int(0,"SELECT \"%w\" FROM ticket WHERE tkt_uuid = '%q'",
852
+ aField[i].zBsln, zTktId );
853
+ if( s > 0 ) aSrc[nSrc++] = s;
854
+ }
855
+ }
856
+ if( nSrc ) content_deltify(rid, aSrc, nSrc, 0);
857
+ if( aSrc!=&(buf[0]) ) fossil_free( aSrc );
858
+ }
756859
if( needMod ){
757860
moderation_table_create();
758861
db_multi_exec(
759862
"INSERT INTO modreq(objid, tktid) VALUES(%d,%Q)",
760863
rid, zTktId
@@ -787,14 +890,14 @@
787890
void *pUuid,
788891
int argc,
789892
const char **argv,
790893
int *argl
791894
){
792
- char *zDate;
895
+ char *zDate, *aUsed;
793896
const char *zUuid;
794897
int i;
795
- int nJ = 0;
898
+ int nJ = 0, rc = TH_OK;
796899
Blob tktchng, cksum;
797900
int needMod;
798901
799902
login_verify_csrf_secret();
800903
if( !captcha_is_correct(0) ){
@@ -804,15 +907,17 @@
804907
zUuid = (const char *)pUuid;
805908
blob_zero(&tktchng);
806909
zDate = date_in_standard_format("now");
807910
blob_appendf(&tktchng, "D %s\n", zDate);
808911
free(zDate);
912
+ aUsed = fossil_malloc_zero( nField );
809913
for(i=0; i<nField; i++){
810914
if( aField[i].zAppend ){
811915
blob_appendf(&tktchng, "J +%s %z\n", aField[i].zName,
812916
fossilize(aField[i].zAppend, -1));
813917
++nJ;
918
+ aUsed[i] = JCARD_APPEND;
814919
}
815920
}
816921
for(i=0; i<nField; i++){
817922
const char *zValue;
818923
int nValue;
@@ -825,12 +930,14 @@
825930
|| strlen(aField[i].zValue)!=nValue
826931
){
827932
if( memcmp(aField[i].zName, "private_", 8)==0 ){
828933
zValue = db_conceal(zValue, nValue);
829934
blob_appendf(&tktchng, "J %s %s\n", aField[i].zName, zValue);
935
+ aUsed[i] = JCARD_PRIVATE;
830936
}else{
831937
blob_appendf(&tktchng, "J %s %#F\n", aField[i].zName, nValue, zValue);
938
+ aUsed[i] = JCARD_ASSIGN;
832939
}
833940
nJ++;
834941
}
835942
}
836943
}
@@ -846,11 +953,11 @@
846953
blob_appendf(&tktchng, "U %F\n", login_name());
847954
md5sum_blob(&tktchng, &cksum);
848955
blob_appendf(&tktchng, "Z %b\n", &cksum);
849956
if( nJ==0 ){
850957
blob_reset(&tktchng);
851
- return TH_OK;
958
+ goto finish;
852959
}
853960
needMod = ticket_need_moderation(0);
854961
if( g.zPath[0]=='d' ){
855962
const char *zNeedMod = needMod ? "required" : "skipped";
856963
/* If called from /debug_tktnew or /debug_tktedit... */
@@ -858,20 +965,22 @@
858965
@ <p>Ticket artifact that would have been submitted:</p>
859966
@ <blockquote><pre>%h(blob_str(&tktchng))</pre></blockquote>
860967
@ <blockquote><pre>Moderation would be %h(zNeedMod).</pre></blockquote>
861968
@ </div>
862969
@ <hr />
863
- return TH_OK;
864970
}else{
865971
if( g.thTrace ){
866972
Th_Trace("submit_ticket {\n<blockquote><pre>\n%h\n</pre></blockquote>\n"
867973
"}<br />\n",
868974
blob_str(&tktchng));
869975
}
870
- ticket_put(&tktchng, zUuid, needMod);
976
+ ticket_put(&tktchng, zUuid, aUsed, needMod);
977
+ rc = ticket_change(zUuid);
871978
}
872
- return ticket_change(zUuid);
979
+ finish:
980
+ fossil_free( aUsed );
981
+ return rc;
873982
}
874983
875984
876985
/*
877986
** WEBPAGE: tktnew
@@ -1141,17 +1250,21 @@
11411250
** By default, the artifacts are decoded and formatted. Text fields
11421251
** are formatted as text/plain, since in the general case Fossil does
11431252
** not have knowledge of the encoding. If the "raw" query parameter
11441253
** is present, then the undecoded and unformatted text of each artifact
11451254
** is displayed.
1255
+**
1256
+** Reassignments of a field of the TICKET table that has a corresponding
1257
+** "baseline for ..." companion are rendered as unified diffs.
11461258
*/
11471259
void tkthistory_page(void){
11481260
Stmt q;
11491261
char *zTitle;
11501262
const char *zUuid;
11511263
int tagid;
11521264
int nChng = 0;
1265
+ Blob *aLastVal = 0; /* holds the last rendered value for each field */
11531266
11541267
login_check_credentials();
11551268
if( !g.perm.Hyperlink || !g.perm.RdTkt ){
11561269
login_needed(g.anon.Hyperlink && g.anon.RdTkt);
11571270
return;
@@ -1177,10 +1290,14 @@
11771290
}
11781291
if( P("raw")!=0 ){
11791292
@ <h2>Raw Artifacts Associated With Ticket %h(zUuid)</h2>
11801293
}else{
11811294
@ <h2>Artifacts Associated With Ticket %h(zUuid)</h2>
1295
+ getAllTicketFields();
1296
+ if( nTicketBslns ){
1297
+ aLastVal = blobarray_new(nField);
1298
+ }
11821299
}
11831300
db_prepare(&q,
11841301
"SELECT datetime(mtime,toLocal()), objid, uuid, NULL, NULL, NULL"
11851302
" FROM event, blob"
11861303
" WHERE objid IN (SELECT rid FROM tagxref WHERE tagid=%d)"
@@ -1198,33 +1315,33 @@
11981315
const char *zDate = db_column_text(&q, 0);
11991316
int rid = db_column_int(&q, 1);
12001317
const char *zChngUuid = db_column_text(&q, 2);
12011318
const char *zFile = db_column_text(&q, 4);
12021319
if( nChng==0 ){
1203
- @ <ol>
1320
+ @ <ol class="tkt-changes">
12041321
}
12051322
if( zFile!=0 ){
12061323
const char *zSrc = db_column_text(&q, 3);
12071324
const char *zUser = db_column_text(&q, 5);
1325
+ @
1326
+ @ <li id="%S(zChngUuid)"><p><span>
12081327
if( zSrc==0 || zSrc[0]==0 ){
1209
- @
1210
- @ <li><p>Delete attachment "%h(zFile)"
1328
+ @ Delete attachment "%h(zFile)"
12111329
}else{
1212
- @
1213
- @ <li><p>Add attachment
1330
+ @ Add attachment
12141331
@ "%z(href("%R/artifact/%!S",zSrc))%s(zFile)</a>"
12151332
}
1216
- @ [%z(href("%R/artifact/%!S",zChngUuid))%S(zChngUuid)</a>]
1333
+ @ [%z(href("%R/artifact/%!S",zChngUuid))%S(zChngUuid)</a>]</span>
12171334
@ (rid %d(rid)) by
12181335
hyperlink_to_user(zUser,zDate," on");
12191336
hyperlink_to_date(zDate, ".</p>");
12201337
}else{
12211338
pTicket = manifest_get(rid, CFTYPE_TICKET, 0);
12221339
if( pTicket ){
12231340
@
1224
- @ <li><p>Ticket change
1225
- @ [%z(href("%R/artifact/%!S",zChngUuid))%S(zChngUuid)</a>]
1341
+ @ <li id="%S(zChngUuid)"><p><span>Ticket change
1342
+ @ [%z(href("%R/artifact/%!S",zChngUuid))%S(zChngUuid)</a>]</span>
12261343
@ (rid %d(rid)) by
12271344
hyperlink_to_user(pTicket->zUser,zDate," on");
12281345
hyperlink_to_date(zDate, ":");
12291346
@ </p>
12301347
if( P("raw")!=0 ){
@@ -1233,21 +1350,23 @@
12331350
@ <blockquote><pre>
12341351
@ %h(blob_str(&c))
12351352
@ </pre></blockquote>
12361353
blob_reset(&c);
12371354
}else{
1238
- ticket_output_change_artifact(pTicket, "a", nChng);
1355
+ ticket_output_change_artifact(pTicket, "a", nChng, aLastVal);
12391356
}
12401357
}
12411358
manifest_destroy(pTicket);
12421359
}
1360
+ @ </li>
12431361
}
12441362
db_finalize(&q);
12451363
if( nChng ){
12461364
@ </ol>
12471365
}
12481366
style_finish_page();
1367
+ if( aLastVal ) blobarray_delete(aLastVal, nField);
12491368
}
12501369
12511370
/*
12521371
** Return TRUE if the given BLOB contains a newline character.
12531372
*/
@@ -1261,48 +1380,95 @@
12611380
}
12621381
12631382
/*
12641383
** The pTkt object is a ticket change artifact. Output a detailed
12651384
** description of this object.
1385
+**
1386
+** If `aLastVal` is not NULL then render selected fields as unified diffs
1387
+** and update corresponding elements of that array with values from `pTkt`.
12661388
*/
12671389
void ticket_output_change_artifact(
12681390
Manifest *pTkt, /* Parsed artifact for the ticket change */
12691391
const char *zListType, /* Which type of list */
1270
- int n /* Which ticket change is this */
1392
+ int n, /* Which ticket change is this */
1393
+ Blob *aLastVal /* Array of the latest values for the diffs */
12711394
){
12721395
int i;
12731396
if( zListType==0 ) zListType = "1";
12741397
getAllTicketFields();
12751398
@ <ol type="%s(zListType)">
12761399
for(i=0; i<pTkt->nField; i++){
1277
- Blob val;
1278
- const char *z, *zX;
1279
- int id;
1280
- z = pTkt->aField[i].zName;
1281
- blob_set(&val, pTkt->aField[i].zValue);
1282
- zX = z[0]=='+' ? z+1 : z;
1283
- id = fieldId(zX);
1400
+ const char *z = pTkt->aField[i].zName;
1401
+ const char *zX = z[0]=='+' ? z+1 : z;
1402
+ const int id = fieldId(zX);
1403
+ const char *zValue = pTkt->aField[i].zValue;
1404
+ const size_t nValue = strlen(zValue);
1405
+ const int bLong = nValue>50 || memchr(zValue,'\n',nValue)!=NULL;
1406
+ /* zValue is long enough to justify a <blockquote> */
1407
+ const int bCanDiff = aLastVal && id>=0 && aField[id].zBsln;
1408
+ /* preliminary flag for rendering via unified diff */
1409
+ int bAppend = 0; /* zValue is being appended to a TICKET's field */
1410
+ int bRegular = 0; /* prev value of a TICKET's field is being superseded*/
12841411
@ <li>\
12851412
if( id<0 ){
12861413
@ Untracked field %h(zX):
12871414
}else if( aField[id].mUsed==USEDBY_TICKETCHNG ){
12881415
@ %h(zX):
12891416
}else if( n==0 ){
12901417
@ %h(zX) initialized to:
12911418
}else if( z[0]=='+' && (aField[id].mUsed&USEDBY_TICKET)!=0 ){
12921419
@ Appended to %h(zX):
1293
- }else{
1294
- @ %h(zX) changed to:
1295
- }
1296
- if( blob_size(&val)>50 || contains_newline(&val) ){
1297
- @ <blockquote><pre class='verbatim'>
1298
- @ %h(blob_str(&val))
1299
- @ </pre></blockquote></li>
1300
- }else{
1301
- @ "%h(blob_str(&val))"</li>
1302
- }
1303
- blob_reset(&val);
1420
+ bAppend = 1;
1421
+ }else{
1422
+ if( !bCanDiff ){
1423
+ @ %h(zX) changed to: \
1424
+ }
1425
+ bRegular = 1;
1426
+ }
1427
+ if( bCanDiff ){
1428
+ Blob *prev = aLastVal+id;
1429
+ Blob val = BLOB_INITIALIZER;
1430
+ if( nValue ){
1431
+ blob_init(&val, zValue, nValue+1);
1432
+ val.nUsed--; /* makes blob_str() faster */
1433
+ }
1434
+ if( bRegular && nValue && blob_buffer(prev) && blob_size(prev) ){
1435
+ Blob d = BLOB_INITIALIZER;
1436
+ DiffConfig DCfg;
1437
+ construct_diff_flags(1, &DCfg);
1438
+ DCfg.diffFlags |= DIFF_HTML | DIFF_LINENO;
1439
+ text_diff(prev, &val, &d, &DCfg);
1440
+ @ %h(zX) changed as:
1441
+ @ %s(blob_str(&d))
1442
+ @ </li>
1443
+ blob_reset(&d);
1444
+ }else{
1445
+ if( bRegular ){
1446
+ @ %h(zX) changed to:
1447
+ }
1448
+ if( bLong ){
1449
+ @ <blockquote><pre class='verbatim'>
1450
+ @ %h(zValue)
1451
+ @ </pre></blockquote></li>
1452
+ }else{
1453
+ @ "%h(zValue)"</li>
1454
+ }
1455
+ }
1456
+ if( blob_buffer(prev) && blob_size(prev) && !bAppend ){
1457
+ blob_truncate(prev,0);
1458
+ }
1459
+ if( nValue ) blob_appendb(prev, &val);
1460
+ blob_reset(&val);
1461
+ }else{
1462
+ if( bLong ){
1463
+ @ <blockquote><pre class='verbatim'>
1464
+ @ %h(zValue)
1465
+ @ </pre></blockquote></li>
1466
+ }else{
1467
+ @ "%h(zValue)"</li>
1468
+ }
1469
+ }
13041470
}
13051471
@ </ol>
13061472
}
13071473
13081474
/*
@@ -1460,10 +1626,11 @@
14601626
}else{
14611627
/* add a new ticket or update an existing ticket */
14621628
enum { set,add,history,err } eCmd = err;
14631629
int i = 0;
14641630
Blob tktchng, cksum;
1631
+ char *aUsed;
14651632
14661633
/* get command type (set/add) and get uuid, if needed for set */
14671634
if( strncmp(g.argv[2],"set",n)==0 || strncmp(g.argv[2],"change",n)==0 ||
14681635
strncmp(g.argv[2],"history",n)==0 ){
14691636
if( strncmp(g.argv[2],"history",n)==0 ){
@@ -1602,10 +1769,11 @@
16021769
}else{
16031770
aField[j].zValue = zFValue;
16041771
}
16051772
}
16061773
}
1774
+ aUsed = fossil_malloc_zero( nField );
16071775
16081776
/* now add the needed artifacts to the repository */
16091777
blob_zero(&tktchng);
16101778
/* add the time to the ticket manifest */
16111779
blob_appendf(&tktchng, "D %s\n", zDate);
@@ -1615,34 +1783,39 @@
16151783
char *zPfx;
16161784
16171785
if( aField[i].zAppend && aField[i].zAppend[0] ){
16181786
zPfx = " +";
16191787
zValue = aField[i].zAppend;
1788
+ aUsed[i] = JCARD_APPEND;
16201789
}else if( aField[i].zValue && aField[i].zValue[0] ){
16211790
zPfx = " ";
16221791
zValue = aField[i].zValue;
1792
+ aUsed[i] = JCARD_ASSIGN;
16231793
}else{
16241794
continue;
16251795
}
16261796
if( memcmp(aField[i].zName, "private_", 8)==0 ){
16271797
zValue = db_conceal(zValue, strlen(zValue));
16281798
blob_appendf(&tktchng, "J%s%s %s\n", zPfx, aField[i].zName, zValue);
1799
+ aUsed[i] = JCARD_PRIVATE;
16291800
}else{
16301801
blob_appendf(&tktchng, "J%s%s %#F\n", zPfx,
16311802
aField[i].zName, strlen(zValue), zValue);
16321803
}
16331804
}
16341805
blob_appendf(&tktchng, "K %s\n", zTktUuid);
16351806
blob_appendf(&tktchng, "U %F\n", zUser);
16361807
md5sum_blob(&tktchng, &cksum);
16371808
blob_appendf(&tktchng, "Z %b\n", &cksum);
1638
- if( ticket_put(&tktchng, zTktUuid, ticket_need_moderation(1))==0 ){
1809
+ if( ticket_put(&tktchng, zTktUuid, aUsed,
1810
+ ticket_need_moderation(1) )==0 ){
16391811
fossil_fatal("%s", g.zErrMsg);
16401812
}else{
16411813
fossil_print("ticket %s succeeded for %s\n",
16421814
(eCmd==set?"set":"add"),zTktUuid);
16431815
}
1816
+ fossil_free( aUsed );
16441817
}
16451818
}
16461819
}
16471820
16481821
16491822
--- src/tkt.c
+++ src/tkt.c
@@ -30,22 +30,27 @@
30 static int nField = 0;
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 #define USEDBY_BOTH 03
 
 
 
40 static u8 haveTicket = 0; /* True if the TICKET table exists */
41 static u8 haveTicketCTime = 0; /* True if TICKET.TKT_CTIME exists */
42 static u8 haveTicketChng = 0; /* True if the TICKETCHNG table exists */
43 static u8 haveTicketChngRid = 0; /* True if TICKETCHNG.TKT_RID exists */
44 static u8 haveTicketChngUser = 0;/* True if TICKETCHNG.TKT_USER exists */
45 static u8 useTicketGenMt = 0; /* use generated TICKET.MIMETYPE */
46 static u8 useTicketChngGenMt = 0;/* use generated TICKETCHNG.MIMETYPE */
 
47
48
49 /*
50 ** Compare two entries in aField[] for sorting purposes
51 */
@@ -73,31 +78,56 @@
73 ** The haveTicket and haveTicketChng variables are set to 1 if the TICKET and
74 ** TICKETCHANGE tables exist, respectively.
75 */
76 static void getAllTicketFields(void){
77 Stmt q;
78 int i, noRegularMimetype;
79 static int once = 0;
80 if( once ) return;
81 once = 1;
 
82 db_prepare(&q, "PRAGMA table_info(ticket)");
83 while( db_step(&q)==SQLITE_ROW ){
84 const char *zFieldName = db_column_text(&q, 1);
85 haveTicket = 1;
86 if( memcmp(zFieldName,"tkt_",4)==0 ){
87 if( strcmp(zFieldName, "tkt_ctime")==0 ) haveTicketCTime = 1;
88 continue;
 
 
 
 
 
 
89 }
90 if( strchr(zFieldName,' ')!=0 ) continue;
91 if( nField%10==0 ){
92 aField = fossil_realloc(aField, sizeof(aField[0])*(nField+10) );
93 }
 
94 aField[nField].zName = mprintf("%s", zFieldName);
95 aField[nField].mUsed = USEDBY_TICKET;
96 nField++;
97 }
98 db_finalize(&q);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99 db_prepare(&q, "PRAGMA table_info(ticketchng)");
100 while( db_step(&q)==SQLITE_ROW ){
101 const char *zFieldName = db_column_text(&q, 1);
102 haveTicketChng = 1;
103 if( memcmp(zFieldName,"tkt_",4)==0 ){
@@ -114,10 +144,11 @@
114 continue;
115 }
116 if( nField%10==0 ){
117 aField = fossil_realloc(aField, sizeof(aField[0])*(nField+10) );
118 }
 
119 aField[nField].zName = mprintf("%s", zFieldName);
120 aField[nField].mUsed = USEDBY_TICKETCHNG;
121 nField++;
122 }
123 db_finalize(&q);
@@ -201,21 +232,39 @@
201 for(i=0; (z = cgi_parameter_name(i))!=0; i++){
202 Th_Store(z, P(z));
203 }
204 }
205
 
 
 
 
 
 
 
 
 
 
206 /*
207 ** Update an entry of the TICKET and TICKETCHNG tables according to the
208 ** information in the ticket artifact given in p. Attempt to create
209 ** the appropriate TICKET table entry if tktid is zero. If tktid is nonzero
210 ** then it will be the ROWID of an existing TICKET entry.
211 **
212 ** Parameter rid is the recordID for the ticket artifact in the BLOB table.
 
 
 
 
 
 
 
213 **
214 ** Return the new rowid of the TICKET table entry.
215 */
216 static int ticket_insert(const Manifest *p, const int rid, int tktid){
 
217 Blob sql1; /* update or replace TICKET ... */
218 Blob sql2; /* list of TICKETCHNG's fields that are in the manifest */
219 Blob sql3; /* list of values which correspond to the previous list */
220 Stmt q;
221 int i, j;
@@ -232,12 +281,11 @@
232 blob_zero(&sql3);
233 blob_append_sql(&sql1, "UPDATE OR REPLACE ticket SET tkt_mtime=:mtime");
234 if( haveTicketCTime ){
235 blob_append_sql(&sql1, ", tkt_ctime=coalesce(tkt_ctime,:mtime)");
236 }
237 aUsed = fossil_malloc( nField );
238 memset(aUsed, 0, nField);
239 for(i=0; i<p->nField; i++){
240 const char * const zName = p->aField[i].zName;
241 const char * const zBaseName = zName[0]=='+' ? zName+1 : zName;
242 j = fieldId(zBaseName);
243 if( j<0 ) continue;
@@ -244,12 +292,16 @@
244 aUsed[j] = 1;
245 if( aField[j].mUsed & USEDBY_TICKET ){
246 if( zName[0]=='+' ){
247 blob_append_sql(&sql1,", \"%w\"=coalesce(\"%w\",'') || %Q",
248 zBaseName, zBaseName, p->aField[i].zValue);
 
249 }else{
250 blob_append_sql(&sql1,", \"%w\"=%Q", zBaseName, p->aField[i].zValue);
 
 
 
251 }
252 }
253 if( aField[j].mUsed & USEDBY_TICKETCHNG ){
254 blob_append_sql(&sql2, ",\"%w\"", zBaseName);
255 blob_append_sql(&sql3, ",%Q", p->aField[i].zValue);
@@ -327,24 +379,37 @@
327 }
328 blob_reset(&sql2);
329 blob_reset(&sql3);
330 fossil_free(aUsed);
331 if( rid>0 ){ /* extract backlinks */
332 int bReplace = 1, mimetype;
333 for(i=0; i<p->nField; i++){
334 const char *zName = p->aField[i].zName;
335 const char *zBaseName = zName[0]=='+' ? zName+1 : zName;
336 j = fieldId(zBaseName);
337 if( j<0 ) continue;
338 if( aField[j].mUsed & USEDBY_TICKETCHNG ){
339 mimetype = mimetype_tktchng;
 
 
 
340 }else{
341 mimetype = mimetype_tkt;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
342 }
343 backlink_extract(p->aField[i].zValue, mimetype, rid, BKLNK_TICKET,
344 p->rDate, bReplace);
345 bReplace = 0;
346 }
347 }
348 return tktid;
349 }
350
@@ -377,12 +442,13 @@
377 void ticket_rebuild_entry(const char *zTktUuid){
378 char *zTag = mprintf("tkt-%s", zTktUuid);
379 int tagid = tag_findid(zTag, 1);
380 Stmt q;
381 Manifest *pTicket;
382 int tktid;
383 int createFlag = 1;
 
384
385 fossil_free(zTag);
386 getAllTicketFields();
387 if( haveTicket==0 ) return;
388 tktid = db_int(0, "SELECT tkt_id FROM ticket WHERE tkt_uuid=%Q", zTktUuid);
@@ -390,22 +456,41 @@
390 if( haveTicketChng ){
391 db_multi_exec("DELETE FROM ticketchng WHERE tkt_id=%d;", tktid);
392 }
393 db_multi_exec("DELETE FROM ticket WHERE tkt_id=%d", tktid);
394 tktid = 0;
 
 
 
395 db_prepare(&q, "SELECT rid FROM tagxref WHERE tagid=%d ORDER BY mtime",tagid);
396 while( db_step(&q)==SQLITE_ROW ){
397 int rid = db_column_int(&q, 0);
398 pTicket = manifest_get(rid, CFTYPE_TICKET, 0);
399 if( pTicket ){
400 tktid = ticket_insert(pTicket, rid, tktid);
401 manifest_ticket_event(rid, pTicket, createFlag, tagid);
402 manifest_destroy(pTicket);
403 }
404 createFlag = 0;
405 }
406 db_finalize(&q);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407 }
408
409
410 /*
411 ** Create the TH1 interpreter and load the "common" code.
@@ -738,23 +823,41 @@
738 return TH_OK;
739 }
740
741 /*
742 ** Write a ticket into the repository.
 
 
743 */
744 static int ticket_put(
745 Blob *pTicket, /* The text of the ticket change record */
746 const char *zTktId, /* The ticket to which this change is applied */
 
747 int needMod /* True if moderation is needed */
748 ){
749 int result;
750 int rid;
751 manifest_crosslink_begin();
752 rid = content_put_ex(pTicket, 0, 0, 0, needMod);
753 if( rid==0 ){
754 fossil_fatal("trouble committing ticket: %s", g.zErrMsg);
755 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
756 if( needMod ){
757 moderation_table_create();
758 db_multi_exec(
759 "INSERT INTO modreq(objid, tktid) VALUES(%d,%Q)",
760 rid, zTktId
@@ -787,14 +890,14 @@
787 void *pUuid,
788 int argc,
789 const char **argv,
790 int *argl
791 ){
792 char *zDate;
793 const char *zUuid;
794 int i;
795 int nJ = 0;
796 Blob tktchng, cksum;
797 int needMod;
798
799 login_verify_csrf_secret();
800 if( !captcha_is_correct(0) ){
@@ -804,15 +907,17 @@
804 zUuid = (const char *)pUuid;
805 blob_zero(&tktchng);
806 zDate = date_in_standard_format("now");
807 blob_appendf(&tktchng, "D %s\n", zDate);
808 free(zDate);
 
809 for(i=0; i<nField; i++){
810 if( aField[i].zAppend ){
811 blob_appendf(&tktchng, "J +%s %z\n", aField[i].zName,
812 fossilize(aField[i].zAppend, -1));
813 ++nJ;
 
814 }
815 }
816 for(i=0; i<nField; i++){
817 const char *zValue;
818 int nValue;
@@ -825,12 +930,14 @@
825 || strlen(aField[i].zValue)!=nValue
826 ){
827 if( memcmp(aField[i].zName, "private_", 8)==0 ){
828 zValue = db_conceal(zValue, nValue);
829 blob_appendf(&tktchng, "J %s %s\n", aField[i].zName, zValue);
 
830 }else{
831 blob_appendf(&tktchng, "J %s %#F\n", aField[i].zName, nValue, zValue);
 
832 }
833 nJ++;
834 }
835 }
836 }
@@ -846,11 +953,11 @@
846 blob_appendf(&tktchng, "U %F\n", login_name());
847 md5sum_blob(&tktchng, &cksum);
848 blob_appendf(&tktchng, "Z %b\n", &cksum);
849 if( nJ==0 ){
850 blob_reset(&tktchng);
851 return TH_OK;
852 }
853 needMod = ticket_need_moderation(0);
854 if( g.zPath[0]=='d' ){
855 const char *zNeedMod = needMod ? "required" : "skipped";
856 /* If called from /debug_tktnew or /debug_tktedit... */
@@ -858,20 +965,22 @@
858 @ <p>Ticket artifact that would have been submitted:</p>
859 @ <blockquote><pre>%h(blob_str(&tktchng))</pre></blockquote>
860 @ <blockquote><pre>Moderation would be %h(zNeedMod).</pre></blockquote>
861 @ </div>
862 @ <hr />
863 return TH_OK;
864 }else{
865 if( g.thTrace ){
866 Th_Trace("submit_ticket {\n<blockquote><pre>\n%h\n</pre></blockquote>\n"
867 "}<br />\n",
868 blob_str(&tktchng));
869 }
870 ticket_put(&tktchng, zUuid, needMod);
 
871 }
872 return ticket_change(zUuid);
 
 
873 }
874
875
876 /*
877 ** WEBPAGE: tktnew
@@ -1141,17 +1250,21 @@
1141 ** By default, the artifacts are decoded and formatted. Text fields
1142 ** are formatted as text/plain, since in the general case Fossil does
1143 ** not have knowledge of the encoding. If the "raw" query parameter
1144 ** is present, then the undecoded and unformatted text of each artifact
1145 ** is displayed.
 
 
 
1146 */
1147 void tkthistory_page(void){
1148 Stmt q;
1149 char *zTitle;
1150 const char *zUuid;
1151 int tagid;
1152 int nChng = 0;
 
1153
1154 login_check_credentials();
1155 if( !g.perm.Hyperlink || !g.perm.RdTkt ){
1156 login_needed(g.anon.Hyperlink && g.anon.RdTkt);
1157 return;
@@ -1177,10 +1290,14 @@
1177 }
1178 if( P("raw")!=0 ){
1179 @ <h2>Raw Artifacts Associated With Ticket %h(zUuid)</h2>
1180 }else{
1181 @ <h2>Artifacts Associated With Ticket %h(zUuid)</h2>
 
 
 
 
1182 }
1183 db_prepare(&q,
1184 "SELECT datetime(mtime,toLocal()), objid, uuid, NULL, NULL, NULL"
1185 " FROM event, blob"
1186 " WHERE objid IN (SELECT rid FROM tagxref WHERE tagid=%d)"
@@ -1198,33 +1315,33 @@
1198 const char *zDate = db_column_text(&q, 0);
1199 int rid = db_column_int(&q, 1);
1200 const char *zChngUuid = db_column_text(&q, 2);
1201 const char *zFile = db_column_text(&q, 4);
1202 if( nChng==0 ){
1203 @ <ol>
1204 }
1205 if( zFile!=0 ){
1206 const char *zSrc = db_column_text(&q, 3);
1207 const char *zUser = db_column_text(&q, 5);
 
 
1208 if( zSrc==0 || zSrc[0]==0 ){
1209 @
1210 @ <li><p>Delete attachment "%h(zFile)"
1211 }else{
1212 @
1213 @ <li><p>Add attachment
1214 @ "%z(href("%R/artifact/%!S",zSrc))%s(zFile)</a>"
1215 }
1216 @ [%z(href("%R/artifact/%!S",zChngUuid))%S(zChngUuid)</a>]
1217 @ (rid %d(rid)) by
1218 hyperlink_to_user(zUser,zDate," on");
1219 hyperlink_to_date(zDate, ".</p>");
1220 }else{
1221 pTicket = manifest_get(rid, CFTYPE_TICKET, 0);
1222 if( pTicket ){
1223 @
1224 @ <li><p>Ticket change
1225 @ [%z(href("%R/artifact/%!S",zChngUuid))%S(zChngUuid)</a>]
1226 @ (rid %d(rid)) by
1227 hyperlink_to_user(pTicket->zUser,zDate," on");
1228 hyperlink_to_date(zDate, ":");
1229 @ </p>
1230 if( P("raw")!=0 ){
@@ -1233,21 +1350,23 @@
1233 @ <blockquote><pre>
1234 @ %h(blob_str(&c))
1235 @ </pre></blockquote>
1236 blob_reset(&c);
1237 }else{
1238 ticket_output_change_artifact(pTicket, "a", nChng);
1239 }
1240 }
1241 manifest_destroy(pTicket);
1242 }
 
1243 }
1244 db_finalize(&q);
1245 if( nChng ){
1246 @ </ol>
1247 }
1248 style_finish_page();
 
1249 }
1250
1251 /*
1252 ** Return TRUE if the given BLOB contains a newline character.
1253 */
@@ -1261,48 +1380,95 @@
1261 }
1262
1263 /*
1264 ** The pTkt object is a ticket change artifact. Output a detailed
1265 ** description of this object.
 
 
 
1266 */
1267 void ticket_output_change_artifact(
1268 Manifest *pTkt, /* Parsed artifact for the ticket change */
1269 const char *zListType, /* Which type of list */
1270 int n /* Which ticket change is this */
 
1271 ){
1272 int i;
1273 if( zListType==0 ) zListType = "1";
1274 getAllTicketFields();
1275 @ <ol type="%s(zListType)">
1276 for(i=0; i<pTkt->nField; i++){
1277 Blob val;
1278 const char *z, *zX;
1279 int id;
1280 z = pTkt->aField[i].zName;
1281 blob_set(&val, pTkt->aField[i].zValue);
1282 zX = z[0]=='+' ? z+1 : z;
1283 id = fieldId(zX);
 
 
 
 
1284 @ <li>\
1285 if( id<0 ){
1286 @ Untracked field %h(zX):
1287 }else if( aField[id].mUsed==USEDBY_TICKETCHNG ){
1288 @ %h(zX):
1289 }else if( n==0 ){
1290 @ %h(zX) initialized to:
1291 }else if( z[0]=='+' && (aField[id].mUsed&USEDBY_TICKET)!=0 ){
1292 @ Appended to %h(zX):
1293 }else{
1294 @ %h(zX) changed to:
1295 }
1296 if( blob_size(&val)>50 || contains_newline(&val) ){
1297 @ <blockquote><pre class='verbatim'>
1298 @ %h(blob_str(&val))
1299 @ </pre></blockquote></li>
1300 }else{
1301 @ "%h(blob_str(&val))"</li>
1302 }
1303 blob_reset(&val);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1304 }
1305 @ </ol>
1306 }
1307
1308 /*
@@ -1460,10 +1626,11 @@
1460 }else{
1461 /* add a new ticket or update an existing ticket */
1462 enum { set,add,history,err } eCmd = err;
1463 int i = 0;
1464 Blob tktchng, cksum;
 
1465
1466 /* get command type (set/add) and get uuid, if needed for set */
1467 if( strncmp(g.argv[2],"set",n)==0 || strncmp(g.argv[2],"change",n)==0 ||
1468 strncmp(g.argv[2],"history",n)==0 ){
1469 if( strncmp(g.argv[2],"history",n)==0 ){
@@ -1602,10 +1769,11 @@
1602 }else{
1603 aField[j].zValue = zFValue;
1604 }
1605 }
1606 }
 
1607
1608 /* now add the needed artifacts to the repository */
1609 blob_zero(&tktchng);
1610 /* add the time to the ticket manifest */
1611 blob_appendf(&tktchng, "D %s\n", zDate);
@@ -1615,34 +1783,39 @@
1615 char *zPfx;
1616
1617 if( aField[i].zAppend && aField[i].zAppend[0] ){
1618 zPfx = " +";
1619 zValue = aField[i].zAppend;
 
1620 }else if( aField[i].zValue && aField[i].zValue[0] ){
1621 zPfx = " ";
1622 zValue = aField[i].zValue;
 
1623 }else{
1624 continue;
1625 }
1626 if( memcmp(aField[i].zName, "private_", 8)==0 ){
1627 zValue = db_conceal(zValue, strlen(zValue));
1628 blob_appendf(&tktchng, "J%s%s %s\n", zPfx, aField[i].zName, zValue);
 
1629 }else{
1630 blob_appendf(&tktchng, "J%s%s %#F\n", zPfx,
1631 aField[i].zName, strlen(zValue), zValue);
1632 }
1633 }
1634 blob_appendf(&tktchng, "K %s\n", zTktUuid);
1635 blob_appendf(&tktchng, "U %F\n", zUser);
1636 md5sum_blob(&tktchng, &cksum);
1637 blob_appendf(&tktchng, "Z %b\n", &cksum);
1638 if( ticket_put(&tktchng, zTktUuid, ticket_need_moderation(1))==0 ){
 
1639 fossil_fatal("%s", g.zErrMsg);
1640 }else{
1641 fossil_print("ticket %s succeeded for %s\n",
1642 (eCmd==set?"set":"add"),zTktUuid);
1643 }
 
1644 }
1645 }
1646 }
1647
1648
1649
--- src/tkt.c
+++ src/tkt.c
@@ -30,22 +30,27 @@
30 static int nField = 0;
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 char *zBsln; /* "baseline for $zName" if that field exists*/
36 unsigned mUsed; /* 01: TICKET 02: TICKETCHNG */
37 } *aField;
38 #define USEDBY_TICKET 01
39 #define USEDBY_TICKETCHNG 02
40 #define USEDBY_BOTH 03
41 #define JCARD_ASSIGN ('=')
42 #define JCARD_APPEND ('+')
43 #define JCARD_PRIVATE ('p')
44 static u8 haveTicket = 0; /* True if the TICKET table exists */
45 static u8 haveTicketCTime = 0; /* True if TICKET.TKT_CTIME exists */
46 static u8 haveTicketChng = 0; /* True if the TICKETCHNG table exists */
47 static u8 haveTicketChngRid = 0; /* True if TICKETCHNG.TKT_RID exists */
48 static u8 haveTicketChngUser = 0;/* True if TICKETCHNG.TKT_USER exists */
49 static u8 useTicketGenMt = 0; /* use generated TICKET.MIMETYPE */
50 static u8 useTicketChngGenMt = 0;/* use generated TICKETCHNG.MIMETYPE */
51 static int nTicketBslns = 0; /* number of valid "baseline for ..." */
52
53
54 /*
55 ** Compare two entries in aField[] for sorting purposes
56 */
@@ -73,31 +78,56 @@
78 ** The haveTicket and haveTicketChng variables are set to 1 if the TICKET and
79 ** TICKETCHANGE tables exist, respectively.
80 */
81 static void getAllTicketFields(void){
82 Stmt q;
83 int i, noRegularMimetype, nBaselines;
84 static int once = 0;
85 if( once ) return;
86 once = 1;
87 nBaselines = 0;
88 db_prepare(&q, "PRAGMA table_info(ticket)");
89 while( db_step(&q)==SQLITE_ROW ){
90 const char *zFieldName = db_column_text(&q, 1);
91 haveTicket = 1;
92 if( memcmp(zFieldName,"tkt_",4)==0 ){
93 if( strcmp(zFieldName, "tkt_ctime")==0 ) haveTicketCTime = 1;
94 continue;
95 }
96 if( memcmp(zFieldName,"baseline for ",13)==0 ){
97 if( strcmp(db_column_text(&q,2),"INTEGER")==0 ){
98 nBaselines++;
99 }
100 continue;
101 }
102 if( strchr(zFieldName,' ')!=0 ) continue;
103 if( nField%10==0 ){
104 aField = fossil_realloc(aField, sizeof(aField[0])*(nField+10) );
105 }
106 aField[nField].zBsln = 0;
107 aField[nField].zName = mprintf("%s", zFieldName);
108 aField[nField].mUsed = USEDBY_TICKET;
109 nField++;
110 }
111 db_finalize(&q);
112 if( nBaselines ){
113 db_prepare(&q, "SELECT 1 FROM pragma_table_info('ticket') "
114 "WHERE type = 'INTEGER' AND name = :n");
115 for(i=0; i<nField && nBaselines!=0; i++){
116 char *zBsln = mprintf("baseline for %s",aField[i].zName);
117 db_bind_text(&q, ":n", zBsln);
118 if( db_step(&q)==SQLITE_ROW ){
119 aField[i].zBsln = zBsln;
120 nTicketBslns++;
121 nBaselines--;
122 }else{
123 free(zBsln);
124 }
125 db_reset(&q);
126 }
127 db_finalize(&q);
128 }
129 db_prepare(&q, "PRAGMA table_info(ticketchng)");
130 while( db_step(&q)==SQLITE_ROW ){
131 const char *zFieldName = db_column_text(&q, 1);
132 haveTicketChng = 1;
133 if( memcmp(zFieldName,"tkt_",4)==0 ){
@@ -114,10 +144,11 @@
144 continue;
145 }
146 if( nField%10==0 ){
147 aField = fossil_realloc(aField, sizeof(aField[0])*(nField+10) );
148 }
149 aField[nField].zBsln = 0;
150 aField[nField].zName = mprintf("%s", zFieldName);
151 aField[nField].mUsed = USEDBY_TICKETCHNG;
152 nField++;
153 }
154 db_finalize(&q);
@@ -201,21 +232,39 @@
232 for(i=0; (z = cgi_parameter_name(i))!=0; i++){
233 Th_Store(z, P(z));
234 }
235 }
236
237 /*
238 ** Information about a single J-card
239 */
240 struct jCardInfo {
241 char *zValue;
242 int mimetype;
243 int rid;
244 double mtime;
245 };
246
247 /*
248 ** Update an entry of the TICKET and TICKETCHNG tables according to the
249 ** information in the ticket artifact given in p. Attempt to create
250 ** the appropriate TICKET table entry if tktid is zero. If tktid is nonzero
251 ** then it will be the ROWID of an existing TICKET entry.
252 **
253 ** Parameter rid is the recordID for the ticket artifact in the BLOB table.
254 ** Upon assignment of a field this rid is stored into a corresponding
255 ** zBsln integer column (provided that it is defined within TICKET table).
256 **
257 ** If a field is USEDBY_TICKETCHNG table then back-references within it
258 ** are extracted and inserted into the BACKLINK table; otherwise
259 ** a corresponding blob in the `fields` array is updated so that the
260 ** caller could extract backlinks from the most recent field's values.
261 **
262 ** Return the new rowid of the TICKET table entry.
263 */
264 static int ticket_insert(const Manifest *p, const int rid, int tktid,
265 Blob *fields){
266 Blob sql1; /* update or replace TICKET ... */
267 Blob sql2; /* list of TICKETCHNG's fields that are in the manifest */
268 Blob sql3; /* list of values which correspond to the previous list */
269 Stmt q;
270 int i, j;
@@ -232,12 +281,11 @@
281 blob_zero(&sql3);
282 blob_append_sql(&sql1, "UPDATE OR REPLACE ticket SET tkt_mtime=:mtime");
283 if( haveTicketCTime ){
284 blob_append_sql(&sql1, ", tkt_ctime=coalesce(tkt_ctime,:mtime)");
285 }
286 aUsed = fossil_malloc_zero( nField );
 
287 for(i=0; i<p->nField; i++){
288 const char * const zName = p->aField[i].zName;
289 const char * const zBaseName = zName[0]=='+' ? zName+1 : zName;
290 j = fieldId(zBaseName);
291 if( j<0 ) continue;
@@ -244,12 +292,16 @@
292 aUsed[j] = 1;
293 if( aField[j].mUsed & USEDBY_TICKET ){
294 if( zName[0]=='+' ){
295 blob_append_sql(&sql1,", \"%w\"=coalesce(\"%w\",'') || %Q",
296 zBaseName, zBaseName, p->aField[i].zValue);
297 /* when appending keep "baseline for ..." unchanged */
298 }else{
299 blob_append_sql(&sql1,", \"%w\"=%Q", zBaseName, p->aField[i].zValue);
300 if( aField[j].zBsln ){
301 blob_append_sql(&sql1,", \"%w\"=%d", aField[j].zBsln, rid);
302 }
303 }
304 }
305 if( aField[j].mUsed & USEDBY_TICKETCHNG ){
306 blob_append_sql(&sql2, ",\"%w\"", zBaseName);
307 blob_append_sql(&sql3, ",%Q", p->aField[i].zValue);
@@ -327,24 +379,37 @@
379 }
380 blob_reset(&sql2);
381 blob_reset(&sql3);
382 fossil_free(aUsed);
383 if( rid>0 ){ /* extract backlinks */
 
384 for(i=0; i<p->nField; i++){
385 const char *zName = p->aField[i].zName;
386 const char *zBaseName = zName[0]=='+' ? zName+1 : zName;
387 j = fieldId(zBaseName);
388 if( j<0 ) continue;
389 if( aField[j].mUsed & USEDBY_TICKETCHNG ){
390 backlink_extract(p->aField[i].zValue, mimetype_tktchng,
391 rid, BKLNK_TICKET, p->rDate,
392 /* existing backlinks must have been
393 * already deleted by the caller */ 0 );
394 }else{
395 /* update field's data with the most recent values */
396 Blob *cards = fields + j;
397 struct jCardInfo card = {
398 fossil_strdup(p->aField[i].zValue),
399 mimetype_tkt, rid, p->rDate
400 };
401 if( blob_size(cards) && zName[0]!='+' ){
402 struct jCardInfo *x = (struct jCardInfo *)blob_buffer(cards);
403 struct jCardInfo *end = x + blob_count(cards,struct jCardInfo);
404 for(; x!=end; x++){
405 fossil_free( x->zValue );
406 }
407 blob_truncate(cards,0);
408 }
409 blob_append(cards, (const char*)(&card), sizeof(card));
410 }
 
 
 
411 }
412 }
413 return tktid;
414 }
415
@@ -377,12 +442,13 @@
442 void ticket_rebuild_entry(const char *zTktUuid){
443 char *zTag = mprintf("tkt-%s", zTktUuid);
444 int tagid = tag_findid(zTag, 1);
445 Stmt q;
446 Manifest *pTicket;
447 int tktid, i;
448 int createFlag = 1;
449 Blob *fields; /* array of blobs; each blob holds array of jCardInfo */
450
451 fossil_free(zTag);
452 getAllTicketFields();
453 if( haveTicket==0 ) return;
454 tktid = db_int(0, "SELECT tkt_id FROM ticket WHERE tkt_uuid=%Q", zTktUuid);
@@ -390,22 +456,41 @@
456 if( haveTicketChng ){
457 db_multi_exec("DELETE FROM ticketchng WHERE tkt_id=%d;", tktid);
458 }
459 db_multi_exec("DELETE FROM ticket WHERE tkt_id=%d", tktid);
460 tktid = 0;
461 fields = blobarray_new( nField );
462 db_multi_exec("DELETE FROM backlink WHERE srctype=%d AND srcid IN "
463 "(SELECT rid FROM tagxref WHERE tagid=%d)",BKLNK_TICKET, tagid);
464 db_prepare(&q, "SELECT rid FROM tagxref WHERE tagid=%d ORDER BY mtime",tagid);
465 while( db_step(&q)==SQLITE_ROW ){
466 int rid = db_column_int(&q, 0);
467 pTicket = manifest_get(rid, CFTYPE_TICKET, 0);
468 if( pTicket ){
469 tktid = ticket_insert(pTicket, rid, tktid, fields);
470 manifest_ticket_event(rid, pTicket, createFlag, tagid);
471 manifest_destroy(pTicket);
472 }
473 createFlag = 0;
474 }
475 db_finalize(&q);
476 /* Extract backlinks from the most recent values of TICKET fields */
477 for(i=0; i<nField; i++){
478 Blob *cards = fields + i;
479 if( blob_size(cards) ){
480 struct jCardInfo *x = (struct jCardInfo *)blob_buffer(cards);
481 struct jCardInfo *end = x + blob_count(cards,struct jCardInfo);
482 for(; x!=end; x++){
483 assert( x->zValue );
484 backlink_extract(x->zValue,x->mimetype,
485 x->rid,BKLNK_TICKET,x->mtime,0);
486 fossil_free( x->zValue );
487 }
488 }
489 blob_truncate(cards,0);
490 }
491 blobarray_delete(fields,nField);
492 }
493
494
495 /*
496 ** Create the TH1 interpreter and load the "common" code.
@@ -738,23 +823,41 @@
823 return TH_OK;
824 }
825
826 /*
827 ** Write a ticket into the repository.
828 ** Upon reassignment of fields try to delta-compress an artifact against
829 ** all artifacts that are referenced in the corresponding zBsln fields.
830 */
831 static int ticket_put(
832 Blob *pTicket, /* The text of the ticket change record */
833 const char *zTktId, /* The ticket to which this change is applied */
834 const char *aUsed, /* Indicators for fields' modifications */
835 int needMod /* True if moderation is needed */
836 ){
837 int result;
838 int rid;
839 manifest_crosslink_begin();
840 rid = content_put_ex(pTicket, 0, 0, 0, needMod);
841 if( rid==0 ){
842 fossil_fatal("trouble committing ticket: %s", g.zErrMsg);
843 }
844 if( nTicketBslns ){
845 int i, s, buf[8], nSrc=0, *aSrc=&(buf[0]);
846 if( nTicketBslns > count(buf) ){
847 aSrc = (int*)fossil_malloc(sizeof(int)*nTicketBslns);
848 }
849 for(i=0; i<nField; i++){
850 if( aField[i].zBsln && aUsed[i]==JCARD_ASSIGN ){
851 s = db_int(0,"SELECT \"%w\" FROM ticket WHERE tkt_uuid = '%q'",
852 aField[i].zBsln, zTktId );
853 if( s > 0 ) aSrc[nSrc++] = s;
854 }
855 }
856 if( nSrc ) content_deltify(rid, aSrc, nSrc, 0);
857 if( aSrc!=&(buf[0]) ) fossil_free( aSrc );
858 }
859 if( needMod ){
860 moderation_table_create();
861 db_multi_exec(
862 "INSERT INTO modreq(objid, tktid) VALUES(%d,%Q)",
863 rid, zTktId
@@ -787,14 +890,14 @@
890 void *pUuid,
891 int argc,
892 const char **argv,
893 int *argl
894 ){
895 char *zDate, *aUsed;
896 const char *zUuid;
897 int i;
898 int nJ = 0, rc = TH_OK;
899 Blob tktchng, cksum;
900 int needMod;
901
902 login_verify_csrf_secret();
903 if( !captcha_is_correct(0) ){
@@ -804,15 +907,17 @@
907 zUuid = (const char *)pUuid;
908 blob_zero(&tktchng);
909 zDate = date_in_standard_format("now");
910 blob_appendf(&tktchng, "D %s\n", zDate);
911 free(zDate);
912 aUsed = fossil_malloc_zero( nField );
913 for(i=0; i<nField; i++){
914 if( aField[i].zAppend ){
915 blob_appendf(&tktchng, "J +%s %z\n", aField[i].zName,
916 fossilize(aField[i].zAppend, -1));
917 ++nJ;
918 aUsed[i] = JCARD_APPEND;
919 }
920 }
921 for(i=0; i<nField; i++){
922 const char *zValue;
923 int nValue;
@@ -825,12 +930,14 @@
930 || strlen(aField[i].zValue)!=nValue
931 ){
932 if( memcmp(aField[i].zName, "private_", 8)==0 ){
933 zValue = db_conceal(zValue, nValue);
934 blob_appendf(&tktchng, "J %s %s\n", aField[i].zName, zValue);
935 aUsed[i] = JCARD_PRIVATE;
936 }else{
937 blob_appendf(&tktchng, "J %s %#F\n", aField[i].zName, nValue, zValue);
938 aUsed[i] = JCARD_ASSIGN;
939 }
940 nJ++;
941 }
942 }
943 }
@@ -846,11 +953,11 @@
953 blob_appendf(&tktchng, "U %F\n", login_name());
954 md5sum_blob(&tktchng, &cksum);
955 blob_appendf(&tktchng, "Z %b\n", &cksum);
956 if( nJ==0 ){
957 blob_reset(&tktchng);
958 goto finish;
959 }
960 needMod = ticket_need_moderation(0);
961 if( g.zPath[0]=='d' ){
962 const char *zNeedMod = needMod ? "required" : "skipped";
963 /* If called from /debug_tktnew or /debug_tktedit... */
@@ -858,20 +965,22 @@
965 @ <p>Ticket artifact that would have been submitted:</p>
966 @ <blockquote><pre>%h(blob_str(&tktchng))</pre></blockquote>
967 @ <blockquote><pre>Moderation would be %h(zNeedMod).</pre></blockquote>
968 @ </div>
969 @ <hr />
 
970 }else{
971 if( g.thTrace ){
972 Th_Trace("submit_ticket {\n<blockquote><pre>\n%h\n</pre></blockquote>\n"
973 "}<br />\n",
974 blob_str(&tktchng));
975 }
976 ticket_put(&tktchng, zUuid, aUsed, needMod);
977 rc = ticket_change(zUuid);
978 }
979 finish:
980 fossil_free( aUsed );
981 return rc;
982 }
983
984
985 /*
986 ** WEBPAGE: tktnew
@@ -1141,17 +1250,21 @@
1250 ** By default, the artifacts are decoded and formatted. Text fields
1251 ** are formatted as text/plain, since in the general case Fossil does
1252 ** not have knowledge of the encoding. If the "raw" query parameter
1253 ** is present, then the undecoded and unformatted text of each artifact
1254 ** is displayed.
1255 **
1256 ** Reassignments of a field of the TICKET table that has a corresponding
1257 ** "baseline for ..." companion are rendered as unified diffs.
1258 */
1259 void tkthistory_page(void){
1260 Stmt q;
1261 char *zTitle;
1262 const char *zUuid;
1263 int tagid;
1264 int nChng = 0;
1265 Blob *aLastVal = 0; /* holds the last rendered value for each field */
1266
1267 login_check_credentials();
1268 if( !g.perm.Hyperlink || !g.perm.RdTkt ){
1269 login_needed(g.anon.Hyperlink && g.anon.RdTkt);
1270 return;
@@ -1177,10 +1290,14 @@
1290 }
1291 if( P("raw")!=0 ){
1292 @ <h2>Raw Artifacts Associated With Ticket %h(zUuid)</h2>
1293 }else{
1294 @ <h2>Artifacts Associated With Ticket %h(zUuid)</h2>
1295 getAllTicketFields();
1296 if( nTicketBslns ){
1297 aLastVal = blobarray_new(nField);
1298 }
1299 }
1300 db_prepare(&q,
1301 "SELECT datetime(mtime,toLocal()), objid, uuid, NULL, NULL, NULL"
1302 " FROM event, blob"
1303 " WHERE objid IN (SELECT rid FROM tagxref WHERE tagid=%d)"
@@ -1198,33 +1315,33 @@
1315 const char *zDate = db_column_text(&q, 0);
1316 int rid = db_column_int(&q, 1);
1317 const char *zChngUuid = db_column_text(&q, 2);
1318 const char *zFile = db_column_text(&q, 4);
1319 if( nChng==0 ){
1320 @ <ol class="tkt-changes">
1321 }
1322 if( zFile!=0 ){
1323 const char *zSrc = db_column_text(&q, 3);
1324 const char *zUser = db_column_text(&q, 5);
1325 @
1326 @ <li id="%S(zChngUuid)"><p><span>
1327 if( zSrc==0 || zSrc[0]==0 ){
1328 @ Delete attachment "%h(zFile)"
 
1329 }else{
1330 @ Add attachment
 
1331 @ "%z(href("%R/artifact/%!S",zSrc))%s(zFile)</a>"
1332 }
1333 @ [%z(href("%R/artifact/%!S",zChngUuid))%S(zChngUuid)</a>]</span>
1334 @ (rid %d(rid)) by
1335 hyperlink_to_user(zUser,zDate," on");
1336 hyperlink_to_date(zDate, ".</p>");
1337 }else{
1338 pTicket = manifest_get(rid, CFTYPE_TICKET, 0);
1339 if( pTicket ){
1340 @
1341 @ <li id="%S(zChngUuid)"><p><span>Ticket change
1342 @ [%z(href("%R/artifact/%!S",zChngUuid))%S(zChngUuid)</a>]</span>
1343 @ (rid %d(rid)) by
1344 hyperlink_to_user(pTicket->zUser,zDate," on");
1345 hyperlink_to_date(zDate, ":");
1346 @ </p>
1347 if( P("raw")!=0 ){
@@ -1233,21 +1350,23 @@
1350 @ <blockquote><pre>
1351 @ %h(blob_str(&c))
1352 @ </pre></blockquote>
1353 blob_reset(&c);
1354 }else{
1355 ticket_output_change_artifact(pTicket, "a", nChng, aLastVal);
1356 }
1357 }
1358 manifest_destroy(pTicket);
1359 }
1360 @ </li>
1361 }
1362 db_finalize(&q);
1363 if( nChng ){
1364 @ </ol>
1365 }
1366 style_finish_page();
1367 if( aLastVal ) blobarray_delete(aLastVal, nField);
1368 }
1369
1370 /*
1371 ** Return TRUE if the given BLOB contains a newline character.
1372 */
@@ -1261,48 +1380,95 @@
1380 }
1381
1382 /*
1383 ** The pTkt object is a ticket change artifact. Output a detailed
1384 ** description of this object.
1385 **
1386 ** If `aLastVal` is not NULL then render selected fields as unified diffs
1387 ** and update corresponding elements of that array with values from `pTkt`.
1388 */
1389 void ticket_output_change_artifact(
1390 Manifest *pTkt, /* Parsed artifact for the ticket change */
1391 const char *zListType, /* Which type of list */
1392 int n, /* Which ticket change is this */
1393 Blob *aLastVal /* Array of the latest values for the diffs */
1394 ){
1395 int i;
1396 if( zListType==0 ) zListType = "1";
1397 getAllTicketFields();
1398 @ <ol type="%s(zListType)">
1399 for(i=0; i<pTkt->nField; i++){
1400 const char *z = pTkt->aField[i].zName;
1401 const char *zX = z[0]=='+' ? z+1 : z;
1402 const int id = fieldId(zX);
1403 const char *zValue = pTkt->aField[i].zValue;
1404 const size_t nValue = strlen(zValue);
1405 const int bLong = nValue>50 || memchr(zValue,'\n',nValue)!=NULL;
1406 /* zValue is long enough to justify a <blockquote> */
1407 const int bCanDiff = aLastVal && id>=0 && aField[id].zBsln;
1408 /* preliminary flag for rendering via unified diff */
1409 int bAppend = 0; /* zValue is being appended to a TICKET's field */
1410 int bRegular = 0; /* prev value of a TICKET's field is being superseded*/
1411 @ <li>\
1412 if( id<0 ){
1413 @ Untracked field %h(zX):
1414 }else if( aField[id].mUsed==USEDBY_TICKETCHNG ){
1415 @ %h(zX):
1416 }else if( n==0 ){
1417 @ %h(zX) initialized to:
1418 }else if( z[0]=='+' && (aField[id].mUsed&USEDBY_TICKET)!=0 ){
1419 @ Appended to %h(zX):
1420 bAppend = 1;
1421 }else{
1422 if( !bCanDiff ){
1423 @ %h(zX) changed to: \
1424 }
1425 bRegular = 1;
1426 }
1427 if( bCanDiff ){
1428 Blob *prev = aLastVal+id;
1429 Blob val = BLOB_INITIALIZER;
1430 if( nValue ){
1431 blob_init(&val, zValue, nValue+1);
1432 val.nUsed--; /* makes blob_str() faster */
1433 }
1434 if( bRegular && nValue && blob_buffer(prev) && blob_size(prev) ){
1435 Blob d = BLOB_INITIALIZER;
1436 DiffConfig DCfg;
1437 construct_diff_flags(1, &DCfg);
1438 DCfg.diffFlags |= DIFF_HTML | DIFF_LINENO;
1439 text_diff(prev, &val, &d, &DCfg);
1440 @ %h(zX) changed as:
1441 @ %s(blob_str(&d))
1442 @ </li>
1443 blob_reset(&d);
1444 }else{
1445 if( bRegular ){
1446 @ %h(zX) changed to:
1447 }
1448 if( bLong ){
1449 @ <blockquote><pre class='verbatim'>
1450 @ %h(zValue)
1451 @ </pre></blockquote></li>
1452 }else{
1453 @ "%h(zValue)"</li>
1454 }
1455 }
1456 if( blob_buffer(prev) && blob_size(prev) && !bAppend ){
1457 blob_truncate(prev,0);
1458 }
1459 if( nValue ) blob_appendb(prev, &val);
1460 blob_reset(&val);
1461 }else{
1462 if( bLong ){
1463 @ <blockquote><pre class='verbatim'>
1464 @ %h(zValue)
1465 @ </pre></blockquote></li>
1466 }else{
1467 @ "%h(zValue)"</li>
1468 }
1469 }
1470 }
1471 @ </ol>
1472 }
1473
1474 /*
@@ -1460,10 +1626,11 @@
1626 }else{
1627 /* add a new ticket or update an existing ticket */
1628 enum { set,add,history,err } eCmd = err;
1629 int i = 0;
1630 Blob tktchng, cksum;
1631 char *aUsed;
1632
1633 /* get command type (set/add) and get uuid, if needed for set */
1634 if( strncmp(g.argv[2],"set",n)==0 || strncmp(g.argv[2],"change",n)==0 ||
1635 strncmp(g.argv[2],"history",n)==0 ){
1636 if( strncmp(g.argv[2],"history",n)==0 ){
@@ -1602,10 +1769,11 @@
1769 }else{
1770 aField[j].zValue = zFValue;
1771 }
1772 }
1773 }
1774 aUsed = fossil_malloc_zero( nField );
1775
1776 /* now add the needed artifacts to the repository */
1777 blob_zero(&tktchng);
1778 /* add the time to the ticket manifest */
1779 blob_appendf(&tktchng, "D %s\n", zDate);
@@ -1615,34 +1783,39 @@
1783 char *zPfx;
1784
1785 if( aField[i].zAppend && aField[i].zAppend[0] ){
1786 zPfx = " +";
1787 zValue = aField[i].zAppend;
1788 aUsed[i] = JCARD_APPEND;
1789 }else if( aField[i].zValue && aField[i].zValue[0] ){
1790 zPfx = " ";
1791 zValue = aField[i].zValue;
1792 aUsed[i] = JCARD_ASSIGN;
1793 }else{
1794 continue;
1795 }
1796 if( memcmp(aField[i].zName, "private_", 8)==0 ){
1797 zValue = db_conceal(zValue, strlen(zValue));
1798 blob_appendf(&tktchng, "J%s%s %s\n", zPfx, aField[i].zName, zValue);
1799 aUsed[i] = JCARD_PRIVATE;
1800 }else{
1801 blob_appendf(&tktchng, "J%s%s %#F\n", zPfx,
1802 aField[i].zName, strlen(zValue), zValue);
1803 }
1804 }
1805 blob_appendf(&tktchng, "K %s\n", zTktUuid);
1806 blob_appendf(&tktchng, "U %F\n", zUser);
1807 md5sum_blob(&tktchng, &cksum);
1808 blob_appendf(&tktchng, "Z %b\n", &cksum);
1809 if( ticket_put(&tktchng, zTktUuid, aUsed,
1810 ticket_need_moderation(1) )==0 ){
1811 fossil_fatal("%s", g.zErrMsg);
1812 }else{
1813 fossil_print("ticket %s succeeded for %s\n",
1814 (eCmd==set?"set":"add"),zTktUuid);
1815 }
1816 fossil_free( aUsed );
1817 }
1818 }
1819 }
1820
1821
1822
-2
--- src/wiki.c
+++ src/wiki.c
@@ -1867,13 +1867,11 @@
18671867
style_header("Changes To %s", pW1->zWikiTitle);
18681868
blob_zero(&d);
18691869
construct_diff_flags(1, &DCfg);
18701870
DCfg.diffFlags |= DIFF_HTML | DIFF_LINENO;
18711871
text_diff(&w2, &w1, &d, &DCfg);
1872
- @ <pre class="udiff">
18731872
@ %s(blob_str(&d))
1874
- @ <pre>
18751873
manifest_destroy(pW1);
18761874
manifest_destroy(pW2);
18771875
style_finish_page();
18781876
}
18791877
18801878
--- src/wiki.c
+++ src/wiki.c
@@ -1867,13 +1867,11 @@
1867 style_header("Changes To %s", pW1->zWikiTitle);
1868 blob_zero(&d);
1869 construct_diff_flags(1, &DCfg);
1870 DCfg.diffFlags |= DIFF_HTML | DIFF_LINENO;
1871 text_diff(&w2, &w1, &d, &DCfg);
1872 @ <pre class="udiff">
1873 @ %s(blob_str(&d))
1874 @ <pre>
1875 manifest_destroy(pW1);
1876 manifest_destroy(pW2);
1877 style_finish_page();
1878 }
1879
1880
--- src/wiki.c
+++ src/wiki.c
@@ -1867,13 +1867,11 @@
1867 style_header("Changes To %s", pW1->zWikiTitle);
1868 blob_zero(&d);
1869 construct_diff_flags(1, &DCfg);
1870 DCfg.diffFlags |= DIFF_HTML | DIFF_LINENO;
1871 text_diff(&w2, &w1, &d, &DCfg);
 
1872 @ %s(blob_str(&d))
 
1873 manifest_destroy(pW1);
1874 manifest_destroy(pW2);
1875 style_finish_page();
1876 }
1877
1878
--- www/changes.wiki
+++ www/changes.wiki
@@ -14,10 +14,13 @@
1414
* Rebuilt [/file/Dockerfile | the stock Dockerfile] to create a "from scratch"
1515
Busybox based container image via an Alpine Linux intermediary
1616
* Added [/doc/trunk/www/containers.md | a new document] describing how to
1717
customize, use, and run that container.
1818
* Added "by hour of day" report to [/reports?view=byhour|the /reports page].
19
+ * Improved correctness, usability, and efficiency for the case
20
+ [/timeline?r=deltify-tkt-blobs|when values in a TICKET's column
21
+ tend to be long and volatile].
1922
2023
<h2 id='v2_19'>Changes for version 2.19 (2022-07-21)</h2>
2124
* On file listing pages, sort filenames using the "uintnocase" collating
2225
sequence, so that filenames that contains embedded integers sort in
2326
numeric order even if they contain a different number of digits.
2427
--- www/changes.wiki
+++ www/changes.wiki
@@ -14,10 +14,13 @@
14 * Rebuilt [/file/Dockerfile | the stock Dockerfile] to create a "from scratch"
15 Busybox based container image via an Alpine Linux intermediary
16 * Added [/doc/trunk/www/containers.md | a new document] describing how to
17 customize, use, and run that container.
18 * Added "by hour of day" report to [/reports?view=byhour|the /reports page].
 
 
 
19
20 <h2 id='v2_19'>Changes for version 2.19 (2022-07-21)</h2>
21 * On file listing pages, sort filenames using the "uintnocase" collating
22 sequence, so that filenames that contains embedded integers sort in
23 numeric order even if they contain a different number of digits.
24
--- www/changes.wiki
+++ www/changes.wiki
@@ -14,10 +14,13 @@
14 * Rebuilt [/file/Dockerfile | the stock Dockerfile] to create a "from scratch"
15 Busybox based container image via an Alpine Linux intermediary
16 * Added [/doc/trunk/www/containers.md | a new document] describing how to
17 customize, use, and run that container.
18 * Added "by hour of day" report to [/reports?view=byhour|the /reports page].
19 * Improved correctness, usability, and efficiency for the case
20 [/timeline?r=deltify-tkt-blobs|when values in a TICKET's column
21 tend to be long and volatile].
22
23 <h2 id='v2_19'>Changes for version 2.19 (2022-07-21)</h2>
24 * On file listing pages, sort filenames using the "uintnocase" collating
25 sequence, so that filenames that contains embedded integers sort in
26 numeric order even if they contain a different number of digits.
27

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button