|
1
|
/* |
|
2
|
** Copyright (c) 2007 D. Richard Hipp |
|
3
|
** |
|
4
|
** This program is free software; you can redistribute it and/or |
|
5
|
** modify it under the terms of the Simplified BSD License (also |
|
6
|
** known as the "2-Clause License" or "FreeBSD License".) |
|
7
|
|
|
8
|
** This program is distributed in the hope that it will be useful, |
|
9
|
** but without any warranty; without even the implied warranty of |
|
10
|
** merchantability or fitness for a particular purpose. |
|
11
|
** |
|
12
|
** Author contact information: |
|
13
|
** [email protected] |
|
14
|
** http://www.hwaci.com/drh/ |
|
15
|
** |
|
16
|
******************************************************************************* |
|
17
|
** |
|
18
|
** This file contains code used to manage tags |
|
19
|
*/ |
|
20
|
#include "config.h" |
|
21
|
#include "tag.h" |
|
22
|
#include <assert.h> |
|
23
|
|
|
24
|
/* |
|
25
|
** Propagate the tag given by tagid to the children of pid. |
|
26
|
** |
|
27
|
** This routine assumes that tagid is a tag that should be |
|
28
|
** propagated and that the tag is already present in pid. |
|
29
|
** |
|
30
|
** If tagtype is 2 then the tag is being propagated from an |
|
31
|
** ancestor node. If tagtype is 0 it means a propagating tag is |
|
32
|
** being blocked. |
|
33
|
*/ |
|
34
|
static void tag_propagate( |
|
35
|
int pid, /* Propagate the tag to children of this node */ |
|
36
|
int tagid, /* Tag to propagate */ |
|
37
|
int tagType, /* 2 for a propagating tag. 0 for an antitag */ |
|
38
|
int origId, /* Artifact of tag, when tagType==2 */ |
|
39
|
const char *zValue, /* Value of the tag. Might be NULL */ |
|
40
|
double mtime /* Timestamp on the tag */ |
|
41
|
){ |
|
42
|
PQueue queue; /* Queue of check-ins to be tagged */ |
|
43
|
Stmt s; /* Query the children of :pid to which to propagate */ |
|
44
|
Stmt ins; /* INSERT INTO tagxref */ |
|
45
|
Stmt eventupdate; /* UPDATE event */ |
|
46
|
|
|
47
|
assert( tagType==0 || tagType==2 ); |
|
48
|
pqueuex_init(&queue); |
|
49
|
pqueuex_insert(&queue, pid, 0.0); |
|
50
|
|
|
51
|
/* Query for children of :pid to which to propagate the tag. |
|
52
|
** Three returns: (1) rid of the child. (2) timestamp of child. |
|
53
|
** (3) True to propagate or false to block. |
|
54
|
*/ |
|
55
|
db_prepare(&s, |
|
56
|
"SELECT cid, plink.mtime," |
|
57
|
" coalesce(srcid=0 AND tagxref.mtime<:mtime, %d) AS doit" |
|
58
|
" FROM plink LEFT JOIN tagxref ON cid=rid AND tagid=%d" |
|
59
|
" WHERE pid=:pid AND isprim", |
|
60
|
tagType==2, tagid |
|
61
|
); |
|
62
|
db_bind_double(&s, ":mtime", mtime); |
|
63
|
|
|
64
|
if( tagType==2 ){ |
|
65
|
/* Set the propagated tag marker on check-in :rid */ |
|
66
|
db_prepare(&ins, |
|
67
|
"REPLACE INTO tagxref(tagid, tagtype, srcid, origid, value, mtime, rid)" |
|
68
|
"VALUES(%d,2,0,%d,%Q,:mtime,:rid)", |
|
69
|
tagid, origId, zValue |
|
70
|
); |
|
71
|
db_bind_double(&ins, ":mtime", mtime); |
|
72
|
}else{ |
|
73
|
/* Remove all references to the tag from check-in :rid */ |
|
74
|
zValue = 0; |
|
75
|
db_prepare(&ins, |
|
76
|
"DELETE FROM tagxref WHERE tagid=%d AND rid=:rid", tagid |
|
77
|
); |
|
78
|
} |
|
79
|
if( tagid==TAG_BGCOLOR ){ |
|
80
|
db_prepare(&eventupdate, |
|
81
|
"UPDATE event SET bgcolor=%Q WHERE objid=:rid", zValue |
|
82
|
); |
|
83
|
} |
|
84
|
while( (pid = pqueuex_extract(&queue))!=0 ){ |
|
85
|
db_bind_int(&s, ":pid", pid); |
|
86
|
while( db_step(&s)==SQLITE_ROW ){ |
|
87
|
int doit = db_column_int(&s, 2); |
|
88
|
if( doit ){ |
|
89
|
int cid = db_column_int(&s, 0); |
|
90
|
double mtime = db_column_double(&s, 1); |
|
91
|
pqueuex_insert(&queue, cid, mtime); |
|
92
|
db_bind_int(&ins, ":rid", cid); |
|
93
|
db_step(&ins); |
|
94
|
db_reset(&ins); |
|
95
|
if( tagid==TAG_BGCOLOR ){ |
|
96
|
db_bind_int(&eventupdate, ":rid", cid); |
|
97
|
db_step(&eventupdate); |
|
98
|
db_reset(&eventupdate); |
|
99
|
} |
|
100
|
if( tagid==TAG_BRANCH ){ |
|
101
|
leaf_eventually_check(cid); |
|
102
|
} |
|
103
|
} |
|
104
|
} |
|
105
|
db_reset(&s); |
|
106
|
} |
|
107
|
pqueuex_clear(&queue); |
|
108
|
db_finalize(&ins); |
|
109
|
db_finalize(&s); |
|
110
|
if( tagid==TAG_BGCOLOR ){ |
|
111
|
db_finalize(&eventupdate); |
|
112
|
} |
|
113
|
} |
|
114
|
|
|
115
|
/* |
|
116
|
** Propagate all propagatable tags in pid to the children of pid. |
|
117
|
*/ |
|
118
|
void tag_propagate_all(int pid){ |
|
119
|
Stmt q; |
|
120
|
db_prepare(&q, |
|
121
|
"SELECT tagid, tagtype, mtime, value, origid FROM tagxref" |
|
122
|
" WHERE rid=%d", |
|
123
|
pid |
|
124
|
); |
|
125
|
while( db_step(&q)==SQLITE_ROW ){ |
|
126
|
int tagid = db_column_int(&q, 0); |
|
127
|
int tagtype = db_column_int(&q, 1); |
|
128
|
double mtime = db_column_double(&q, 2); |
|
129
|
const char *zValue = db_column_text(&q, 3); |
|
130
|
int origid = db_column_int(&q, 4); |
|
131
|
if( tagtype==1 ) tagtype = 0; |
|
132
|
tag_propagate(pid, tagid, tagtype, origid, zValue, mtime); |
|
133
|
} |
|
134
|
db_finalize(&q); |
|
135
|
} |
|
136
|
|
|
137
|
/* |
|
138
|
** Get a tagid for the given TAG. Create a new tag if necessary |
|
139
|
** if createFlag is 1. |
|
140
|
*/ |
|
141
|
int tag_findid(const char *zTag, int createFlag){ |
|
142
|
int id; |
|
143
|
id = db_int(0, "SELECT tagid FROM tag WHERE tagname=%Q", zTag); |
|
144
|
if( id==0 && createFlag ){ |
|
145
|
db_multi_exec("INSERT INTO tag(tagname) VALUES(%Q)", zTag); |
|
146
|
id = db_last_insert_rowid(); |
|
147
|
} |
|
148
|
return id; |
|
149
|
} |
|
150
|
|
|
151
|
/* |
|
152
|
** Insert a tag into the database. |
|
153
|
** |
|
154
|
** Also translate zTag into a tagid and return the tagid. (In other words |
|
155
|
** if zTag is "bgcolor" then return TAG_BGCOLOR.) |
|
156
|
*/ |
|
157
|
int tag_insert( |
|
158
|
const char *zTag, /* Name of the tag (w/o the "+" or "-" prefix */ |
|
159
|
int tagtype, /* 0:cancel 1:singleton 2:propagated */ |
|
160
|
const char *zValue, /* Value if the tag is really a property */ |
|
161
|
int srcId, /* Artifact that contains this tag */ |
|
162
|
double mtime, /* Timestamp. Use default if <=0.0 */ |
|
163
|
int rid /* Artifact to which the tag is to attached */ |
|
164
|
){ |
|
165
|
Stmt s; |
|
166
|
const char *zCol; |
|
167
|
int tagid = tag_findid(zTag, 1); |
|
168
|
int rc; |
|
169
|
|
|
170
|
if( mtime<=0.0 ){ |
|
171
|
mtime = db_double(0.0, "SELECT julianday('now')"); |
|
172
|
} |
|
173
|
db_prepare(&s, |
|
174
|
"SELECT 1 FROM tagxref" |
|
175
|
" WHERE tagid=%d" |
|
176
|
" AND rid=%d" |
|
177
|
" AND mtime>=:mtime", |
|
178
|
tagid, rid |
|
179
|
); |
|
180
|
db_bind_double(&s, ":mtime", mtime); |
|
181
|
rc = db_step(&s); |
|
182
|
db_finalize(&s); |
|
183
|
if( rc==SQLITE_ROW ){ |
|
184
|
/* Another entry that is more recent already exists. Do nothing */ |
|
185
|
return tagid; |
|
186
|
} |
|
187
|
db_prepare(&s, |
|
188
|
"REPLACE INTO tagxref(tagid,tagtype,srcId,origid,value,mtime,rid)" |
|
189
|
" VALUES(%d,%d,%d,%d,%Q,:mtime,%d)", |
|
190
|
tagid, tagtype, srcId, rid, zValue, rid |
|
191
|
); |
|
192
|
db_bind_double(&s, ":mtime", mtime); |
|
193
|
db_step(&s); |
|
194
|
db_finalize(&s); |
|
195
|
if( tagid==TAG_BRANCH ) leaf_eventually_check(rid); |
|
196
|
if( tagtype==0 ){ |
|
197
|
zValue = 0; |
|
198
|
} |
|
199
|
zCol = 0; |
|
200
|
switch( tagid ){ |
|
201
|
case TAG_BGCOLOR: { |
|
202
|
zCol = "bgcolor"; |
|
203
|
break; |
|
204
|
} |
|
205
|
case TAG_COMMENT: { |
|
206
|
zCol = "ecomment"; |
|
207
|
break; |
|
208
|
} |
|
209
|
case TAG_USER: { |
|
210
|
zCol = "euser"; |
|
211
|
break; |
|
212
|
} |
|
213
|
case TAG_PRIVATE: { |
|
214
|
db_multi_exec( |
|
215
|
"INSERT OR IGNORE INTO private(rid) VALUES(%d);", |
|
216
|
rid |
|
217
|
); |
|
218
|
} |
|
219
|
} |
|
220
|
if( zCol ){ |
|
221
|
db_multi_exec("UPDATE event SET \"%w\"=%Q WHERE objid=%d", |
|
222
|
zCol, zValue, rid); |
|
223
|
if( tagid==TAG_COMMENT && zValue!=0 ){ |
|
224
|
char *zCopy = fossil_strdup(zValue); |
|
225
|
backlink_extract(zCopy, MT_NONE, rid, BKLNK_COMMENT, mtime, 1); |
|
226
|
free(zCopy); |
|
227
|
} |
|
228
|
} |
|
229
|
if( tagid==TAG_DATE ){ |
|
230
|
db_multi_exec("UPDATE event " |
|
231
|
" SET mtime=julianday(%Q)," |
|
232
|
" omtime=coalesce(omtime,mtime)" |
|
233
|
" WHERE objid=%d", |
|
234
|
zValue, rid); |
|
235
|
} |
|
236
|
if( tagid==TAG_PARENT && tagtype==1 ){ |
|
237
|
manifest_reparent_checkin(rid, zValue); |
|
238
|
} |
|
239
|
if( tagtype==1 ) tagtype = 0; |
|
240
|
tag_propagate(rid, tagid, tagtype, rid, zValue, mtime); |
|
241
|
return tagid; |
|
242
|
} |
|
243
|
|
|
244
|
|
|
245
|
/* |
|
246
|
** COMMAND: test-tag |
|
247
|
** |
|
248
|
** Usage: %fossil test-tag (+|*|-)TAGNAME ARTIFACT-ID ?VALUE? |
|
249
|
** |
|
250
|
** Add a tag or anti-tag to the rebuildable tables of the local repository. |
|
251
|
** No tag artifact is created so the new tag is erased the next |
|
252
|
** time the repository is rebuilt. This routine is for testing |
|
253
|
** use only. |
|
254
|
*/ |
|
255
|
void testtag_cmd(void){ |
|
256
|
const char *zTag; |
|
257
|
const char *zValue; |
|
258
|
int rid; |
|
259
|
int tagtype; |
|
260
|
db_must_be_within_tree(); |
|
261
|
if( g.argc!=4 && g.argc!=5 ){ |
|
262
|
usage("TAGNAME ARTIFACT-ID ?VALUE?"); |
|
263
|
} |
|
264
|
zTag = g.argv[2]; |
|
265
|
switch( zTag[0] ){ |
|
266
|
case '+': tagtype = 1; break; |
|
267
|
case '*': tagtype = 2; break; |
|
268
|
case '-': tagtype = 0; break; |
|
269
|
default: |
|
270
|
fossil_fatal("tag should begin with '+', '*', or '-'"); |
|
271
|
return; |
|
272
|
} |
|
273
|
rid = name_to_rid(g.argv[3]); |
|
274
|
if( rid==0 ){ |
|
275
|
fossil_fatal("no such object: %s", g.argv[3]); |
|
276
|
} |
|
277
|
g.markPrivate = content_is_private(rid); |
|
278
|
zValue = g.argc==5 ? g.argv[4] : 0; |
|
279
|
db_begin_transaction(); |
|
280
|
tag_insert(zTag, tagtype, zValue, -1, 0.0, rid); |
|
281
|
db_end_transaction(0); |
|
282
|
} |
|
283
|
|
|
284
|
/* |
|
285
|
** OR this value into the tagtype argument to tag_add_artifact to |
|
286
|
** cause the tag to be displayed on standard output rather than be |
|
287
|
** inserted. Used for --dry-run options and debugging. |
|
288
|
*/ |
|
289
|
#if INTERFACE |
|
290
|
#define TAG_ADD_DRYRUN 0x04 |
|
291
|
#endif |
|
292
|
|
|
293
|
/* |
|
294
|
** Add a control record to the repository that either creates |
|
295
|
** or cancels a tag. |
|
296
|
** |
|
297
|
** tagtype should normally be 0, 1, or 2. But if the TAG_ADD_DRYRUN bit |
|
298
|
** is also set, then simply print the text of the tag on standard output |
|
299
|
** (for testing purposes) rather than create the tag. |
|
300
|
*/ |
|
301
|
void tag_add_artifact( |
|
302
|
const char *zPrefix, /* Prefix to prepend to tag name */ |
|
303
|
const char *zTagname, /* The tag to add or cancel */ |
|
304
|
const char *zObjName, /* Name of object attached to */ |
|
305
|
const char *zValue, /* Value for the tag. Might be NULL */ |
|
306
|
int tagtype, /* 0:cancel 1:singleton 2:propagated */ |
|
307
|
const char *zDateOvrd, /* Override date string */ |
|
308
|
const char *zUserOvrd /* Override user name */ |
|
309
|
){ |
|
310
|
int rid; |
|
311
|
int nrid; |
|
312
|
char *zDate; |
|
313
|
Blob uuid; |
|
314
|
Blob ctrl; |
|
315
|
Blob cksum; |
|
316
|
static const char zTagtype[] = { '-', '+', '*' }; |
|
317
|
int dryRun = 0; |
|
318
|
|
|
319
|
if( tagtype & TAG_ADD_DRYRUN ){ |
|
320
|
tagtype &= ~TAG_ADD_DRYRUN; |
|
321
|
dryRun = 1; |
|
322
|
} |
|
323
|
assert( tagtype>=0 && tagtype<=2 ); |
|
324
|
user_select(); |
|
325
|
blob_zero(&uuid); |
|
326
|
blob_append(&uuid, zObjName, -1); |
|
327
|
if( name_to_uuid(&uuid, 9, "*") ){ |
|
328
|
fossil_fatal("%s", g.zErrMsg); |
|
329
|
return; |
|
330
|
} |
|
331
|
rid = name_to_rid(blob_str(&uuid)); |
|
332
|
g.markPrivate = content_is_private(rid); |
|
333
|
blob_zero(&ctrl); |
|
334
|
|
|
335
|
#if 0 |
|
336
|
if( validate16(zTagname, strlen(zTagname)) ){ |
|
337
|
fossil_fatal( |
|
338
|
"invalid tag name \"%s\" - might be confused with" |
|
339
|
" a hexadecimal artifact ID", |
|
340
|
zTagname |
|
341
|
); |
|
342
|
} |
|
343
|
#endif |
|
344
|
zDate = date_in_standard_format(zDateOvrd ? zDateOvrd : "now"); |
|
345
|
blob_appendf(&ctrl, "D %s\n", zDate); |
|
346
|
blob_appendf(&ctrl, "T %c%s%F %b", |
|
347
|
zTagtype[tagtype], zPrefix, zTagname, &uuid); |
|
348
|
if( tagtype>0 && zValue && zValue[0] ){ |
|
349
|
blob_appendf(&ctrl, " %F\n", zValue); |
|
350
|
}else{ |
|
351
|
blob_appendf(&ctrl, "\n"); |
|
352
|
} |
|
353
|
blob_appendf(&ctrl, "U %F\n", zUserOvrd ? zUserOvrd : login_name()); |
|
354
|
md5sum_blob(&ctrl, &cksum); |
|
355
|
blob_appendf(&ctrl, "Z %b\n", &cksum); |
|
356
|
if( dryRun ){ |
|
357
|
fossil_print("%s", blob_str(&ctrl)); |
|
358
|
blob_reset(&ctrl); |
|
359
|
}else{ |
|
360
|
nrid = content_put(&ctrl); |
|
361
|
manifest_crosslink(nrid, &ctrl, MC_PERMIT_HOOKS); |
|
362
|
} |
|
363
|
assert( blob_is_reset(&ctrl) ); |
|
364
|
if( g.localOpen ){ |
|
365
|
manifest_to_disk(rid); |
|
366
|
} |
|
367
|
} |
|
368
|
|
|
369
|
/* |
|
370
|
** If zTag is NULL or valid for use as a tag for the `tag add` and |
|
371
|
** `tag cancel` commands, returns without side effects, else emits a |
|
372
|
** fatal error message. We reject certain prefixes to avoid that |
|
373
|
** clients cause undue grief by improperly tagging artifacts as being, |
|
374
|
** e.g., wiki pages or tickets. |
|
375
|
** |
|
376
|
** Note that we intentionally allow the "sym-" prefix, partly for |
|
377
|
** historical compatibility and partly because it can be applied |
|
378
|
** properly, whereas the other reserved name types have special |
|
379
|
** meanings for fossil and cannot be sensibly manually manipulated. |
|
380
|
*/ |
|
381
|
static void tag_cmd_tagname_check(const char *zTag){ |
|
382
|
if(zTag && *zTag && |
|
383
|
(strncmp(zTag,"wiki-",5)==0 |
|
384
|
|| strncmp(zTag,"tkt-",4)==0 |
|
385
|
|| strncmp(zTag,"event-",6)==0)){ |
|
386
|
fossil_fatal("Invalid prefix for tag name: %s", zTag); |
|
387
|
} |
|
388
|
} |
|
389
|
|
|
390
|
/* |
|
391
|
** COMMAND: tag |
|
392
|
** |
|
393
|
** Usage: %fossil tag SUBCOMMAND ... |
|
394
|
** |
|
395
|
** Run various subcommands to control tags and properties. |
|
396
|
** |
|
397
|
** > fossil tag add ?OPTIONS? TAGNAME ARTIFACT-ID ?VALUE? |
|
398
|
** |
|
399
|
** Add a new tag or property to an artifact referenced by |
|
400
|
** ARTIFACT-ID. For check-ins, the tag will be usable instead |
|
401
|
** of a CHECK-IN in commands such as update and merge. If the |
|
402
|
** --propagate flag is present and ARTIFACT-ID refers to a |
|
403
|
** wiki page, forum post, technote, or check-in, the tag |
|
404
|
** propagates to all descendants of that artifact. |
|
405
|
** |
|
406
|
** Options: |
|
407
|
** --date-override DATETIME Set date and time added |
|
408
|
** -n|--dry-run Display the tag text, but do not |
|
409
|
** actually insert it into the database |
|
410
|
** --propagate Propagating tag |
|
411
|
** --raw Raw tag name. Ignored for |
|
412
|
** non-CHECK-IN artifacts. |
|
413
|
** --user-override USER Name USER when adding the tag |
|
414
|
** |
|
415
|
** The --date-override and --user-override options support |
|
416
|
** importing history from other SCM systems. DATETIME has |
|
417
|
** the form 'YYYY-MM-DD HH:MM:SS'. |
|
418
|
** |
|
419
|
** Note that fossil uses some tag prefixes internally and this |
|
420
|
** command will reject tags with these prefixes to avoid |
|
421
|
** causing problems or confusion: "wiki-", "tkt-", "event-". |
|
422
|
** |
|
423
|
** > fossil tag cancel ?--raw? TAGNAME ARTIFACT-ID |
|
424
|
** |
|
425
|
** Remove the tag TAGNAME from the artifact referenced by |
|
426
|
** ARTIFACT-ID, and also remove the propagation of the tag to |
|
427
|
** any descendants. Use the -n|--dry-run option to see |
|
428
|
** what would have happened. Certain tag name prefixes are |
|
429
|
** forbidden, as documented for the 'add' subcommand. |
|
430
|
** |
|
431
|
** Options: |
|
432
|
** --date-override DATETIME Set date and time deleted |
|
433
|
** -n|--dry-run Display the control artifact, but do |
|
434
|
** not insert it into the database |
|
435
|
** --raw Raw tag name. Ignored for |
|
436
|
** non-CHECK-IN artifacts. |
|
437
|
** --user-override USER Name USER when deleting the tag |
|
438
|
** |
|
439
|
** > fossil tag find ?OPTIONS? TAGNAME |
|
440
|
** |
|
441
|
** List all objects that use TAGNAME. |
|
442
|
** |
|
443
|
** Options: |
|
444
|
** -n|--limit N Limit to N results |
|
445
|
** --raw Interprets tag as a raw name instead of a |
|
446
|
** branch name and matches any type of artifact. |
|
447
|
** Changes the output to include only the |
|
448
|
** hashes of matching objects. |
|
449
|
** -t|--type TYPE One of: ci (check-in), w (wiki), |
|
450
|
** e (event/technote), f (forum post), |
|
451
|
** t (ticket). Default is all types. Ignored |
|
452
|
** if --raw is used. |
|
453
|
** |
|
454
|
** > fossil tag list|ls ?OPTIONS? ?ARTIFACT-ID? |
|
455
|
** |
|
456
|
** List all tags or, if ARTIFACT-ID is supplied, all tags and |
|
457
|
** their values for that artifact. The tagtype option accepts |
|
458
|
** one of: propagated, singleton, cancel. For historical |
|
459
|
** scripting compatibility, the internal tag types "wiki-", |
|
460
|
** "tkt-", and "event-" (technote) are elided by default |
|
461
|
** unless the --raw or --prefix options are used. |
|
462
|
** |
|
463
|
** Options: |
|
464
|
** -v|--inverse Inverse the meaning of --tagtype TYPE |
|
465
|
** --prefix List only tags with the given prefix |
|
466
|
** Fossil-internal prefixes include "sym-" |
|
467
|
** (branch name), "wiki-", "event-" |
|
468
|
** (technote), and "tkt-" (ticket). The |
|
469
|
** prefix is stripped from the resulting |
|
470
|
** list unless --raw is provided. Ignored if |
|
471
|
** ARTIFACT-ID is provided. |
|
472
|
** --raw List raw names of tags |
|
473
|
** --sep SEP Separator when concatenating values |
|
474
|
** --tagtype TYPE List only tags of type TYPE, which must |
|
475
|
** be one of: cancel, singleton, propagated |
|
476
|
** --values List tag values |
|
477
|
** If --sep is supplied, list all values of a tag on |
|
478
|
** the same line, separated by SEP; otherwise list |
|
479
|
** each value on its own line. |
|
480
|
** |
|
481
|
** The option --raw allows the manipulation of all types of tags |
|
482
|
** used for various internal purposes in fossil. It also shows |
|
483
|
** "cancel" tags for the "find" and "list" subcommands. You should |
|
484
|
** not use this option to make changes unless you are sure what |
|
485
|
** you are doing. |
|
486
|
** |
|
487
|
** If you need to use a tagname that might be confused with |
|
488
|
** a hexadecimal baseline or artifact ID, you can explicitly |
|
489
|
** disambiguate it by prefixing it with "tag:". For instance: |
|
490
|
** |
|
491
|
** fossil update decaf |
|
492
|
** |
|
493
|
** will be taken as an artifact or baseline ID and fossil will |
|
494
|
** probably complain that no such revision was found. However |
|
495
|
** |
|
496
|
** fossil update tag:decaf |
|
497
|
** |
|
498
|
** will assume that "decaf" is a tag/branch name. |
|
499
|
** |
|
500
|
*/ |
|
501
|
void tag_cmd(void){ |
|
502
|
int n; |
|
503
|
|
|
504
|
db_find_and_open_repository(0, 0); |
|
505
|
if( g.argc<3 ){ |
|
506
|
goto tag_cmd_usage; |
|
507
|
} |
|
508
|
n = strlen(g.argv[2]); |
|
509
|
if( n==0 ){ |
|
510
|
goto tag_cmd_usage; |
|
511
|
} |
|
512
|
|
|
513
|
if( strncmp(g.argv[2],"add",n)==0 ){ |
|
514
|
char *zValue; |
|
515
|
int dryRun = 0; |
|
516
|
int fRaw = find_option("raw","",0)!=0; |
|
517
|
const char *zPrefix = ""; |
|
518
|
int fPropagate = find_option("propagate","",0)!=0; |
|
519
|
const char *zDateOvrd = find_option("date-override",0,1); |
|
520
|
const char *zUserOvrd = find_option("user-override",0,1); |
|
521
|
const char *zTag; |
|
522
|
const char *zObjId; |
|
523
|
int objType; |
|
524
|
if( find_option("dry-run","n",0)!=0 ) dryRun = TAG_ADD_DRYRUN; |
|
525
|
if( g.argc!=5 && g.argc!=6 ){ |
|
526
|
usage("add ?options? TAGNAME ARTIFACT-ID ?VALUE?"); |
|
527
|
} |
|
528
|
zTag = g.argv[3]; |
|
529
|
tag_cmd_tagname_check(zTag); |
|
530
|
zObjId = g.argv[4]; |
|
531
|
zValue = g.argc==6 ? g.argv[5] : 0; |
|
532
|
objType = whatis_rid_type(symbolic_name_to_rid(zObjId, 0)); |
|
533
|
switch(objType){ |
|
534
|
case 0: |
|
535
|
fossil_fatal("Cannot resolve artifact ID: %s", zObjId); |
|
536
|
break; |
|
537
|
case CFTYPE_MANIFEST: |
|
538
|
zPrefix = fRaw ? "" : "sym-"; |
|
539
|
break; |
|
540
|
default: break; |
|
541
|
} |
|
542
|
db_begin_transaction(); |
|
543
|
tag_add_artifact(zPrefix, zTag, zObjId, zValue, |
|
544
|
1+fPropagate+dryRun,zDateOvrd,zUserOvrd); |
|
545
|
db_end_transaction(0); |
|
546
|
}else |
|
547
|
|
|
548
|
if( strncmp(g.argv[2],"branch",n)==0 ){ |
|
549
|
fossil_fatal("the \"fossil tag branch\" command is discontinued\n" |
|
550
|
"Use the \"fossil branch new\" command instead."); |
|
551
|
}else |
|
552
|
|
|
553
|
if( strncmp(g.argv[2],"cancel",n)==0 ){ |
|
554
|
int dryRun = 0; |
|
555
|
int fRaw = find_option("raw","",0)!=0; |
|
556
|
const char *zPrefix = ""; |
|
557
|
const char *zDateOvrd = find_option("date-override",0,1); |
|
558
|
const char *zUserOvrd = find_option("user-override",0,1); |
|
559
|
const char *zTag; |
|
560
|
const char *zObjId; |
|
561
|
int objType; |
|
562
|
if( find_option("dry-run","n",0)!=0 ) dryRun = TAG_ADD_DRYRUN; |
|
563
|
if( g.argc!=5 ){ |
|
564
|
usage("cancel ?options? TAGNAME ARTIFACT-ID"); |
|
565
|
} |
|
566
|
zTag = g.argv[3]; |
|
567
|
tag_cmd_tagname_check(zTag); |
|
568
|
zObjId = g.argv[4]; |
|
569
|
objType = whatis_rid_type(symbolic_name_to_rid(zObjId, 0)); |
|
570
|
switch(objType){ |
|
571
|
case 0: |
|
572
|
fossil_fatal("Cannot resolve artifact ID: %s", zObjId); |
|
573
|
break; |
|
574
|
case CFTYPE_MANIFEST: |
|
575
|
zPrefix = fRaw ? "" : "sym-"; |
|
576
|
break; |
|
577
|
default: break; |
|
578
|
} |
|
579
|
db_begin_transaction(); |
|
580
|
tag_add_artifact(zPrefix, zTag, zObjId, 0, dryRun, |
|
581
|
zDateOvrd, zUserOvrd); |
|
582
|
db_end_transaction(0); |
|
583
|
}else |
|
584
|
|
|
585
|
if( strncmp(g.argv[2],"find",n)==0 ){ |
|
586
|
Stmt q; |
|
587
|
int fRaw = find_option("raw","",0)!=0; |
|
588
|
const char *zFindLimit = find_option("limit","n",1); |
|
589
|
const int nFindLimit = zFindLimit ? atoi(zFindLimit) : -2000; |
|
590
|
const char *zType = find_option("type","t",1); |
|
591
|
Blob sql = empty_blob; |
|
592
|
const char *zTag; |
|
593
|
if( zType==0 || zType[0]==0 ) zType = "*"; |
|
594
|
if( g.argc!=4 ){ |
|
595
|
usage("find ?--raw? ?-t|--type TYPE? ?-n|--limit #? TAGNAME"); |
|
596
|
} |
|
597
|
zTag = g.argv[3]; |
|
598
|
if( fRaw ){ |
|
599
|
blob_append_sql(&sql, |
|
600
|
"SELECT blob.uuid FROM tagxref, blob" |
|
601
|
" WHERE tagid=(SELECT tagid FROM tag WHERE tagname=%Q)" |
|
602
|
" AND tagxref.tagtype>0" |
|
603
|
" AND blob.rid=tagxref.rid", |
|
604
|
zTag |
|
605
|
); |
|
606
|
if( nFindLimit>0 ){ |
|
607
|
blob_append_sql(&sql, " LIMIT %d", nFindLimit); |
|
608
|
} |
|
609
|
db_prepare(&q, "%s", blob_sql_text(&sql)); |
|
610
|
blob_reset(&sql); |
|
611
|
while( db_step(&q)==SQLITE_ROW ){ |
|
612
|
fossil_print("%s\n", db_column_text(&q, 0)); |
|
613
|
} |
|
614
|
db_finalize(&q); |
|
615
|
}else{ |
|
616
|
blob_append_sql(&sql, |
|
617
|
"%s" |
|
618
|
" AND event.type GLOB '%q'" |
|
619
|
" AND blob.rid IN (" |
|
620
|
" SELECT rid FROM tagxref" |
|
621
|
" WHERE tagtype>0 AND tagid IN (" |
|
622
|
" SELECT tagid FROM tag WHERE tagname IN " |
|
623
|
" ('%q','sym-%q','wiki-%q','tkt-%q','event-%q')" |
|
624
|
" )" |
|
625
|
")" |
|
626
|
" ORDER BY event.mtime DESC /*sort*/", |
|
627
|
timeline_query_for_tty(), zType, zTag, zTag, zTag, zTag, zTag |
|
628
|
); |
|
629
|
db_prepare(&q, "%s", blob_sql_text(&sql)); |
|
630
|
blob_reset(&sql); |
|
631
|
print_timeline(&q, nFindLimit, 79, 0, 0); |
|
632
|
db_finalize(&q); |
|
633
|
} |
|
634
|
}else |
|
635
|
|
|
636
|
if(( strncmp(g.argv[2],"list",n)==0 )||( strncmp(g.argv[2],"ls",n)==0 )){ |
|
637
|
Stmt q; |
|
638
|
const int fRaw = find_option("raw","",0)!=0; |
|
639
|
const char *zTagType = find_option("tagtype","t",1); |
|
640
|
const int fInverse = find_option("inverse","v",0)!=0; |
|
641
|
const char *zTagPrefix = find_option("prefix","",1); |
|
642
|
int nTagType = fRaw ? -1 : 0; |
|
643
|
int fValues = find_option("values","",0)!=0; |
|
644
|
const char *zSep = find_option("sep","",1); |
|
645
|
|
|
646
|
|
|
647
|
if( zTagType!=0 ){ |
|
648
|
int l = strlen(zTagType); |
|
649
|
if( strncmp(zTagType,"cancel",l)==0 ){ |
|
650
|
nTagType = 0; |
|
651
|
}else if( strncmp(zTagType,"singleton",l)==0 ){ |
|
652
|
nTagType = 1; |
|
653
|
}else if( strncmp(zTagType,"propagated",l)==0 ){ |
|
654
|
nTagType = 2; |
|
655
|
}else{ |
|
656
|
fossil_fatal("unrecognized tag type"); |
|
657
|
} |
|
658
|
} |
|
659
|
if( g.argc==3 ){ |
|
660
|
const int nTagPrefix = zTagPrefix ? (int)strlen(zTagPrefix) : 0; |
|
661
|
if( !fValues ){ |
|
662
|
db_prepare(&q, |
|
663
|
"SELECT tagname FROM tag" |
|
664
|
" WHERE EXISTS(SELECT 1 FROM tagxref" |
|
665
|
" WHERE tagid=tag.tagid" |
|
666
|
" AND tagtype%s%d)" |
|
667
|
" AND CASE WHEN %Q IS NULL THEN 1 ELSE tagname GLOB %Q||'*' " |
|
668
|
" END ORDER BY tagname COLLATE uintnocase", |
|
669
|
zTagType!=0 ? (fInverse!=0?"<>":"=") : ">"/*safe-for-%s*/, |
|
670
|
nTagType, zTagPrefix, zTagPrefix |
|
671
|
); |
|
672
|
}else{ |
|
673
|
if( zSep ){ |
|
674
|
db_prepare(&q, |
|
675
|
/* work around group_concat() with DISTINCT and custom separator */ |
|
676
|
"SELECT tagname," |
|
677
|
" rtrim(replace(group_concat(DISTINCT value||'@!' " |
|
678
|
" ORDER BY value ASC), '@!,', %Q),'@!')" |
|
679
|
" FROM tagxref, tag" |
|
680
|
" WHERE tagxref.tagid=tag.tagid AND tagtype%s%d" |
|
681
|
" AND CASE WHEN %Q IS NULL THEN 1 ELSE tagname GLOB %Q||'*' END" |
|
682
|
" GROUP BY tagname" |
|
683
|
" ORDER BY tagname COLLATE uintnocase", |
|
684
|
( zSep && strlen(zSep)>0 ) ? zSep : ",", |
|
685
|
zTagType!=0 ? (fInverse!=0?"<>":"=") : ">"/*safe-for-%s*/, |
|
686
|
nTagType, zTagPrefix, zTagPrefix |
|
687
|
); |
|
688
|
}else{ |
|
689
|
db_prepare(&q, |
|
690
|
"SELECT DISTINCT tagname, value" |
|
691
|
" FROM tagxref, tag" |
|
692
|
" WHERE tagxref.tagid=tag.tagid AND tagtype%s%d" |
|
693
|
" AND CASE WHEN %Q IS NULL THEN 1 ELSE tagname GLOB %Q||'*' END" |
|
694
|
" ORDER BY tagname, value COLLATE uintnocase", |
|
695
|
zTagType!=0 ? (fInverse!=0?"<>":"=") : ">"/*safe-for-%s*/, |
|
696
|
nTagType, zTagPrefix, zTagPrefix |
|
697
|
); |
|
698
|
} |
|
699
|
} |
|
700
|
while( db_step(&q)==SQLITE_ROW ){ |
|
701
|
const char *zName = db_column_text(&q, 0); |
|
702
|
const char *zValue = db_column_text(&q, 1); |
|
703
|
int nWidth = fValues ? 20 : 0; |
|
704
|
const char *zzValue = (fValues && zValue) ? mprintf(" %s", zValue) : ""; |
|
705
|
if( fRaw ){ |
|
706
|
fossil_print("%-*s%s\n", nWidth, zName, zzValue); |
|
707
|
}else if( nTagPrefix>0 ){ |
|
708
|
assert(db_column_bytes(&q,0)>=nTagPrefix); |
|
709
|
fossil_print("%-*s%s\n", nWidth, &zName[nTagPrefix], zzValue); |
|
710
|
}else if( strncmp(zName, "sym-", 4)==0 ){ |
|
711
|
fossil_print("%-*s%s\n", nWidth, &zName[4], zzValue); |
|
712
|
} |
|
713
|
} |
|
714
|
db_finalize(&q); |
|
715
|
}else if( g.argc==4 ){ |
|
716
|
char const *zObjId = g.argv[3]; |
|
717
|
const int rid = name_to_rid(zObjId); |
|
718
|
const int objType = whatis_rid_type(rid); |
|
719
|
int nTagOffset = 0; |
|
720
|
|
|
721
|
zTagPrefix = 0; |
|
722
|
if(objType<=0){ |
|
723
|
fossil_fatal("Cannot resolve artifact ID: %s", zObjId); |
|
724
|
}else if(fRaw==0){ |
|
725
|
/* Figure out the tag prefix to strip */ |
|
726
|
switch(objType){ |
|
727
|
case CFTYPE_MANIFEST: zTagPrefix = "sym-"; break; |
|
728
|
case CFTYPE_WIKI: zTagPrefix = "wiki-"; break; |
|
729
|
case CFTYPE_TICKET: zTagPrefix = "tkt-"; break; |
|
730
|
case CFTYPE_EVENT: zTagPrefix = "event-"; break; |
|
731
|
default: break; |
|
732
|
} |
|
733
|
if(zTagPrefix!=0){ |
|
734
|
nTagOffset = (int)strlen(zTagPrefix); |
|
735
|
} |
|
736
|
} |
|
737
|
db_prepare(&q, |
|
738
|
"SELECT tagname, value FROM tagxref, tag" |
|
739
|
" WHERE tagxref.rid=%d AND tagxref.tagid=tag.tagid" |
|
740
|
" AND tagtype%s%d" |
|
741
|
" ORDER BY tagname COLLATE uintnocase", |
|
742
|
rid, |
|
743
|
zTagType!=0 ? (fInverse!=0?"<>":"=") : ">"/*safe-for-%s*/, |
|
744
|
nTagType |
|
745
|
); |
|
746
|
while( db_step(&q)==SQLITE_ROW ){ |
|
747
|
const char *zName = db_column_text(&q, 0); |
|
748
|
const char *zValue = db_column_text(&q, 1); |
|
749
|
if( zTagPrefix && strncmp(zName, zTagPrefix, nTagOffset)==0 ){ |
|
750
|
zName += nTagOffset; |
|
751
|
} |
|
752
|
if( zValue && zValue[0] ){ |
|
753
|
fossil_print("%s=%s\n", zName, zValue); |
|
754
|
}else{ |
|
755
|
fossil_print("%s\n", zName); |
|
756
|
} |
|
757
|
} |
|
758
|
db_finalize(&q); |
|
759
|
}else{ |
|
760
|
usage("list ?OPTIONS? ?CHECK-IN?"); |
|
761
|
} |
|
762
|
}else |
|
763
|
{ |
|
764
|
goto tag_cmd_usage; |
|
765
|
} |
|
766
|
|
|
767
|
/* Cleanup */ |
|
768
|
return; |
|
769
|
|
|
770
|
tag_cmd_usage: |
|
771
|
usage("add|cancel|find|list ..."); |
|
772
|
} |
|
773
|
|
|
774
|
/* |
|
775
|
** COMMAND: reparent* |
|
776
|
** |
|
777
|
** Usage: %fossil reparent [OPTIONS] CHECK-IN PARENT ... |
|
778
|
** |
|
779
|
** Create a "parent" tag that causes CHECK-IN to be interpreted as a |
|
780
|
** child of PARENT. If multiple PARENTs are listed, then the first is |
|
781
|
** the primary parent and others are merge ancestors. |
|
782
|
** |
|
783
|
** This is an experts-only command. It is used to patch up a repository |
|
784
|
** that has been damaged by a shun or that has been pieced together from |
|
785
|
** two or more separate repositories. You should never need to reparent |
|
786
|
** during normal operations. |
|
787
|
** |
|
788
|
** Reparenting is accomplished by adding a parent tag. So to undo the |
|
789
|
** reparenting operation, simply delete the tag. |
|
790
|
** |
|
791
|
** --test Make database entries but do not add the tag artifact. |
|
792
|
** So the reparent operation will be undone by the next |
|
793
|
** "fossil rebuild" command. |
|
794
|
** -n|--dry-run Print the tag that would have been created but do not |
|
795
|
** actually change the database in any way. |
|
796
|
** --date-override DATETIME Set the change time on the control artifact |
|
797
|
** --user-override USER Set the user name on the control artifact |
|
798
|
*/ |
|
799
|
void reparent_cmd(void){ |
|
800
|
int bTest = find_option("test","",0)!=0; |
|
801
|
int rid; |
|
802
|
int i; |
|
803
|
Blob value; |
|
804
|
char *zUuid; |
|
805
|
int dryRun = 0; |
|
806
|
const char *zDateOvrd; /* The change time on the control artifact */ |
|
807
|
const char *zUserOvrd; /* The user name on the control artifact */ |
|
808
|
|
|
809
|
if( find_option("dry-run","n",0)!=0 ) dryRun = TAG_ADD_DRYRUN; |
|
810
|
zDateOvrd = find_option("date-override",0,1); |
|
811
|
zUserOvrd = find_option("user-override",0,1); |
|
812
|
db_find_and_open_repository(0, 0); |
|
813
|
verify_all_options(); |
|
814
|
if( g.argc<4 ){ |
|
815
|
usage("[OPTIONS] CHECK-IN PARENT ..."); |
|
816
|
} |
|
817
|
rid = name_to_typed_rid(g.argv[2], "ci"); |
|
818
|
blob_init(&value, 0, 0); |
|
819
|
for(i=3; i<g.argc; i++){ |
|
820
|
int pid = name_to_typed_rid(g.argv[i], "ci"); |
|
821
|
if( i>3 ) blob_append(&value, " ", 1); |
|
822
|
zUuid = rid_to_uuid(pid); |
|
823
|
blob_append(&value, zUuid, strlen(zUuid)); |
|
824
|
fossil_free(zUuid); |
|
825
|
} |
|
826
|
if( bTest && !dryRun ){ |
|
827
|
tag_insert("parent", 1, blob_str(&value), -1, 0.0, rid); |
|
828
|
}else{ |
|
829
|
zUuid = rid_to_uuid(rid); |
|
830
|
tag_add_artifact("","parent",zUuid,blob_str(&value),1|dryRun, |
|
831
|
zDateOvrd,zUserOvrd); |
|
832
|
} |
|
833
|
} |
|
834
|
|
|
835
|
|
|
836
|
/* |
|
837
|
** WEBPAGE: taglist |
|
838
|
** |
|
839
|
** List all non-propagating symbolic tags. |
|
840
|
*/ |
|
841
|
void taglist_page(void){ |
|
842
|
Stmt q; |
|
843
|
|
|
844
|
login_check_credentials(); |
|
845
|
if( !g.perm.Read ){ |
|
846
|
login_needed(g.anon.Read); |
|
847
|
} |
|
848
|
cgi_check_for_malice(); |
|
849
|
login_anonymous_available(); |
|
850
|
style_header("Tags"); |
|
851
|
style_adunit_config(ADUNIT_RIGHT_OK); |
|
852
|
style_submenu_element("Timeline", "tagtimeline"); |
|
853
|
@ <h2>Non-propagating tags:</h2> |
|
854
|
@ <table class='sortable' data-column-types='ktn' data-init-sort='2'> |
|
855
|
@ <thead><tr> |
|
856
|
@ <th>Tag Name</th> |
|
857
|
@ <th>Most Recent</th> |
|
858
|
@ <th>Count</th> |
|
859
|
@ </tr></thead><tbody> |
|
860
|
|
|
861
|
db_prepare(&q, |
|
862
|
"SELECT substr(tagname,5),\n" |
|
863
|
"row_number()OVER(ORDER BY tagname COLLATE uintnocase),\n" |
|
864
|
"substr(datetime(max(event.mtime)),1,16),\n" |
|
865
|
"count(*)\n" |
|
866
|
"FROM tagxref JOIN tag USING(tagid)\n" |
|
867
|
" JOIN event ON event.objid=tagxref.rid\n" |
|
868
|
"WHERE tagname like 'sym-%%'\n" |
|
869
|
"AND tagxref.tagtype=1\n" |
|
870
|
"GROUP BY 1\n" |
|
871
|
"ORDER BY 3 DESC;\n" |
|
872
|
); |
|
873
|
while( db_step(&q)==SQLITE_ROW ){ |
|
874
|
const char *zName = db_column_text(&q, 0); |
|
875
|
int rn = db_column_int(&q, 1); |
|
876
|
const char *zDate = db_column_text(&q, 2); |
|
877
|
int cnt = db_column_int(&q, 3); |
|
878
|
@ <tr><td data-sortkey="%06x(rn)">\ |
|
879
|
if( g.perm.Hyperlink ){ |
|
880
|
@ %z(chref("taglink","%R/timeline?t=%T",zName))%h(zName)</a></td>\ |
|
881
|
}else{ |
|
882
|
@ <span class="tagDsp">%h(zName)</span></td>\ |
|
883
|
} |
|
884
|
@ <td> %h(zDate) </td>\ |
|
885
|
@ <td align="center">%d(cnt)</td></tr> |
|
886
|
} |
|
887
|
@ </table> |
|
888
|
db_finalize(&q); |
|
889
|
style_table_sorter(); |
|
890
|
style_finish_page(); |
|
891
|
} |
|
892
|
|
|
893
|
/* |
|
894
|
** WEBPAGE: /tagtimeline |
|
895
|
** |
|
896
|
** Render a timeline with all check-ins that contain non-propagating |
|
897
|
** symbolic tags. |
|
898
|
** |
|
899
|
** Query parameters: |
|
900
|
** |
|
901
|
** ng No graph |
|
902
|
** nohidden Hide check-ins with "hidden" tag |
|
903
|
** onlyhidden Show only check-ins with "hidden" tag |
|
904
|
** brbg Background color by branch name |
|
905
|
** ubg Background color by user name |
|
906
|
*/ |
|
907
|
void tagtimeline_page(void){ |
|
908
|
Blob sql = empty_blob; |
|
909
|
Stmt q; |
|
910
|
int tmFlags; /* Timeline display flags */ |
|
911
|
int fNoHidden = PB("nohidden")!=0; /* The "nohidden" query parameter */ |
|
912
|
int fOnlyHidden = PB("onlyhidden")!=0; /* The "onlyhidden" query parameter */ |
|
913
|
|
|
914
|
login_check_credentials(); |
|
915
|
if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
|
916
|
|
|
917
|
style_header("Tagged Check-ins"); |
|
918
|
style_submenu_element("List", "taglist"); |
|
919
|
login_anonymous_available(); |
|
920
|
timeline_ss_submenu(); |
|
921
|
@ <h2>Check-ins with non-propagating tags:</h2> |
|
922
|
blob_append(&sql, timeline_query_for_www(), -1); |
|
923
|
blob_append_sql(&sql, |
|
924
|
"AND blob.rid IN (SELECT rid FROM tagxref" |
|
925
|
" WHERE tagtype=1 AND srcid>0" |
|
926
|
" AND tagid IN (SELECT tagid FROM tag " |
|
927
|
" WHERE tagname GLOB 'sym-*'))"); |
|
928
|
if( fNoHidden || fOnlyHidden ){ |
|
929
|
const char* zUnaryOp = fNoHidden ? "NOT" : ""; |
|
930
|
blob_append_sql(&sql, |
|
931
|
" AND %s EXISTS(SELECT 1 FROM tagxref" |
|
932
|
" WHERE tagid=%d AND tagtype>0 AND rid=blob.rid)\n", |
|
933
|
zUnaryOp/*safe-for-%s*/, TAG_HIDDEN); |
|
934
|
} |
|
935
|
db_prepare(&q, "%s ORDER BY event.mtime DESC /*sort*/", blob_sql_text(&sql)); |
|
936
|
blob_reset(&sql); |
|
937
|
/* Always specify TIMELINE_DISJOINT, or graph_finish() may fail because of too |
|
938
|
** many descenders to (off-screen) parents. */ |
|
939
|
tmFlags = TIMELINE_XMERGE | TIMELINE_FILLGAPS | TIMELINE_NOSCROLL; |
|
940
|
if( PB("ng")==0 ) tmFlags |= TIMELINE_GRAPH; |
|
941
|
if( PB("brbg")!=0 ) tmFlags |= TIMELINE_BRCOLOR; |
|
942
|
if( PB("ubg")!=0 ) tmFlags |= TIMELINE_UCOLOR; |
|
943
|
www_print_timeline(&q, tmFlags, 0, 0, 0, 0, 0, 0); |
|
944
|
db_finalize(&q); |
|
945
|
@ <br> |
|
946
|
style_finish_page(); |
|
947
|
} |
|
948
|
|
|
949
|
/* |
|
950
|
** Returns true if the given blob.rid value has the given tag ID |
|
951
|
** applied to it, else false. |
|
952
|
*/ |
|
953
|
int rid_has_tag(int rid, int tagId){ |
|
954
|
return db_exists( |
|
955
|
"SELECT tag.tagid FROM tagxref, tag" |
|
956
|
" WHERE tagxref.rid=%d AND tagtype>0 " |
|
957
|
" AND tag.tagid=%d" |
|
958
|
" AND tagxref.tagid=tag.tagid", |
|
959
|
rid, tagId |
|
960
|
); |
|
961
|
} |
|
962
|
|
|
963
|
|
|
964
|
/* |
|
965
|
** Returns tagxref.rowid if the given blob.rid has a tagxref.rid entry |
|
966
|
** of an active (non-cancelled) tag matching the given rid and tag |
|
967
|
** name string, else returns 0. Note that this function does not |
|
968
|
** distinguish between a non-existent tag and a cancelled tag. |
|
969
|
** |
|
970
|
** Design note: the return value is the tagxref.rowid because that |
|
971
|
** gives us an easy way to fetch the value of the tag later on, if |
|
972
|
** needed. |
|
973
|
*/ |
|
974
|
int rid_has_active_tag_name(int rid, const char *zTagName){ |
|
975
|
static Stmt q = empty_Stmt_m; |
|
976
|
int rc; |
|
977
|
|
|
978
|
assert( 0 != zTagName ); |
|
979
|
if( !q.pStmt ){ |
|
980
|
db_static_prepare(&q, |
|
981
|
"SELECT x.rowid FROM tagxref x, tag t" |
|
982
|
" WHERE x.rid=$rid AND x.tagtype>0 " |
|
983
|
" AND x.tagid=t.tagid" |
|
984
|
" AND t.tagname=$tagname" |
|
985
|
); |
|
986
|
} |
|
987
|
db_bind_int(&q, "$rid", rid); |
|
988
|
db_bind_text(&q, "$tagname", zTagName); |
|
989
|
rc = (SQLITE_ROW==db_step(&q)) ? db_column_int(&q, 0) : 0; |
|
990
|
db_reset(&q); |
|
991
|
return rc; |
|
992
|
} |
|
993
|
|