|
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
|
#include "VERSION.h" |
|
19
|
#include "config.h" |
|
20
|
#include "json_tag.h" |
|
21
|
|
|
22
|
#if INTERFACE |
|
23
|
#include "json_detail.h" |
|
24
|
#endif |
|
25
|
|
|
26
|
|
|
27
|
static cson_value * json_tag_add(void); |
|
28
|
static cson_value * json_tag_cancel(void); |
|
29
|
static cson_value * json_tag_find(void); |
|
30
|
static cson_value * json_tag_list(void); |
|
31
|
/* |
|
32
|
** Mapping of /json/tag/XXX commands/paths to callbacks. |
|
33
|
*/ |
|
34
|
static const JsonPageDef JsonPageDefs_Tag[] = { |
|
35
|
{"add", json_tag_add, 0}, |
|
36
|
{"cancel", json_tag_cancel, 0}, |
|
37
|
{"find", json_tag_find, 0}, |
|
38
|
{"list", json_tag_list, 0}, |
|
39
|
/* Last entry MUST have a NULL name. */ |
|
40
|
{NULL,NULL,0} |
|
41
|
}; |
|
42
|
|
|
43
|
/* |
|
44
|
** Implements the /json/tag family of pages/commands. |
|
45
|
** |
|
46
|
*/ |
|
47
|
cson_value * json_page_tag(void){ |
|
48
|
return json_page_dispatch_helper(&JsonPageDefs_Tag[0]); |
|
49
|
} |
|
50
|
|
|
51
|
|
|
52
|
/* |
|
53
|
** Impl of /json/tag/add. |
|
54
|
*/ |
|
55
|
static cson_value * json_tag_add(void){ |
|
56
|
cson_value * payV = NULL; |
|
57
|
cson_object * pay = NULL; |
|
58
|
char const * zName = NULL; |
|
59
|
char const * zCheckin = NULL; |
|
60
|
char fRaw = 0; |
|
61
|
char fPropagate = 0; |
|
62
|
char const * zValue = NULL; |
|
63
|
const char *zPrefix = NULL; |
|
64
|
|
|
65
|
if( !g.perm.Write ){ |
|
66
|
json_set_err(FSL_JSON_E_DENIED, |
|
67
|
"Requires 'i' permissions."); |
|
68
|
return NULL; |
|
69
|
} |
|
70
|
fRaw = json_find_option_bool("raw",NULL,NULL,0); |
|
71
|
fPropagate = json_find_option_bool("propagate",NULL,NULL,0); |
|
72
|
zName = json_find_option_cstr("name",NULL,NULL); |
|
73
|
zPrefix = fRaw ? "" : "sym-"; |
|
74
|
if(!zName || !*zName){ |
|
75
|
if(!fossil_has_json()){ |
|
76
|
zName = json_command_arg(3); |
|
77
|
} |
|
78
|
if(!zName || !*zName){ |
|
79
|
json_set_err(FSL_JSON_E_MISSING_ARGS, |
|
80
|
"'name' parameter is missing."); |
|
81
|
return NULL; |
|
82
|
} |
|
83
|
} |
|
84
|
|
|
85
|
zCheckin = json_find_option_cstr("checkin",NULL,NULL); |
|
86
|
if( !zCheckin ){ |
|
87
|
if(!fossil_has_json()){ |
|
88
|
zCheckin = json_command_arg(4); |
|
89
|
} |
|
90
|
if(!zCheckin || !*zCheckin){ |
|
91
|
json_set_err(FSL_JSON_E_MISSING_ARGS, |
|
92
|
"'checkin' parameter is missing."); |
|
93
|
return NULL; |
|
94
|
} |
|
95
|
} |
|
96
|
|
|
97
|
|
|
98
|
zValue = json_find_option_cstr("value",NULL,NULL); |
|
99
|
if(!zValue && !fossil_has_json()){ |
|
100
|
zValue = json_command_arg(5); |
|
101
|
} |
|
102
|
|
|
103
|
db_begin_transaction(); |
|
104
|
tag_add_artifact(zPrefix, zName, zCheckin, zValue, |
|
105
|
1+fPropagate,NULL/*DateOvrd*/,NULL/*UserOvrd*/); |
|
106
|
db_end_transaction(0); |
|
107
|
|
|
108
|
payV = cson_value_new_object(); |
|
109
|
pay = cson_value_get_object(payV); |
|
110
|
cson_object_set(pay, "name", json_new_string(zName) ); |
|
111
|
cson_object_set(pay, "value", (zValue&&*zValue) |
|
112
|
? json_new_string(zValue) |
|
113
|
: cson_value_null()); |
|
114
|
cson_object_set(pay, "propagate", cson_value_new_bool(fPropagate)); |
|
115
|
cson_object_set(pay, "raw", cson_value_new_bool(fRaw)); |
|
116
|
{ |
|
117
|
Blob uu = empty_blob; |
|
118
|
int rc; |
|
119
|
blob_append(&uu, zName, -1); |
|
120
|
rc = name_to_uuid(&uu, 9, "*"); |
|
121
|
if(0!=rc){ |
|
122
|
json_set_err(FSL_JSON_E_UNKNOWN, |
|
123
|
"Could not convert name back to artifact hash!"); |
|
124
|
blob_reset(&uu); |
|
125
|
goto error; |
|
126
|
} |
|
127
|
cson_object_set(pay, "appliedTo", json_new_string(blob_buffer(&uu))); |
|
128
|
blob_reset(&uu); |
|
129
|
} |
|
130
|
|
|
131
|
goto ok; |
|
132
|
error: |
|
133
|
assert( 0 != g.json.resultCode ); |
|
134
|
cson_value_free(payV); |
|
135
|
payV = NULL; |
|
136
|
ok: |
|
137
|
return payV; |
|
138
|
} |
|
139
|
|
|
140
|
|
|
141
|
/* |
|
142
|
** Impl of /json/tag/cancel. |
|
143
|
*/ |
|
144
|
static cson_value * json_tag_cancel(void){ |
|
145
|
char const * zName = NULL; |
|
146
|
char const * zCheckin = NULL; |
|
147
|
char fRaw = 0; |
|
148
|
const char *zPrefix = NULL; |
|
149
|
|
|
150
|
if( !g.perm.Write ){ |
|
151
|
json_set_err(FSL_JSON_E_DENIED, |
|
152
|
"Requires 'i' permissions."); |
|
153
|
return NULL; |
|
154
|
} |
|
155
|
|
|
156
|
fRaw = json_find_option_bool("raw",NULL,NULL,0); |
|
157
|
zPrefix = fRaw ? "" : "sym-"; |
|
158
|
zName = json_find_option_cstr("name",NULL,NULL); |
|
159
|
if(!zName || !*zName){ |
|
160
|
if(!fossil_has_json()){ |
|
161
|
zName = json_command_arg(3); |
|
162
|
} |
|
163
|
if(!zName || !*zName){ |
|
164
|
json_set_err(FSL_JSON_E_MISSING_ARGS, |
|
165
|
"'name' parameter is missing."); |
|
166
|
return NULL; |
|
167
|
} |
|
168
|
} |
|
169
|
|
|
170
|
zCheckin = json_find_option_cstr("checkin",NULL,NULL); |
|
171
|
if( !zCheckin ){ |
|
172
|
if(!fossil_has_json()){ |
|
173
|
zCheckin = json_command_arg(4); |
|
174
|
} |
|
175
|
if(!zCheckin || !*zCheckin){ |
|
176
|
json_set_err(FSL_JSON_E_MISSING_ARGS, |
|
177
|
"'checkin' parameter is missing."); |
|
178
|
return NULL; |
|
179
|
} |
|
180
|
} |
|
181
|
/* FIXME?: verify that the tag is currently active. We have no real |
|
182
|
error case unless we do that. |
|
183
|
*/ |
|
184
|
db_begin_transaction(); |
|
185
|
tag_add_artifact(zPrefix, zName, zCheckin, NULL, 0, 0, 0); |
|
186
|
db_end_transaction(0); |
|
187
|
return NULL; |
|
188
|
} |
|
189
|
|
|
190
|
|
|
191
|
/* |
|
192
|
** Impl of /json/tag/find. |
|
193
|
*/ |
|
194
|
static cson_value * json_tag_find(void){ |
|
195
|
cson_value * payV = NULL; |
|
196
|
cson_object * pay = NULL; |
|
197
|
cson_value * listV = NULL; |
|
198
|
cson_array * list = NULL; |
|
199
|
char const * zName = NULL; |
|
200
|
char const * zType = NULL; |
|
201
|
char const * zType2 = NULL; |
|
202
|
char fRaw = 0; |
|
203
|
Stmt q = empty_Stmt; |
|
204
|
int limit = 0; |
|
205
|
int tagid = 0; |
|
206
|
|
|
207
|
if( !g.perm.Read ){ |
|
208
|
json_set_err(FSL_JSON_E_DENIED, |
|
209
|
"Requires 'o' permissions."); |
|
210
|
return NULL; |
|
211
|
} |
|
212
|
zName = json_find_option_cstr("name",NULL,NULL); |
|
213
|
if(!zName || !*zName){ |
|
214
|
if(!fossil_has_json()){ |
|
215
|
zName = json_command_arg(3); |
|
216
|
} |
|
217
|
if(!zName || !*zName){ |
|
218
|
json_set_err(FSL_JSON_E_MISSING_ARGS, |
|
219
|
"'name' parameter is missing."); |
|
220
|
return NULL; |
|
221
|
} |
|
222
|
} |
|
223
|
zType = json_find_option_cstr("type",NULL,"t"); |
|
224
|
if(!zType || !*zType){ |
|
225
|
zType = "*"; |
|
226
|
zType2 = zType; |
|
227
|
}else{ |
|
228
|
switch(*zType){ |
|
229
|
case 'c': zType = "ci"; zType2 = "checkin"; break; |
|
230
|
case 'e': zType = "e"; zType2 = "event"; break; |
|
231
|
case 'w': zType = "w"; zType2 = "wiki"; break; |
|
232
|
case 't': zType = "t"; zType2 = "ticket"; break; |
|
233
|
} |
|
234
|
} |
|
235
|
|
|
236
|
limit = json_find_option_int("limit",NULL,"n",0); |
|
237
|
fRaw = json_find_option_bool("raw",NULL,NULL,0); |
|
238
|
|
|
239
|
tagid = db_int(0, "SELECT tagid FROM tag WHERE tagname='%s' || %Q", |
|
240
|
fRaw ? "" : "sym-", |
|
241
|
zName); |
|
242
|
|
|
243
|
payV = cson_value_new_object(); |
|
244
|
pay = cson_value_get_object(payV); |
|
245
|
cson_object_set(pay, "name", json_new_string(zName)); |
|
246
|
cson_object_set(pay, "raw", cson_value_new_bool(fRaw)); |
|
247
|
cson_object_set(pay, "type", json_new_string(zType2)); |
|
248
|
cson_object_set(pay, "limit", json_new_int(limit)); |
|
249
|
|
|
250
|
#if 1 |
|
251
|
if( tagid<=0 ){ |
|
252
|
cson_object_set(pay,"artifacts", cson_value_null()); |
|
253
|
json_warn(FSL_JSON_W_TAG_NOT_FOUND, "Tag not found."); |
|
254
|
return payV; |
|
255
|
} |
|
256
|
#endif |
|
257
|
|
|
258
|
if( fRaw ){ |
|
259
|
db_prepare(&q, |
|
260
|
"SELECT blob.uuid FROM tagxref, blob" |
|
261
|
" WHERE tagid=(SELECT tagid FROM tag WHERE tagname=%Q)" |
|
262
|
" AND tagxref.tagtype>0" |
|
263
|
" AND blob.rid=tagxref.rid" |
|
264
|
"%s LIMIT %d", |
|
265
|
zName, |
|
266
|
(limit>0)?"":"--", limit |
|
267
|
); |
|
268
|
while( db_step(&q)==SQLITE_ROW ){ |
|
269
|
if(!listV){ |
|
270
|
listV = cson_value_new_array(); |
|
271
|
list = cson_value_get_array(listV); |
|
272
|
} |
|
273
|
cson_array_append(list, cson_sqlite3_column_to_value(q.pStmt,0)); |
|
274
|
} |
|
275
|
db_finalize(&q); |
|
276
|
}else{ |
|
277
|
char const * zSqlBase = /*modified from timeline_query_for_tty()*/ |
|
278
|
" SELECT" |
|
279
|
#if 0 |
|
280
|
" blob.rid AS rid," |
|
281
|
#endif |
|
282
|
" uuid AS uuid," |
|
283
|
" cast(strftime('%s',event.mtime) as int) AS timestamp," |
|
284
|
" coalesce(ecomment,comment) AS comment," |
|
285
|
" coalesce(euser,user) AS user," |
|
286
|
" CASE event.type" |
|
287
|
" WHEN 'ci' THEN 'checkin'" |
|
288
|
" WHEN 'w' THEN 'wiki'" |
|
289
|
" WHEN 'e' THEN 'event'" |
|
290
|
" WHEN 't' THEN 'ticket'" |
|
291
|
" ELSE 'unknown'" |
|
292
|
" END" |
|
293
|
" AS eventType" |
|
294
|
" FROM event, blob" |
|
295
|
" WHERE blob.rid=event.objid" |
|
296
|
; |
|
297
|
/* FIXME: re-add tags. */ |
|
298
|
db_prepare(&q, |
|
299
|
"%s" |
|
300
|
" AND event.type GLOB '%q'" |
|
301
|
" AND blob.rid IN (" |
|
302
|
" SELECT rid FROM tagxref" |
|
303
|
" WHERE tagtype>0 AND tagid=%d" |
|
304
|
" )" |
|
305
|
" ORDER BY event.mtime DESC" |
|
306
|
"%s LIMIT %d", |
|
307
|
zSqlBase /*safe-for-%s*/, zType, tagid, |
|
308
|
(limit>0)?"":"--", limit |
|
309
|
); |
|
310
|
listV = json_stmt_to_array_of_obj(&q, NULL); |
|
311
|
db_finalize(&q); |
|
312
|
} |
|
313
|
|
|
314
|
if(!listV) { |
|
315
|
listV = cson_value_null(); |
|
316
|
} |
|
317
|
cson_object_set(pay, "artifacts", listV); |
|
318
|
return payV; |
|
319
|
} |
|
320
|
|
|
321
|
|
|
322
|
/* |
|
323
|
** Impl for /json/tag/list |
|
324
|
** |
|
325
|
** TODOs: |
|
326
|
** |
|
327
|
** Add -type TYPE (ci, w, e, t) |
|
328
|
*/ |
|
329
|
static cson_value * json_tag_list(void){ |
|
330
|
cson_value * payV = NULL; |
|
331
|
cson_object * pay = NULL; |
|
332
|
cson_value const * tagsVal = NULL; |
|
333
|
char const * zCheckin = NULL; |
|
334
|
char fRaw = 0; |
|
335
|
char fTicket = 0; |
|
336
|
Stmt q = empty_Stmt; |
|
337
|
|
|
338
|
if( !g.perm.Read ){ |
|
339
|
json_set_err(FSL_JSON_E_DENIED, |
|
340
|
"Requires 'o' permissions."); |
|
341
|
return NULL; |
|
342
|
} |
|
343
|
|
|
344
|
fRaw = json_find_option_bool("raw",NULL,NULL,0); |
|
345
|
fTicket = json_find_option_bool("includeTickets","tkt","t",0); |
|
346
|
zCheckin = json_find_option_cstr("checkin",NULL,NULL); |
|
347
|
if( !zCheckin ){ |
|
348
|
zCheckin = json_command_arg( g.json.dispatchDepth + 1); |
|
349
|
if( !zCheckin && cson_value_is_string(g.json.reqPayload.v) ){ |
|
350
|
zCheckin = cson_string_cstr(cson_value_get_string(g.json.reqPayload.v)); |
|
351
|
assert(zCheckin); |
|
352
|
} |
|
353
|
} |
|
354
|
payV = cson_value_new_object(); |
|
355
|
pay = cson_value_get_object(payV); |
|
356
|
cson_object_set(pay, "raw", cson_value_new_bool(fRaw) ); |
|
357
|
if( zCheckin ){ |
|
358
|
/** |
|
359
|
Tags for a specific check-in. Output format: |
|
360
|
|
|
361
|
RAW mode: |
|
362
|
|
|
363
|
{ |
|
364
|
"sym-tagname": (value || null), |
|
365
|
...other tags... |
|
366
|
} |
|
367
|
|
|
368
|
Non-raw: |
|
369
|
|
|
370
|
{ |
|
371
|
"tagname": (value || null), |
|
372
|
...other tags... |
|
373
|
} |
|
374
|
*/ |
|
375
|
cson_value * objV = NULL; |
|
376
|
cson_object * obj = NULL; |
|
377
|
int const rid = name_to_rid(zCheckin); |
|
378
|
if(0==rid){ |
|
379
|
json_set_err(FSL_JSON_E_UNRESOLVED_UUID, |
|
380
|
"Could not find artifact for check-in [%s].", |
|
381
|
zCheckin); |
|
382
|
goto error; |
|
383
|
} |
|
384
|
cson_object_set(pay, "checkin", json_new_string(zCheckin)); |
|
385
|
db_prepare(&q, |
|
386
|
"SELECT tagname, value FROM tagxref, tag" |
|
387
|
" WHERE tagxref.rid=%d AND tagxref.tagid=tag.tagid" |
|
388
|
" AND tagtype>%d" |
|
389
|
" ORDER BY tagname", |
|
390
|
rid, |
|
391
|
fRaw ? -1 : 0 |
|
392
|
); |
|
393
|
while( SQLITE_ROW == db_step(&q) ){ |
|
394
|
const char *zName = db_column_text(&q, 0); |
|
395
|
const char *zValue = db_column_text(&q, 1); |
|
396
|
if( fRaw==0 ){ |
|
397
|
if( 0!=strncmp(zName, "sym-", 4) ) continue; |
|
398
|
zName += 4; |
|
399
|
assert( *zName ); |
|
400
|
} |
|
401
|
if(NULL==objV){ |
|
402
|
objV = cson_value_new_object(); |
|
403
|
obj = cson_value_get_object(objV); |
|
404
|
tagsVal = objV; |
|
405
|
cson_object_set( pay, "tags", objV ); |
|
406
|
} |
|
407
|
if( zValue && zValue[0] ){ |
|
408
|
cson_object_set( obj, zName, json_new_string(zValue) ); |
|
409
|
}else{ |
|
410
|
cson_object_set( obj, zName, cson_value_null() ); |
|
411
|
} |
|
412
|
} |
|
413
|
db_finalize(&q); |
|
414
|
}else{/* all tags */ |
|
415
|
/* Output format: |
|
416
|
|
|
417
|
RAW mode: |
|
418
|
|
|
419
|
["tagname", "sym-tagname2",...] |
|
420
|
|
|
421
|
Non-raw: |
|
422
|
|
|
423
|
["tagname", "tagname2",...] |
|
424
|
|
|
425
|
i don't really like the discrepancy in the format but this list |
|
426
|
can get really long and (A) most tags don't have values, (B) i |
|
427
|
don't want to bloat it more, and (C) cson_object_set() is O(N) |
|
428
|
(N=current number of properties) because it uses an unsorted list |
|
429
|
internally (for memory reasons), so this can slow down appreciably |
|
430
|
on a long list. The culprit is really tkt- tags, as there is one |
|
431
|
for each ticket (941 in the main fossil repo as of this writing). |
|
432
|
*/ |
|
433
|
Blob sql = empty_blob; |
|
434
|
cson_value * arV = NULL; |
|
435
|
cson_array * ar = NULL; |
|
436
|
blob_append(&sql, |
|
437
|
"SELECT tagname FROM tag" |
|
438
|
" WHERE EXISTS(SELECT 1 FROM tagxref" |
|
439
|
" WHERE tagid=tag.tagid" |
|
440
|
" AND tagtype>0)", |
|
441
|
-1 |
|
442
|
); |
|
443
|
if(!fTicket){ |
|
444
|
blob_append(&sql, " AND tagname NOT GLOB('tkt-*') ", -1); |
|
445
|
} |
|
446
|
blob_append(&sql, |
|
447
|
" ORDER BY tagname", -1); |
|
448
|
db_prepare(&q, "%s", blob_sql_text(&sql)); |
|
449
|
blob_reset(&sql); |
|
450
|
cson_object_set(pay, "includeTickets", cson_value_new_bool(fTicket) ); |
|
451
|
while( SQLITE_ROW == db_step(&q) ){ |
|
452
|
const char *zName = db_column_text(&q, 0); |
|
453
|
if(NULL==arV){ |
|
454
|
arV = cson_value_new_array(); |
|
455
|
ar = cson_value_get_array(arV); |
|
456
|
cson_object_set(pay, "tags", arV); |
|
457
|
tagsVal = arV; |
|
458
|
} |
|
459
|
else if( !fRaw && (0==strncmp(zName, "sym-", 4))){ |
|
460
|
zName += 4; |
|
461
|
assert( *zName ); |
|
462
|
} |
|
463
|
cson_array_append(ar, json_new_string(zName)); |
|
464
|
} |
|
465
|
db_finalize(&q); |
|
466
|
} |
|
467
|
|
|
468
|
goto end; |
|
469
|
error: |
|
470
|
assert(0 != g.json.resultCode); |
|
471
|
cson_value_free(payV); |
|
472
|
payV = NULL; |
|
473
|
end: |
|
474
|
if( payV && !tagsVal ){ |
|
475
|
cson_object_set( pay, "tags", cson_value_null() ); |
|
476
|
} |
|
477
|
return payV; |
|
478
|
} |
|
479
|
#endif /* FOSSIL_ENABLE_JSON */ |
|
480
|
|