Fossil SCM

fossil-scm / src / json_timeline.c
Blame History Raw 770 lines
1
#ifdef FOSSIL_ENABLE_JSON
2
/*
3
** Copyright (c) 2011 D. Richard Hipp
4
**
5
** This program is free software; you can redistribute it and/or
6
** modify it under the terms of the Simplified BSD License (also
7
** known as the "2-Clause License" or "FreeBSD License".)
8
**
9
** This program is distributed in the hope that it will be useful,
10
** but without any warranty; without even the implied warranty of
11
** merchantability or fitness for a particular purpose.
12
**
13
** Author contact information:
14
** [email protected]
15
** http://www.hwaci.com/drh/
16
**
17
*/
18
19
#include "VERSION.h"
20
#include "config.h"
21
#include "json_timeline.h"
22
23
#if INTERFACE
24
#include "json_detail.h"
25
#endif
26
27
static cson_value * json_timeline_branch(void);
28
static cson_value * json_timeline_ci(void);
29
static cson_value * json_timeline_ticket(void);
30
/*
31
** Mapping of /json/timeline/XXX commands/paths to callbacks.
32
*/
33
static const JsonPageDef JsonPageDefs_Timeline[] = {
34
/* the short forms are only enabled in CLI mode, to avoid
35
that we end up with HTTP clients using 3 different names
36
for the same requests.
37
*/
38
{"branch", json_timeline_branch, 0},
39
{"checkin", json_timeline_ci, 0},
40
{"event" /* old name for technotes */, json_timeline_event, 0},
41
{"technote", json_timeline_event, 0},
42
{"ticket", json_timeline_ticket, 0},
43
{"wiki", json_timeline_wiki, 0},
44
/* Last entry MUST have a NULL name. */
45
{NULL,NULL,0}
46
};
47
48
49
/*
50
** Implements the /json/timeline family of pages/commands. Far from
51
** complete.
52
**
53
*/
54
cson_value * json_page_timeline(void){
55
#if 0
56
/* The original timeline code does not require 'h' access,
57
but it arguably should. For JSON mode i think one could argue
58
that History permissions are required.
59
*/
60
if(! g.perm.Hyperlink && !g.perm.Read ){
61
json_set_err(FSL_JSON_E_DENIED, "Timeline requires 'h' or 'o' access.");
62
return NULL;
63
}
64
#endif
65
return json_page_dispatch_helper(&JsonPageDefs_Timeline[0]);
66
}
67
68
/*
69
** Create a temporary table suitable for storing timeline data.
70
*/
71
static void json_timeline_temp_table(void){
72
/* Field order MUST match that from json_timeline_query()!!! */
73
static const char zSql[] =
74
@ CREATE TEMP TABLE IF NOT EXISTS json_timeline(
75
@ sortId INTEGER PRIMARY KEY,
76
@ rid INTEGER,
77
@ uuid TEXT,
78
@ mtime INTEGER,
79
@ timestampString TEXT,
80
@ comment TEXT,
81
@ user TEXT,
82
@ isLeaf BOOLEAN,
83
@ bgColor TEXT,
84
@ eventType TEXT,
85
@ tags TEXT,
86
@ tagId INTEGER,
87
@ brief TEXT
88
@ )
89
;
90
db_multi_exec("%s", zSql /*safe-for-%s*/);
91
}
92
93
/*
94
** Return a pointer to a constant string that forms the basis
95
** for a timeline query for the JSON interface. It MUST NOT
96
** be used in a formatted string argument.
97
*/
98
char const * json_timeline_query(void){
99
/* Field order MUST match that from json_timeline_temp_table()!!! */
100
static const char zBaseSql[] =
101
@ SELECT
102
@ NULL,
103
@ blob.rid,
104
@ uuid,
105
@ CAST(strftime('%s',event.mtime) AS INTEGER),
106
@ datetime(event.mtime),
107
@ coalesce(ecomment, comment),
108
@ coalesce(euser, user),
109
@ blob.rid IN leaf,
110
@ bgcolor,
111
@ event.type,
112
@ (SELECT group_concat(substr(tagname,5), ',') FROM tag, tagxref
113
@ WHERE tagname GLOB 'sym-*' AND tag.tagid=tagxref.tagid
114
@ AND tagxref.rid=blob.rid AND tagxref.tagtype>0) as tags,
115
@ tagid as tagId,
116
@ brief as brief
117
@ FROM event JOIN blob
118
@ WHERE blob.rid=event.objid
119
;
120
return zBaseSql;
121
}
122
123
/*
124
** Internal helper to append query information if the
125
** "tag" or "branch" request properties (CLI: --tag/--branch)
126
** are set. Limits the query to a particular branch/tag.
127
**
128
** tag works like HTML mode's "t" option and branch works like HTML
129
** mode's "r" option. They are very similar, but subtly different -
130
** tag mode shows only entries with a given tag but branch mode can
131
** also reveal some with "related" tags (meaning they were merged into
132
** the requested branch, or back).
133
**
134
** pSql is the target blob to append the query [subset]
135
** to.
136
**
137
** Returns a positive value if it modifies pSql, 0 if it
138
** does not. It returns a negative value if the tag
139
** provided to the request was not found (pSql is not modified
140
** in that case).
141
**
142
** If payload is not NULL then on success its "tag" or "branch"
143
** property is set to the tag/branch name found in the request.
144
**
145
** Only one of "tag" or "branch" modes will work at a time, and if
146
** both are specified, which one takes precedence is unspecified.
147
*/
148
static signed char json_timeline_add_tag_branch_clause(Blob *pSql,
149
cson_object * pPayload){
150
char const * zTag = NULL;
151
char const * zBranch = NULL;
152
char const * zMiOnly = NULL;
153
char const * zUnhide = NULL;
154
int tagid = 0;
155
if(! g.perm.Read ){
156
return 0;
157
}
158
zTag = json_find_option_cstr("tag",NULL,NULL);
159
if(!zTag || !*zTag){
160
zBranch = json_find_option_cstr("branch",NULL,NULL);
161
if(!zBranch || !*zBranch){
162
return 0;
163
}
164
zTag = zBranch;
165
zMiOnly = json_find_option_cstr("mionly",NULL,NULL);
166
}
167
zUnhide = json_find_option_cstr("unhide",NULL,NULL);
168
tagid = db_int(0, "SELECT tagid FROM tag WHERE tagname='sym-%q'",
169
zTag);
170
if(tagid<=0){
171
return -1;
172
}
173
if(pPayload){
174
cson_object_set( pPayload, zBranch ? "branch" : "tag",
175
json_new_string(zTag) );
176
}
177
blob_appendf(pSql,
178
" AND ("
179
" EXISTS(SELECT 1 FROM tagxref"
180
" WHERE tagid=%d AND tagtype>0 AND rid=blob.rid)",
181
tagid);
182
if(!zUnhide){
183
blob_appendf(pSql,
184
" AND NOT EXISTS(SELECT 1 FROM plink "
185
" JOIN tagxref ON rid=blob.rid"
186
" WHERE tagid=%d AND tagtype>0 AND rid=blob.rid)",
187
TAG_HIDDEN);
188
}
189
if(zBranch){
190
/* from "r" flag code in page_timeline().*/
191
blob_appendf(pSql,
192
" OR EXISTS(SELECT 1 FROM plink JOIN tagxref ON rid=cid"
193
" WHERE tagid=%d AND tagtype>0 AND pid=blob.rid)",
194
tagid);
195
if( !zUnhide ){
196
blob_appendf(pSql,
197
" AND NOT EXISTS(SELECT 1 FROM plink JOIN tagxref ON rid=cid"
198
" WHERE tagid=%d AND tagtype>0 AND pid=blob.rid)",
199
TAG_HIDDEN);
200
}
201
if( zMiOnly==0 ){
202
blob_appendf(pSql,
203
" OR EXISTS(SELECT 1 FROM plink JOIN tagxref ON rid=pid"
204
" WHERE tagid=%d AND tagtype>0 AND cid=blob.rid)",
205
tagid);
206
if( !zUnhide ){
207
blob_appendf(pSql,
208
" AND NOT EXISTS(SELECT 1 FROM plink JOIN tagxref ON rid=pid"
209
" WHERE tagid=%d AND tagtype>0 AND cid=blob.rid)",
210
TAG_HIDDEN);
211
}
212
}
213
}
214
blob_append(pSql," ) ",3);
215
return 1;
216
}
217
/*
218
** Helper for the timeline family of functions. Possibly appends 1
219
** AND clause and an ORDER BY clause to pSql, depending on the state
220
** of the "after" ("a") or "before" ("b") environment parameters.
221
** This function gives "after" precedence over "before", and only
222
** applies one of them.
223
**
224
** Returns -1 if it adds a "before" clause, 1 if it adds
225
** an "after" clause, and 0 if adds only an order-by clause.
226
*/
227
static signed char json_timeline_add_time_clause(Blob *pSql){
228
char const * zAfter = NULL;
229
char const * zBefore = NULL;
230
int rc = 0;
231
zAfter = json_find_option_cstr("after",NULL,"a");
232
zBefore = zAfter ? NULL : json_find_option_cstr("before",NULL,"b");
233
234
if(zAfter&&*zAfter){
235
while( fossil_isspace(*zAfter) ) ++zAfter;
236
blob_appendf(pSql,
237
" AND event.mtime>=(SELECT julianday(%Q,fromLocal())) "
238
" ORDER BY event.mtime ASC ",
239
zAfter);
240
rc = 1;
241
}else if(zBefore && *zBefore){
242
while( fossil_isspace(*zBefore) ) ++zBefore;
243
blob_appendf(pSql,
244
" AND event.mtime<=(SELECT julianday(%Q,fromLocal())) "
245
" ORDER BY event.mtime DESC ",
246
zBefore);
247
rc = -1;
248
}else{
249
blob_append(pSql, " ORDER BY event.mtime DESC ", -1);
250
rc = 0;
251
}
252
return rc;
253
}
254
255
/*
256
** Tries to figure out a timeline query length limit base on
257
** environment parameters. If it can it returns that value,
258
** else it returns some statically defined default value.
259
**
260
** Never returns a negative value. 0 means no limit.
261
*/
262
static int json_timeline_limit(int defaultLimit){
263
int limit = -1;
264
if(!g.isHTTP){/* CLI mode */
265
char const * arg = find_option("limit","n",1);
266
if(arg && *arg){
267
limit = atoi(arg);
268
}
269
}
270
if( (limit<0) && fossil_has_json() ){
271
limit = json_getenv_int("limit",-1);
272
}
273
return (limit<0) ? defaultLimit : limit;
274
}
275
276
/*
277
** Internal helper for the json_timeline_EVENTTYPE() family of
278
** functions. zEventType must be one of (ci, w, t). pSql must be a
279
** cleanly-initialized, empty Blob to store the sql in. If pPayload is
280
** not NULL it is assumed to be the pending response payload. If
281
** json_timeline_limit() returns non-0, this function adds a LIMIT
282
** clause to the generated SQL.
283
**
284
** If pPayload is not NULL then this might add properties to pPayload,
285
** reflecting options set in the request environment.
286
**
287
** Returns 0 on success. On error processing should not continue and
288
** the returned value should be used as g.json.resultCode.
289
*/
290
static int json_timeline_setup_sql( char const * zEventType,
291
Blob * pSql,
292
cson_object * pPayload ){
293
int limit;
294
assert( zEventType && *zEventType && pSql );
295
json_timeline_temp_table();
296
blob_append(pSql, "INSERT OR IGNORE INTO json_timeline ", -1);
297
blob_append(pSql, json_timeline_query(), -1 );
298
blob_appendf(pSql, " AND event.type IN(%Q) ", zEventType);
299
if( json_timeline_add_tag_branch_clause(pSql, pPayload) < 0 ){
300
return FSL_JSON_E_INVALID_ARGS;
301
}
302
json_timeline_add_time_clause(pSql);
303
limit = json_timeline_limit(20);
304
if(limit>0){
305
blob_appendf(pSql,"LIMIT %d ",limit);
306
}
307
if(pPayload){
308
cson_object_set(pPayload, "limit", json_new_int(limit));
309
}
310
return 0;
311
}
312
313
314
/*
315
** If any files are associated with the given rid, a JSON array
316
** containing information about them is returned (and is owned by the
317
** caller). If no files are associated with it then NULL is returned.
318
**
319
** flags may optionally be a bitmask of json_get_changed_files flags,
320
** or 0 for defaults.
321
*/
322
cson_value * json_get_changed_files(int rid, int flags){
323
cson_value * rowsV = NULL;
324
cson_array * rows = NULL;
325
Stmt q = empty_Stmt;
326
db_prepare(&q,
327
"SELECT (pid<=0) AS isnew,"
328
" (fid==0) AS isdel,"
329
" (SELECT name FROM filename WHERE fnid=mlink.fnid) AS name,"
330
" (SELECT uuid FROM blob WHERE rid=fid) as uuid,"
331
" (SELECT uuid FROM blob WHERE rid=pid) as parent,"
332
" blob.size as size"
333
" FROM mlink"
334
" LEFT JOIN blob ON blob.rid=fid"
335
" WHERE mid=%d AND pid!=fid"
336
" AND NOT mlink.isaux"
337
" ORDER BY name /*sort*/",
338
rid
339
);
340
while( (SQLITE_ROW == db_step(&q)) ){
341
cson_value * rowV = cson_value_new_object();
342
cson_object * row = cson_value_get_object(rowV);
343
int const isNew = db_column_int(&q,0);
344
int const isDel = db_column_int(&q,1);
345
char * zDownload = NULL;
346
if(!rowsV){
347
rowsV = cson_value_new_array();
348
rows = cson_value_get_array(rowsV);
349
}
350
cson_array_append( rows, rowV );
351
cson_object_set(row, "name", json_new_string(db_column_text(&q,2)));
352
cson_object_set(row, "uuid", json_new_string(db_column_text(&q,3)));
353
if(!isNew && (flags & json_get_changed_files_ELIDE_PARENT)){
354
cson_object_set(row, "parent", json_new_string(db_column_text(&q,4)));
355
}
356
cson_object_set(row, "size", json_new_int(db_column_int(&q,5)));
357
358
cson_object_set(row, "state",
359
json_new_string(json_artifact_status_to_string(isNew,isDel)));
360
zDownload = mprintf("/raw/%s?name=%s",
361
/* reminder: g.zBaseURL is of course not set
362
for CLI mode. */
363
db_column_text(&q,2),
364
db_column_text(&q,3));
365
cson_object_set(row, "downloadPath", json_new_string(zDownload));
366
free(zDownload);
367
}
368
db_finalize(&q);
369
return rowsV;
370
}
371
372
static cson_value * json_timeline_branch(void){
373
cson_value * pay = NULL;
374
Blob sql = empty_blob;
375
Stmt q = empty_Stmt;
376
int limit = 0;
377
if(!g.perm.Read){
378
json_set_err(FSL_JSON_E_DENIED,
379
"Requires 'o' permissions.");
380
return NULL;
381
}
382
json_timeline_temp_table();
383
blob_append(&sql,
384
"SELECT"
385
" blob.rid AS rid,"
386
" uuid AS uuid,"
387
" CAST(strftime('%s',event.mtime) AS INTEGER) as timestamp,"
388
" coalesce(ecomment, comment) as comment,"
389
" coalesce(euser, user) as user,"
390
" blob.rid IN leaf as isLeaf,"
391
" bgcolor as bgColor"
392
" FROM event JOIN blob"
393
" WHERE blob.rid=event.objid",
394
-1);
395
396
blob_append_sql(&sql,
397
" AND event.type='ci'"
398
" AND blob.rid IN (SELECT rid FROM tagxref"
399
" WHERE tagtype>0 AND tagid=%d AND srcid!=0)"
400
" ORDER BY event.mtime DESC",
401
TAG_BRANCH);
402
limit = json_timeline_limit(20);
403
if(limit>0){
404
blob_append_sql(&sql," LIMIT %d ",limit);
405
}
406
db_prepare(&q,"%s", blob_sql_text(&sql));
407
blob_reset(&sql);
408
pay = json_stmt_to_array_of_obj(&q, NULL);
409
db_finalize(&q);
410
assert(NULL != pay);
411
if(pay){
412
/* get the array-form tags of each record. */
413
cson_string * tags = cson_new_string("tags",4);
414
cson_string * isLeaf = cson_new_string("isLeaf",6);
415
cson_array * ar = cson_value_get_array(pay);
416
cson_object * outer = NULL;
417
unsigned int i = 0;
418
unsigned int len = cson_array_length_get(ar);
419
cson_value_add_reference( cson_string_value(tags) );
420
cson_value_add_reference( cson_string_value(isLeaf) );
421
for( ; i < len; ++i ){
422
cson_object * row = cson_value_get_object(cson_array_get(ar,i));
423
int rid = cson_value_get_integer(cson_object_get(row,"rid"));
424
assert( rid > 0 );
425
cson_object_set_s(row, tags, json_tags_for_checkin_rid(rid,0));
426
cson_object_set_s(row, isLeaf,
427
json_value_to_bool(cson_object_get(row,"isLeaf")));
428
cson_object_set(row, "rid", NULL)
429
/* remove rid - we don't really want it to be public */;
430
}
431
cson_value_free( cson_string_value(tags) );
432
cson_value_free( cson_string_value(isLeaf) );
433
434
/* now we wrap the payload in an outer shell, for consistency with
435
other /json/timeline/xyz APIs...
436
*/
437
outer = cson_new_object();
438
if(limit>0){
439
cson_object_set( outer, "limit", json_new_int(limit) );
440
}
441
cson_object_set( outer, "timeline", pay );
442
pay = cson_object_value(outer);
443
}
444
return pay;
445
}
446
447
/*
448
** Implementation of /json/timeline/ci.
449
**
450
** Still a few TODOs (like figuring out how to structure
451
** inheritance info).
452
*/
453
static cson_value * json_timeline_ci(void){
454
cson_value * payV = NULL;
455
cson_object * pay = NULL;
456
cson_value * tmp = NULL;
457
cson_value * listV = NULL;
458
cson_array * list = NULL;
459
int check = 0;
460
char verboseFlag;
461
Stmt q = empty_Stmt;
462
char warnRowToJsonFailed = 0;
463
Blob sql = empty_blob;
464
if( !g.perm.Hyperlink ){
465
/* Reminder to self: HTML impl requires 'o' (Read)
466
rights.
467
*/
468
json_set_err( FSL_JSON_E_DENIED, "Check-in timeline requires 'h' access." );
469
return NULL;
470
}
471
verboseFlag = json_find_option_bool("verbose",NULL,"v",0);
472
if( !verboseFlag ){
473
verboseFlag = json_find_option_bool("files",NULL,"f",0);
474
}
475
payV = cson_value_new_object();
476
pay = cson_value_get_object(payV);
477
check = json_timeline_setup_sql( "ci", &sql, pay );
478
if(check){
479
json_set_err(check, "Query initialization failed.");
480
goto error;
481
}
482
#define SET(K) if(0!=(check=cson_object_set(pay,K,tmp))){ \
483
json_set_err((cson_rc.AllocError==check) \
484
? FSL_JSON_E_ALLOC : FSL_JSON_E_UNKNOWN,\
485
"Object property insertion failed"); \
486
goto error;\
487
} (void)0
488
489
#if 0
490
/* only for testing! */
491
tmp = cson_value_new_string(blob_buffer(&sql),strlen(blob_buffer(&sql)));
492
SET("timelineSql");
493
#endif
494
db_multi_exec("%s", blob_buffer(&sql)/*safe-for-%s*/);
495
blob_reset(&sql);
496
db_prepare(&q, "SELECT "
497
" rid AS rid"
498
" FROM json_timeline"
499
" ORDER BY rowid");
500
listV = cson_value_new_array();
501
list = cson_value_get_array(listV);
502
tmp = listV;
503
SET("timeline");
504
while( (SQLITE_ROW == db_step(&q) )){
505
/* convert each row into a JSON object...*/
506
int const rid = db_column_int(&q,0);
507
cson_value * rowV = json_artifact_for_ci(rid, verboseFlag);
508
cson_object * row = cson_value_get_object(rowV);
509
if(!row){
510
if( !warnRowToJsonFailed ){
511
warnRowToJsonFailed = 1;
512
json_warn( FSL_JSON_W_ROW_TO_JSON_FAILED,
513
"Could not convert at least one timeline result row to JSON." );
514
}
515
continue;
516
}
517
cson_array_append(list, rowV);
518
}
519
#undef SET
520
goto ok;
521
error:
522
assert( 0 != g.json.resultCode );
523
cson_value_free(payV);
524
payV = NULL;
525
ok:
526
db_finalize(&q);
527
return payV;
528
}
529
530
/*
531
** Implementation of /json/timeline/event.
532
**
533
*/
534
cson_value * json_timeline_event(void){
535
/* This code is 95% the same as json_timeline_ci(), by the way. */
536
cson_value * payV = NULL;
537
cson_object * pay = NULL;
538
cson_array * list = NULL;
539
int check = 0;
540
Stmt q = empty_Stmt;
541
Blob sql = empty_blob;
542
if( !g.perm.RdWiki ){
543
json_set_err( FSL_JSON_E_DENIED, "Event timeline requires 'j' access.");
544
return NULL;
545
}
546
payV = cson_value_new_object();
547
pay = cson_value_get_object(payV);
548
check = json_timeline_setup_sql( "e", &sql, pay );
549
if(check){
550
json_set_err(check, "Query initialization failed.");
551
goto error;
552
}
553
554
#if 0
555
/* only for testing! */
556
cson_object_set(pay, "timelineSql", cson_value_new_string(blob_buffer(&sql),
557
strlen(blob_buffer(&sql))));
558
#endif
559
db_multi_exec("%s", blob_buffer(&sql) /*safe-for-%s*/);
560
blob_reset(&sql);
561
db_prepare(&q, "SELECT"
562
/* For events, the name is generally more useful than
563
the uuid, but the uuid is unambiguous and can be used
564
with commands like 'artifact'. */
565
" substr((SELECT tagname FROM tag AS tn "
566
" WHERE tn.tagid=json_timeline.tagId "
567
" AND tagname LIKE 'event-%%'),7) AS name,"
568
" uuid as uuid,"
569
" mtime AS timestamp,"
570
" comment AS comment, "
571
" user AS user,"
572
" eventType AS eventType"
573
" FROM json_timeline"
574
" ORDER BY rowid");
575
list = cson_new_array();
576
json_stmt_to_array_of_obj(&q, list);
577
cson_object_set(pay, "timeline", cson_array_value(list));
578
goto ok;
579
error:
580
assert( 0 != g.json.resultCode );
581
cson_value_free(payV);
582
payV = NULL;
583
ok:
584
db_finalize(&q);
585
blob_reset(&sql);
586
return payV;
587
}
588
589
/*
590
** Implementation of /json/timeline/wiki.
591
**
592
*/
593
cson_value * json_timeline_wiki(void){
594
/* This code is 95% the same as json_timeline_ci(), by the way. */
595
cson_value * payV = NULL;
596
cson_object * pay = NULL;
597
cson_array * list = NULL;
598
int check = 0;
599
Stmt q = empty_Stmt;
600
Blob sql = empty_blob;
601
if( !g.perm.RdWiki && !g.perm.Read ){
602
json_set_err( FSL_JSON_E_DENIED,
603
"Wiki timeline requires 'o' or 'j' access.");
604
return NULL;
605
}
606
payV = cson_value_new_object();
607
pay = cson_value_get_object(payV);
608
check = json_timeline_setup_sql( "w", &sql, pay );
609
if(check){
610
json_set_err(check, "Query initialization failed.");
611
goto error;
612
}
613
614
#if 0
615
/* only for testing! */
616
cson_object_set(pay, "timelineSql", cson_value_new_string(blob_buffer(&sql),
617
strlen(blob_buffer(&sql))));
618
#endif
619
db_multi_exec("%s", blob_buffer(&sql) /*safe-for-%s*/);
620
blob_reset(&sql);
621
db_prepare(&q, "SELECT"
622
" uuid AS uuid,"
623
" mtime AS timestamp,"
624
#if 0
625
" timestampString AS timestampString,"
626
#endif
627
" comment AS comment, "
628
" user AS user,"
629
" eventType AS eventType"
630
#if 0
631
/* can wiki pages have tags? */
632
" tags AS tags," /*FIXME: split this into
633
a JSON array*/
634
" tagId AS tagId,"
635
#endif
636
" FROM json_timeline"
637
" ORDER BY rowid");
638
list = cson_new_array();
639
json_stmt_to_array_of_obj(&q, list);
640
cson_object_set(pay, "timeline", cson_array_value(list));
641
goto ok;
642
error:
643
assert( 0 != g.json.resultCode );
644
cson_value_free(payV);
645
payV = NULL;
646
ok:
647
db_finalize(&q);
648
blob_reset(&sql);
649
return payV;
650
}
651
652
/*
653
** Implementation of /json/timeline/ticket.
654
**
655
*/
656
static cson_value * json_timeline_ticket(void){
657
/* This code is 95% the same as json_timeline_ci(), by the way. */
658
cson_value * payV = NULL;
659
cson_object * pay = NULL;
660
cson_value * tmp = NULL;
661
cson_value * listV = NULL;
662
cson_array * list = NULL;
663
int check = 0;
664
Stmt q = empty_Stmt;
665
Blob sql = empty_blob;
666
if( !g.perm.RdTkt && !g.perm.Read ){
667
json_set_err(FSL_JSON_E_DENIED,
668
"Ticket timeline requires 'o' or 'r' access.");
669
return NULL;
670
}
671
payV = cson_value_new_object();
672
pay = cson_value_get_object(payV);
673
check = json_timeline_setup_sql( "t", &sql, pay );
674
if(check){
675
json_set_err(check, "Query initialization failed.");
676
goto error;
677
}
678
679
db_multi_exec("%s", blob_buffer(&sql) /*safe-for-%s*/);
680
#define SET(K) if(0!=(check=cson_object_set(pay,K,tmp))){ \
681
json_set_err((cson_rc.AllocError==check) \
682
? FSL_JSON_E_ALLOC : FSL_JSON_E_UNKNOWN, \
683
"Object property insertion failed."); \
684
goto error;\
685
} (void)0
686
687
#if 0
688
/* only for testing! */
689
tmp = cson_value_new_string(blob_buffer(&sql),strlen(blob_buffer(&sql)));
690
SET("timelineSql");
691
#endif
692
693
blob_reset(&sql);
694
/*
695
REMINDER/FIXME(?): we have both uuid (the change uuid?) and
696
ticketUuid (the actual ticket). This is different from the wiki
697
timeline, where we only have the wiki page uuid.
698
*/
699
db_prepare(&q, "SELECT rid AS rid,"
700
" uuid AS uuid,"
701
" mtime AS timestamp,"
702
#if 0
703
" timestampString AS timestampString,"
704
#endif
705
" user AS user,"
706
" eventType AS eventType,"
707
" comment AS comment,"
708
" brief AS briefComment"
709
" FROM json_timeline"
710
" ORDER BY rowid");
711
listV = cson_value_new_array();
712
list = cson_value_get_array(listV);
713
tmp = listV;
714
SET("timeline");
715
while( (SQLITE_ROW == db_step(&q) )){
716
/* convert each row into a JSON object...*/
717
int rc;
718
int const rid = db_column_int(&q,0);
719
Manifest * pMan = NULL;
720
cson_value * rowV;
721
cson_object * row;
722
/*printf("rid=%d\n",rid);*/
723
pMan = manifest_get(rid, CFTYPE_TICKET, 0);
724
if(!pMan){
725
/* this might be an attachment? I'm seeing this with
726
rid 15380, uuid [1292fef05f2472108].
727
728
/json/artifact/1292fef05f2472108 returns not-found,
729
probably because we haven't added artifact/ticket
730
yet(?).
731
*/
732
continue;
733
}
734
735
rowV = cson_sqlite3_row_to_object(q.pStmt);
736
row = cson_value_get_object(rowV);
737
if(!row){
738
manifest_destroy(pMan);
739
json_warn( FSL_JSON_W_ROW_TO_JSON_FAILED,
740
"Could not convert at least one timeline result row to JSON." );
741
continue;
742
}
743
/* FIXME: certainly there's a more efficient way for use to get
744
the ticket UUIDs?
745
*/
746
cson_object_set(row,"ticketUuid",json_new_string(pMan->zTicketUuid));
747
manifest_destroy(pMan);
748
rc = cson_array_append( list, rowV );
749
if( 0 != rc ){
750
cson_value_free(rowV);
751
g.json.resultCode = (cson_rc.AllocError==rc)
752
? FSL_JSON_E_ALLOC
753
: FSL_JSON_E_UNKNOWN;
754
goto error;
755
}
756
}
757
#undef SET
758
goto ok;
759
error:
760
assert( 0 != g.json.resultCode );
761
cson_value_free(payV);
762
payV = NULL;
763
ok:
764
blob_reset(&sql);
765
db_finalize(&q);
766
return payV;
767
}
768
769
#endif /* FOSSIL_ENABLE_JSON */
770

Keyboard Shortcuts

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