Fossil SCM

fossil-scm / src / manifest.c
Blame History Raw 3255 lines
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 cross link control files and
19
** manifests. The file is named "manifest.c" because it was
20
** original only used to parse manifests. Then later clusters
21
** and control files and wiki pages and tickets were added.
22
*/
23
#include "config.h"
24
#include "manifest.h"
25
#include <assert.h>
26
27
#if INTERFACE
28
/*
29
** Types of control files
30
*/
31
#define CFTYPE_ANY 0
32
#define CFTYPE_MANIFEST 1
33
#define CFTYPE_CLUSTER 2
34
#define CFTYPE_CONTROL 3
35
#define CFTYPE_WIKI 4
36
#define CFTYPE_TICKET 5
37
#define CFTYPE_ATTACHMENT 6
38
#define CFTYPE_EVENT 7
39
#define CFTYPE_FORUM 8
40
41
/*
42
** File permissions used by Fossil internally.
43
*/
44
#define PERM_REG 0 /* regular file */
45
#define PERM_EXE 1 /* executable */
46
#define PERM_LNK 2 /* symlink */
47
48
/*
49
** Flags for use with manifest_crosslink().
50
*/
51
#define MC_NONE 0 /* default handling */
52
#define MC_PERMIT_HOOKS 1 /* permit hooks to execute */
53
#define MC_NO_ERRORS 2 /* do not issue errors for a bad parse */
54
55
/*
56
** A single F-card within a manifest
57
*/
58
struct ManifestFile {
59
char *zName; /* Name of a file */
60
char *zUuid; /* Artifact hash for the file */
61
char *zPerm; /* File permissions */
62
char *zPrior; /* Prior name if the name was changed */
63
};
64
65
66
/*
67
** A parsed manifest or cluster.
68
*/
69
struct Manifest {
70
Blob content; /* The original content blob */
71
int type; /* Type of artifact. One of CFTYPE_xxxxx */
72
int rid; /* The blob-id for this manifest */
73
const char *zBaseline;/* Baseline manifest. The B card. */
74
Manifest *pBaseline; /* The actual baseline manifest */
75
char *zComment; /* Decoded comment. The C card. */
76
double rDate; /* Date and time from D card. 0.0 if no D card. */
77
char *zUser; /* Name of the user from the U card. */
78
char *zRepoCksum; /* MD5 checksum of the baseline content. R card. */
79
char *zWiki; /* Text of the wiki page. W card. */
80
char *zWikiTitle; /* Name of the wiki page. L card. */
81
char *zMimetype; /* Mime type of wiki or comment text. N card. */
82
char *zThreadTitle; /* The forum thread title. H card */
83
double rEventDate; /* Date of an event. E card. */
84
char *zEventId; /* Artifact hash for an event. E card. */
85
char *zTicketUuid; /* UUID for a ticket. K card. */
86
char *zAttachName; /* Filename of an attachment. A card. */
87
char *zAttachSrc; /* Artifact hash for document being attached. A card. */
88
char *zAttachTarget; /* Ticket or wiki that attachment applies to. A card */
89
char *zThreadRoot; /* Thread root artifact. G card */
90
char *zInReplyTo; /* Forum in-reply-to artifact. I card */
91
int nFile; /* Number of F cards */
92
int nFileAlloc; /* Slots allocated in aFile[] */
93
int iFile; /* Index of current file in iterator */
94
ManifestFile *aFile; /* One entry for each F-card */
95
int nParent; /* Number of parents. */
96
int nParentAlloc; /* Slots allocated in azParent[] */
97
char **azParent; /* Hashes of parents. One for each P card argument */
98
int nCherrypick; /* Number of entries in aCherrypick[] */
99
struct {
100
char *zCPTarget; /* Hash for cherry-picked version w/ +|- prefix */
101
char *zCPBase; /* Hash for cherry-pick baseline. NULL for singletons */
102
} *aCherrypick;
103
int nCChild; /* Number of cluster children */
104
int nCChildAlloc; /* Number of closts allocated in azCChild[] */
105
char **azCChild; /* Hashes of referenced objects in a cluster. M cards */
106
int nTag; /* Number of T Cards */
107
int nTagAlloc; /* Slots allocated in aTag[] */
108
struct TagType {
109
char *zName; /* Name of the tag */
110
char *zUuid; /* Hash of artifact that the tag is applied to */
111
char *zValue; /* Value if the tag is really a property */
112
} *aTag; /* One for each T card */
113
int nField; /* Number of J cards */
114
int nFieldAlloc; /* Slots allocated in aField[] */
115
struct {
116
char *zName; /* Key or field name */
117
char *zValue; /* Value of the field */
118
} *aField; /* One for each J card */
119
};
120
#endif
121
122
/*
123
** Allowed and required card types in each style of artifact
124
*/
125
static struct {
126
const char *zAllowed; /* Allowed cards. Human-readable */
127
const char *zRequired; /* Required cards. Human-readable */
128
} manifestCardTypes[] = {
129
/* Allowed Required */
130
/* CFTYPE_MANIFEST 1 */ { "BCDFNPQRTUZ", "DZ" },
131
/* Wants to be "CDUZ" ----^^^^
132
** but we must limit for historical compatibility */
133
/* CFTYPE_CLUSTER 2 */ { "MZ", "MZ" },
134
/* CFTYPE_CONTROL 3 */ { "DTUZ", "DTUZ" },
135
/* CFTYPE_WIKI 4 */ { "CDLNPUWZ", "DLUWZ" },
136
/* CFTYPE_TICKET 5 */ { "DJKUZ", "DJKUZ" },
137
/* CFTYPE_ATTACHMENT 6 */ { "ACDNUZ", "ADZ" },
138
/* CFTYPE_EVENT 7 */ { "CDENPTUWZ", "DEWZ" },
139
/* CFTYPE_FORUM 8 */ { "DGHINPUWZ", "DUWZ" },
140
};
141
142
/*
143
** Names of manifest types
144
*/
145
static const char *const azNameOfMType[] = {
146
"manifest",
147
"cluster",
148
"tag",
149
"wiki",
150
"ticket",
151
"attachment",
152
"technote",
153
"forum post"
154
};
155
156
/*
157
** A cache of parsed manifests. This reduces the number of
158
** calls to manifest_parse() when doing a rebuild.
159
*/
160
#define MX_MANIFEST_CACHE 6
161
static struct {
162
int nxAge;
163
int aAge[MX_MANIFEST_CACHE];
164
Manifest *apManifest[MX_MANIFEST_CACHE];
165
} manifestCache;
166
167
/*
168
** True if manifest_crosslink_begin() has been called but
169
** manifest_crosslink_end() is still pending.
170
*/
171
static int manifest_crosslink_busy = 0;
172
173
/*
174
** There are some triggers that need to fire whenever new content
175
** is added to the EVENT table, to make corresponding changes to the
176
** PENDING_ALERT and CHAT tables. These are done with TEMP triggers
177
** which are created as needed. The reasons for using TEMP triggers:
178
**
179
** * A small minority of invocations of Fossil need to use those triggers.
180
** So we save CPU cycles in the common case by not having to parse the
181
** trigger definition
182
**
183
** * We don't have to worry about dangling table references inside
184
** of triggers. For example, we can create a trigger that adds
185
** to the CHAT table. But an admin can still drop that CHAT table
186
** at any moment, since the trigger that refers to CHAT is a TEMP
187
** trigger and won't persist to cause problems.
188
**
189
** * Because TEMP triggers are defined by the specific version of the
190
** application that is running, we don't have to worry with legacy
191
** compatibility of the triggers.
192
**
193
** This boolean variable is set when the TEMP triggers for EVENT
194
** have been created.
195
*/
196
static int manifest_event_triggers_are_enabled = 0;
197
198
/*
199
** Clear the memory allocated in a manifest object
200
*/
201
void manifest_destroy(Manifest *p){
202
if( p ){
203
blob_reset(&p->content);
204
fossil_free(p->aFile);
205
fossil_free(p->azParent);
206
fossil_free(p->azCChild);
207
fossil_free(p->aTag);
208
fossil_free(p->aField);
209
fossil_free(p->aCherrypick);
210
if( p->pBaseline ) manifest_destroy(p->pBaseline);
211
memset(p, 0, sizeof(*p));
212
fossil_free(p);
213
}
214
}
215
216
/*
217
** Given a string of upper-case letters, compute a mask of the letters
218
** present. For example, "ABC" computes 0x0007. "DE" gives 0x0018".
219
*/
220
static unsigned int manifest_card_mask(const char *z){
221
unsigned int m = 0;
222
char c;
223
while( (c = *(z++))>='A' && c<='Z' ){
224
m |= 1 << (c - 'A');
225
}
226
return m;
227
}
228
229
/*
230
** Given an integer mask representing letters A-Z, return the
231
** letter which is the first bit set in the mask. Example:
232
** 0x03520 gives 'F' since the F-bit is the lowest.
233
*/
234
static char maskToType(unsigned int x){
235
char c = 'A';
236
if( x==0 ) return '?';
237
while( (x&1)==0 ){ x >>= 1; c++; }
238
return c;
239
}
240
241
/*
242
** Add an element to the manifest cache using LRU replacement.
243
*/
244
void manifest_cache_insert(Manifest *p){
245
while( p ){
246
int i;
247
Manifest *pBaseline = p->pBaseline;
248
p->pBaseline = 0;
249
for(i=0; i<MX_MANIFEST_CACHE; i++){
250
if( manifestCache.apManifest[i]==0 ) break;
251
}
252
if( i>=MX_MANIFEST_CACHE ){
253
int oldest = 0;
254
int oldestAge = manifestCache.aAge[0];
255
for(i=1; i<MX_MANIFEST_CACHE; i++){
256
if( manifestCache.aAge[i]<oldestAge ){
257
oldest = i;
258
oldestAge = manifestCache.aAge[i];
259
}
260
}
261
manifest_destroy(manifestCache.apManifest[oldest]);
262
i = oldest;
263
}
264
manifestCache.aAge[i] = ++manifestCache.nxAge;
265
manifestCache.apManifest[i] = p;
266
p = pBaseline;
267
}
268
}
269
270
/*
271
** Try to extract a line from the manifest cache. Return 1 if found.
272
** Return 0 if not found.
273
*/
274
static Manifest *manifest_cache_find(int rid){
275
int i;
276
Manifest *p;
277
for(i=0; i<MX_MANIFEST_CACHE; i++){
278
if( manifestCache.apManifest[i] && manifestCache.apManifest[i]->rid==rid ){
279
p = manifestCache.apManifest[i];
280
manifestCache.apManifest[i] = 0;
281
return p;
282
}
283
}
284
return 0;
285
}
286
287
/*
288
** Clear the manifest cache.
289
*/
290
void manifest_cache_clear(void){
291
int i;
292
for(i=0; i<MX_MANIFEST_CACHE; i++){
293
if( manifestCache.apManifest[i] ){
294
manifest_destroy(manifestCache.apManifest[i]);
295
}
296
}
297
memset(&manifestCache, 0, sizeof(manifestCache));
298
}
299
300
#ifdef FOSSIL_DONT_VERIFY_MANIFEST_MD5SUM
301
# define md5sum_init(X)
302
# define md5sum_step_text(X,Y)
303
#endif
304
305
/*
306
** Return true if z points to the first character after a blank line.
307
** Tolerate either \r\n or \n line endings.
308
*/
309
static int after_blank_line(const char *z){
310
if( z[-1]!='\n' ) return 0;
311
if( z[-2]=='\n' ) return 1;
312
if( z[-2]=='\r' && z[-3]=='\n' ) return 1;
313
return 0;
314
}
315
316
/*
317
** Remove the PGP signature from the artifact, if there is one.
318
*/
319
static void remove_pgp_signature(const char **pz, int *pn){
320
const char *z = *pz;
321
int n = *pn;
322
int i;
323
if( strncmp(z, "-----BEGIN PGP SIGNED MESSAGE-----", 34)==0 ) i = 34;
324
else if( strncmp(z, "-----BEGIN SSH SIGNED MESSAGE-----", 34)==0 ) i = 34;
325
else return;
326
for(; i<n && !after_blank_line(z+i); i++){}
327
if( i>=n ) return;
328
z += i;
329
n -= i;
330
*pz = z;
331
for(i=n-1; i>=0; i--){
332
if( z[i]=='\n' &&
333
(strncmp(&z[i],"\n-----BEGIN PGP SIGNATURE-----", 29)==0
334
|| strncmp(&z[i],"\n-----BEGIN SSH SIGNATURE-----", 29)==0 )){
335
n = i+1;
336
break;
337
}
338
}
339
*pn = n;
340
return;
341
}
342
343
/*
344
** Verify the Z-card checksum on the artifact, if there is such a
345
** checksum. Return 0 if there is no Z-card. Return 1 if the Z-card
346
** exists and is correct. Return 2 if the Z-card exists and has the wrong
347
** value.
348
**
349
** 0123456789 123456789 123456789 123456789
350
** Z aea84f4f863865a8d59d0384e4d2a41c
351
*/
352
static int verify_z_card(const char *z, int n, Blob *pErr){
353
const char *zHash;
354
if( n<35 ) return 0;
355
if( z[n-35]!='Z' || z[n-34]!=' ' ) return 0;
356
md5sum_init();
357
md5sum_step_text(z, n-35);
358
zHash = md5sum_finish(0);
359
if( memcmp(&z[n-33], zHash, 32)==0 ){
360
return 1;
361
}else{
362
if(pErr!=0){
363
blob_appendf(pErr, "incorrect Z-card cksum: expected %.32s", zHash);
364
}
365
return 2;
366
}
367
}
368
369
/*
370
** A structure used for rapid parsing of the Manifest file
371
*/
372
typedef struct ManifestText ManifestText;
373
struct ManifestText {
374
char *z; /* The first character of the next token */
375
char *zEnd; /* One character beyond the end of the manifest */
376
int atEol; /* True if z points to the start of a new line */
377
};
378
379
/*
380
** Return a pointer to the next token. The token is zero-terminated.
381
** Return NULL if there are no more tokens on the current line.
382
*/
383
static char *next_token(ManifestText *p, int *pLen){
384
char *zStart;
385
int n;
386
if( p->atEol ) return 0;
387
zStart = p->z;
388
n = strcspn(p->z, " \n");
389
p->atEol = p->z[n]=='\n';
390
p->z[n] = 0;
391
p->z += n+1;
392
if( pLen ) *pLen = n;
393
return zStart;
394
}
395
396
/*
397
** Return the card-type for the next card. Or, return 0 if there are no
398
** more cards or if we are not at the end of the current card.
399
*/
400
static char next_card(ManifestText *p){
401
char c;
402
if( !p->atEol || p->z>=p->zEnd ) return 0;
403
c = p->z[0];
404
if( p->z[1]==' ' ){
405
p->z += 2;
406
p->atEol = 0;
407
}else if( p->z[1]=='\n' ){
408
p->z += 2;
409
p->atEol = 1;
410
}else{
411
c = 0;
412
}
413
return c;
414
}
415
416
/*
417
** Shorthand for a control-artifact parsing error
418
*/
419
#define SYNTAX(T) {zErr=(T); goto manifest_syntax_error;}
420
421
/*
422
** A cache of manifest IDs which manifest_parse() has seen in this
423
** session.
424
*/
425
static Bag seenManifests = Bag_INIT;
426
/*
427
** Frees all memory owned by the manifest "has-seen" cache. Intended
428
** to be called only from the app's atexit() handler.
429
*/
430
void manifest_clear_cache(){
431
bag_clear(&seenManifests);
432
}
433
434
/*
435
** Parse a blob into a Manifest object. The Manifest object
436
** takes over the input blob and will free it when the
437
** Manifest object is freed. Zeros are inserted into the blob
438
** as string terminators so that blob should not be used again.
439
**
440
** Return a pointer to an allocated Manifest object if the content
441
** really is a structural artifact of some kind. The returned Manifest
442
** object needs to be freed by a subsequent call to manifest_destroy().
443
** Return NULL if there are syntax errors or if the input blob does
444
** not describe a valid structural artifact.
445
**
446
** This routine is strict about the format of a structural artifacts.
447
** The format must match exactly or else it is rejected. This
448
** rule minimizes the risk that a content artifact will be mistaken
449
** for a structural artifact simply because they look the same.
450
**
451
** The pContent is reset. If a pointer is returned, then pContent will
452
** be reset when the Manifest object is cleared. If NULL is
453
** returned then the Manifest object is cleared automatically
454
** and pContent is reset before the return.
455
**
456
** The entire input blob can be PGP clear-signed. The signature is ignored.
457
** The artifact consists of zero or more cards, one card per line.
458
** (Except: the content of the W card can extend of multiple lines.)
459
** Each card is divided into tokens by a single space character.
460
** The first token is a single upper-case letter which is the card type.
461
** The card type determines the other parameters to the card.
462
** Cards must occur in lexicographical order.
463
*/
464
Manifest *manifest_parse(Blob *pContent, int rid, Blob *pErr){
465
Manifest *p;
466
int i, lineNo=0;
467
ManifestText x;
468
char cPrevType = 0;
469
char cType;
470
char *z;
471
int n;
472
char *zUuid;
473
int sz = 0;
474
int isRepeat;
475
int nSelfTag = 0; /* Number of T cards referring to this manifest */
476
int nSimpleTag = 0; /* Number of T cards with "+" prefix */
477
const char *zErr = 0;
478
unsigned int m;
479
unsigned int seenCard = 0; /* Which card types have been seen */
480
char zErrBuf[100]; /* Write error messages here */
481
482
if( rid==0 ){
483
isRepeat = 1;
484
}else if( bag_find(&seenManifests, rid) ){
485
isRepeat = 1;
486
}else{
487
isRepeat = 0;
488
bag_insert(&seenManifests, rid);
489
}
490
491
/* Every structural artifact ends with a '\n' character. Exit early
492
** if that is not the case for this artifact.
493
*/
494
if( !isRepeat ) g.parseCnt[0]++;
495
z = blob_materialize(pContent);
496
n = blob_size(pContent);
497
if( n<=0 || z[n-1]!='\n' ){
498
blob_reset(pContent);
499
if(pErr!=0){
500
blob_appendf(pErr, "%s", n ? "not terminated with \\n" : "zero-length");
501
}
502
return 0;
503
}
504
505
/* Strip off the PGP signature if there is one.
506
*/
507
remove_pgp_signature((const char**)&z, &n);
508
509
/* Verify that the first few characters of the artifact look like
510
** a control artifact.
511
*/
512
if( n<10 || z[0]<'A' || z[0]>'Z' || z[1]!=' ' ){
513
blob_reset(pContent);
514
if(pErr!=0){
515
blob_appendf(pErr, "line 1 not recognized");
516
}
517
return 0;
518
}
519
/* Then verify the Z-card.
520
*/
521
#if 1
522
/* Disable this ***ONLY*** (ONLY!) when testing hand-written inputs
523
for card-related syntax errors. */
524
if( verify_z_card(z, n, pErr)==2 ){
525
blob_reset(pContent);
526
return 0;
527
}
528
#else
529
#warning ACHTUNG - z-card check is disabled for testing purposes.
530
if(0 && verify_z_card(NULL, 0, NULL)){
531
/*avoid unused static func error*/
532
}
533
#endif
534
535
/* Allocate a Manifest object to hold the parsed control artifact.
536
*/
537
p = fossil_malloc( sizeof(*p) );
538
memset(p, 0, sizeof(*p));
539
memcpy(&p->content, pContent, sizeof(p->content));
540
p->rid = rid;
541
blob_zero(pContent);
542
pContent = &p->content;
543
544
/* Begin parsing, card by card.
545
*/
546
x.z = z;
547
x.zEnd = &z[n];
548
x.atEol = 1;
549
while( (cType = next_card(&x))!=0 ){
550
if( cType<cPrevType ){
551
/* Cards must be in increasing order. However, out-of-order detection
552
** was broken prior to 2021-02-10 due to a bug. Furthermore, there
553
** was a bug in technote generation (prior to 2021-02-10) that caused
554
** the P card to occur before the N card. Hence, for historical
555
** compatibility, we do allow the N card of a technote to occur after
556
** the P card. See tickets 15d04de574383d61 and 5e67a7f4041a36ad.
557
*/
558
if( cType!='N' || cPrevType!='P' || p->zEventId==0 ){
559
SYNTAX("cards not in lexicographical order");
560
}
561
}
562
lineNo++;
563
if( cType<'A' || cType>'Z' ) SYNTAX("bad card type");
564
seenCard |= 1 << (cType-'A');
565
cPrevType = cType;
566
switch( cType ){
567
/*
568
** A <filename> <target> ?<source>?
569
**
570
** Identifies an attachment to either a wiki page or a ticket.
571
** <source> is the artifact that is the attachment. <source>
572
** is omitted to delete an attachment. <target> is the name of
573
** a wiki page or ticket to which that attachment is connected.
574
*/
575
case 'A': {
576
char *zName, *zTarget, *zSrc;
577
int nTarget = 0, nSrc = 0;
578
zName = next_token(&x, 0);
579
zTarget = next_token(&x, &nTarget);
580
zSrc = next_token(&x, &nSrc);
581
if( zName==0 || zTarget==0 ) goto manifest_syntax_error;
582
if( p->zAttachName!=0 ) goto manifest_syntax_error;
583
defossilize(zName);
584
if( !file_is_simple_pathname_nonstrict(zName) ){
585
SYNTAX("invalid filename on A-card");
586
}
587
defossilize(zTarget);
588
if( !hname_validate(zTarget,nTarget)
589
&& !wiki_name_is_wellformed((const unsigned char *)zTarget) ){
590
SYNTAX("invalid target on A-card");
591
}
592
if( zSrc && !hname_validate(zSrc,nSrc) ){
593
SYNTAX("invalid source on A-card");
594
}
595
p->zAttachName = (char*)file_tail(zName);
596
p->zAttachSrc = zSrc;
597
p->zAttachTarget = zTarget;
598
p->type = CFTYPE_ATTACHMENT;
599
break;
600
}
601
602
/*
603
** B <uuid>
604
**
605
** A B-line gives the artifact hash for the baseline of a delta-manifest.
606
*/
607
case 'B': {
608
if( p->zBaseline ) SYNTAX("more than one B-card");
609
p->zBaseline = next_token(&x, &sz);
610
if( p->zBaseline==0 ) SYNTAX("missing hash on B-card");
611
if( !hname_validate(p->zBaseline,sz) ){
612
SYNTAX("invalid hash on B-card");
613
}
614
p->type = CFTYPE_MANIFEST;
615
break;
616
}
617
618
619
/*
620
** C <comment>
621
**
622
** Comment text is fossil-encoded. There may be no more than
623
** one C line. C lines are required for manifests, are optional
624
** for Events and Attachments, and are disallowed on all other
625
** control files.
626
*/
627
case 'C': {
628
if( p->zComment!=0 ) SYNTAX("more than one C-card");
629
p->zComment = next_token(&x, 0);
630
if( p->zComment==0 ) SYNTAX("missing comment text on C-card");
631
defossilize(p->zComment);
632
break;
633
}
634
635
/*
636
** D <timestamp>
637
**
638
** The timestamp should be ISO 8601. YYYY-MM-DDtHH:MM:SS
639
** There can be no more than 1 D line. D lines are required
640
** for all control files except for clusters.
641
*/
642
case 'D': {
643
if( p->rDate>0.0 ) SYNTAX("more than one D-card");
644
p->rDate = db_double(0.0, "SELECT julianday(%Q)", next_token(&x,0));
645
if( p->rDate<=0.0 ) SYNTAX("cannot parse date on D-card");
646
break;
647
}
648
649
/*
650
** E <timestamp> <uuid>
651
**
652
** An "event" card that contains the timestamp of the event in the
653
** format YYYY-MM-DDtHH:MM:SS and a unique identifier for the event.
654
** The event timestamp is distinct from the D timestamp. The D
655
** timestamp is when the artifact was created whereas the E timestamp
656
** is when the specific event is said to occur.
657
*/
658
case 'E': {
659
if( p->rEventDate>0.0 ) SYNTAX("more than one E-card");
660
p->rEventDate = db_double(0.0,"SELECT julianday(%Q)", next_token(&x,0));
661
if( p->rEventDate<=0.0 ) SYNTAX("malformed date on E-card");
662
p->zEventId = next_token(&x, &sz);
663
if( p->zEventId==0 ) SYNTAX("missing hash on E-card");
664
if( !hname_validate(p->zEventId, sz) ){
665
SYNTAX("malformed hash on E-card");
666
}
667
p->type = CFTYPE_EVENT;
668
break;
669
}
670
671
/*
672
** F <filename> ?<uuid>? ?<permissions>? ?<old-name>?
673
**
674
** Identifies a file in a manifest. Multiple F lines are
675
** allowed in a manifest. F lines are not allowed in any
676
** other control file. The filename and old-name are fossil-encoded.
677
*/
678
case 'F': {
679
char *zName, *zPerm, *zPriorName;
680
zName = next_token(&x,0);
681
if( zName==0 ) SYNTAX("missing filename on F-card");
682
defossilize(zName);
683
if( !file_is_simple_pathname_nonstrict(zName) ){
684
SYNTAX("F-card filename is not a simple path");
685
}
686
zUuid = next_token(&x, &sz);
687
if( p->zBaseline==0 || zUuid!=0 ){
688
if( zUuid==0 ) SYNTAX("missing hash on F-card");
689
if( !hname_validate(zUuid,sz) ){
690
SYNTAX("F-card hash invalid");
691
}
692
}
693
zPerm = next_token(&x,0);
694
zPriorName = next_token(&x,0);
695
if( zPriorName ){
696
defossilize(zPriorName);
697
if( !file_is_simple_pathname_nonstrict(zPriorName) ){
698
SYNTAX("F-card old filename is not a simple path");
699
}
700
}
701
if( p->nFile>=p->nFileAlloc ){
702
p->nFileAlloc = p->nFileAlloc*2 + 10;
703
p->aFile = fossil_realloc(p->aFile,
704
p->nFileAlloc*sizeof(p->aFile[0]) );
705
}
706
i = p->nFile++;
707
if( i>0 && fossil_strcmp(p->aFile[i-1].zName, zName)>=0 ){
708
SYNTAX("incorrect F-card sort order");
709
}
710
if( file_is_reserved_name(zName,-1) ){
711
/* If reserved names leaked into historical manifests due to
712
** slack oversight by older versions of Fossil, simply ignore
713
** those files */
714
p->nFile--;
715
break;
716
}
717
p->aFile[i].zName = zName;
718
p->aFile[i].zUuid = zUuid;
719
p->aFile[i].zPerm = zPerm;
720
p->aFile[i].zPrior = zPriorName;
721
p->type = CFTYPE_MANIFEST;
722
break;
723
}
724
725
/*
726
** G <hash>
727
**
728
** A G-card identifies the initial root forum post for the thread
729
** of which this post is a part. Forum posts only.
730
*/
731
case 'G': {
732
if( p->zThreadRoot!=0 ) SYNTAX("more than one G-card");
733
p->zThreadRoot = next_token(&x, &sz);
734
if( p->zThreadRoot==0 ) SYNTAX("missing hash on G-card");
735
if( !hname_validate(p->zThreadRoot,sz) ){
736
SYNTAX("Invalid hash on G-card");
737
}
738
p->type = CFTYPE_FORUM;
739
break;
740
}
741
742
/*
743
** H <threadtitle>
744
**
745
** The title for a forum thread.
746
*/
747
case 'H': {
748
if( p->zThreadTitle!=0 ) SYNTAX("more than one H-card");
749
p->zThreadTitle = next_token(&x,0);
750
if( p->zThreadTitle==0 ) SYNTAX("missing title on H-card");
751
defossilize(p->zThreadTitle);
752
p->type = CFTYPE_FORUM;
753
break;
754
}
755
756
/*
757
** I <hash>
758
**
759
** A I-card identifies another forum post that the current forum post
760
** is in reply to.
761
*/
762
case 'I': {
763
if( p->zInReplyTo!=0 ) SYNTAX("more than one I-card");
764
p->zInReplyTo = next_token(&x, &sz);
765
if( p->zInReplyTo==0 ) SYNTAX("missing hash on I-card");
766
if( !hname_validate(p->zInReplyTo,sz) ){
767
SYNTAX("Invalid hash on I-card");
768
}
769
p->type = CFTYPE_FORUM;
770
break;
771
}
772
773
/*
774
** J <name> ?<value>?
775
**
776
** Specifies a name value pair for ticket. If the first character
777
** of <name> is "+" then the <value> is appended to any preexisting
778
** value. If <value> is omitted then it is understood to be an
779
** empty string.
780
*/
781
case 'J': {
782
char *zName, *zValue;
783
zName = next_token(&x,0);
784
zValue = next_token(&x,0);
785
if( zName==0 ) SYNTAX("name missing from J-card");
786
if( zValue==0 ) zValue = "";
787
defossilize(zValue);
788
if( p->nField>=p->nFieldAlloc ){
789
p->nFieldAlloc = p->nFieldAlloc*2 + 10;
790
p->aField = fossil_realloc(p->aField,
791
p->nFieldAlloc*sizeof(p->aField[0]) );
792
}
793
i = p->nField++;
794
p->aField[i].zName = zName;
795
p->aField[i].zValue = zValue;
796
if( i>0 && fossil_strcmp(p->aField[i-1].zName, zName)>=0 ){
797
SYNTAX("incorrect J-card sort order");
798
}
799
p->type = CFTYPE_TICKET;
800
break;
801
}
802
803
804
/*
805
** K <uuid>
806
**
807
** A K-line gives the UUID for the ticket which this control file
808
** is amending.
809
*/
810
case 'K': {
811
if( p->zTicketUuid!=0 ) SYNTAX("more than one K-card");
812
p->zTicketUuid = next_token(&x, &sz);
813
if( sz!=HNAME_LEN_SHA1 ) SYNTAX("K-card UUID is the wrong size");
814
if( !validate16(p->zTicketUuid, sz) ){
815
SYNTAX("invalid K-card UUID");
816
}
817
p->type = CFTYPE_TICKET;
818
break;
819
}
820
821
/*
822
** L <wikititle>
823
**
824
** The wiki page title is fossil-encoded. There may be no more than
825
** one L line.
826
*/
827
case 'L': {
828
if( p->zWikiTitle!=0 ) SYNTAX("more than one L-card");
829
p->zWikiTitle = next_token(&x,0);
830
if( p->zWikiTitle==0 ) SYNTAX("missing title on L-card");
831
defossilize(p->zWikiTitle);
832
if( !wiki_name_is_wellformed((const unsigned char *)p->zWikiTitle) ){
833
SYNTAX("L-card has malformed wiki name");
834
}
835
p->type = CFTYPE_WIKI;
836
break;
837
}
838
839
/*
840
** M <hash>
841
**
842
** An M-line identifies another artifact by its hash. M-lines
843
** occur in clusters only.
844
*/
845
case 'M': {
846
zUuid = next_token(&x, &sz);
847
if( zUuid==0 ) SYNTAX("missing hash on M-card");
848
if( !hname_validate(zUuid,sz) ){
849
SYNTAX("Invalid hash on M-card");
850
}
851
if( p->nCChild>=p->nCChildAlloc ){
852
p->nCChildAlloc = p->nCChildAlloc*2 + 10;
853
p->azCChild = fossil_realloc(p->azCChild
854
, p->nCChildAlloc*sizeof(p->azCChild[0]) );
855
}
856
i = p->nCChild++;
857
p->azCChild[i] = zUuid;
858
if( i>0 && fossil_strcmp(p->azCChild[i-1], zUuid)>=0 ){
859
SYNTAX("M-card in the wrong order");
860
}
861
p->type = CFTYPE_CLUSTER;
862
break;
863
}
864
865
/*
866
** N <uuid>
867
**
868
** An N-line identifies the mimetype of wiki or comment text.
869
*/
870
case 'N': {
871
if( p->zMimetype!=0 ) SYNTAX("more than one N-card");
872
p->zMimetype = next_token(&x,0);
873
if( p->zMimetype==0 ) SYNTAX("missing mimetype on N-card");
874
defossilize(p->zMimetype);
875
break;
876
}
877
878
/*
879
** P <uuid> ...
880
**
881
** Specify one or more other artifacts which are the parents of
882
** this artifact. The first parent is the primary parent. All
883
** others are parents by merge. Note that the initial empty
884
** check-in historically has an empty P-card, so empty P-cards
885
** must be accepted.
886
*/
887
case 'P': {
888
while( (zUuid = next_token(&x, &sz))!=0 ){
889
if( !hname_validate(zUuid, sz) ){
890
SYNTAX("invalid hash on P-card");
891
}
892
if( p->nParent>=p->nParentAlloc ){
893
p->nParentAlloc = p->nParentAlloc*2 + 5;
894
p->azParent = fossil_realloc(p->azParent,
895
p->nParentAlloc*sizeof(char*));
896
}
897
i = p->nParent++;
898
p->azParent[i] = zUuid;
899
}
900
break;
901
}
902
903
/*
904
** Q (+|-)<uuid> ?<uuid>?
905
**
906
** Specify one or a range of check-ins that are cherrypicked into
907
** this check-in ("+") or backed out of this check-in ("-").
908
*/
909
case 'Q': {
910
if( (zUuid=next_token(&x, &sz))==0 ) SYNTAX("missing hash on Q-card");
911
if( zUuid[0]!='+' && zUuid[0]!='-' ){
912
SYNTAX("Q-card does not begin with '+' or '-'");
913
}
914
if( !hname_validate(&zUuid[1], sz-1) ){
915
SYNTAX("invalid hash on Q-card");
916
}
917
n = p->nCherrypick;
918
p->nCherrypick++;
919
p->aCherrypick = fossil_realloc(p->aCherrypick,
920
p->nCherrypick*sizeof(p->aCherrypick[0]));
921
p->aCherrypick[n].zCPTarget = zUuid;
922
p->aCherrypick[n].zCPBase = zUuid = next_token(&x, &sz);
923
if( zUuid && !hname_validate(zUuid,sz) ){
924
SYNTAX("invalid second hash on Q-card");
925
}
926
p->type = CFTYPE_MANIFEST;
927
break;
928
}
929
930
/*
931
** R <md5sum>
932
**
933
** Specify the MD5 checksum over the name and content of all files
934
** in the manifest.
935
*/
936
case 'R': {
937
if( p->zRepoCksum!=0 ) SYNTAX("more than one R-card");
938
p->zRepoCksum = next_token(&x, &sz);
939
if( sz!=32 ) SYNTAX("wrong size cksum on R-card");
940
if( !validate16(p->zRepoCksum, 32) ) SYNTAX("malformed R-card cksum");
941
p->type = CFTYPE_MANIFEST;
942
break;
943
}
944
945
/*
946
** T (+|*|-)<tagname> <uuid> ?<value>?
947
**
948
** Create or cancel a tag or property. The tagname is fossil-encoded.
949
** The first character of the name must be either "+" to create a
950
** singleton tag, "*" to create a propagating tag, or "-" to create
951
** anti-tag that undoes a prior "+" or blocks propagation of a "*".
952
**
953
** The tag is applied to <uuid>. If <uuid> is "*" then the tag is
954
** applied to the current manifest. If <value> is provided then
955
** the tag is really a property with the given value.
956
**
957
** Tags are not allowed in clusters. Multiple T lines are allowed.
958
*/
959
case 'T': {
960
char *zName, *zValue;
961
zName = next_token(&x, 0);
962
if( zName==0 ) SYNTAX("missing name on T-card");
963
zUuid = next_token(&x, &sz);
964
if( zUuid==0 ) SYNTAX("missing artifact hash on T-card");
965
zValue = next_token(&x, 0);
966
if( zValue ) defossilize(zValue);
967
if( hname_validate(zUuid, sz) ){
968
/* A valid artifact hash */
969
}else if( sz==1 && zUuid[0]=='*' ){
970
zUuid = 0;
971
nSelfTag++;
972
}else{
973
SYNTAX("malformed artifact hash on T-card");
974
}
975
defossilize(zName);
976
if( zName[0]!='-' && zName[0]!='+' && zName[0]!='*' ){
977
SYNTAX("T-card name does not begin with '-', '+', or '*'");
978
}
979
if( zName[0]=='+' ) nSimpleTag++;
980
if( validate16(&zName[1], strlen(&zName[1])) ){
981
/* Do not allow tags whose names look like a hash */
982
SYNTAX("T-card name looks like a hexadecimal hash");
983
}
984
if( p->nTag>=p->nTagAlloc ){
985
p->nTagAlloc = p->nTagAlloc*2 + 10;
986
p->aTag = fossil_realloc(p->aTag, p->nTagAlloc*sizeof(p->aTag[0]) );
987
}
988
i = p->nTag++;
989
p->aTag[i].zName = zName;
990
p->aTag[i].zUuid = zUuid;
991
p->aTag[i].zValue = zValue;
992
if( i>0 ){
993
int c = fossil_strcmp(p->aTag[i-1].zName, zName);
994
if( c>0 || (c==0 && fossil_strcmp(p->aTag[i-1].zUuid, zUuid)>=0) ){
995
SYNTAX("T-card in the wrong order");
996
}
997
}
998
break;
999
}
1000
1001
/*
1002
** U ?<login>?
1003
**
1004
** Identify the user who created this control file by their
1005
** login. Only one U line is allowed. Prohibited in clusters.
1006
** If the user name is omitted, take that to be "anonymous".
1007
*/
1008
case 'U': {
1009
if( p->zUser!=0 ) SYNTAX("more than one U-card");
1010
p->zUser = next_token(&x, 0);
1011
if( p->zUser==0 || p->zUser[0]==0 ){
1012
p->zUser = "anonymous";
1013
}else{
1014
defossilize(p->zUser);
1015
}
1016
break;
1017
}
1018
1019
/*
1020
** W <size>
1021
**
1022
** The next <size> bytes of the file contain the text of the wiki
1023
** page. There is always an extra \n before the start of the next
1024
** record.
1025
*/
1026
case 'W': {
1027
char *zSize;
1028
unsigned size, oldsize, c;
1029
Blob wiki;
1030
zSize = next_token(&x, 0);
1031
if( zSize==0 ) SYNTAX("missing size on W-card");
1032
if( x.atEol==0 ) SYNTAX("no content after W-card");
1033
for(oldsize=size=0; (c = zSize[0])>='0' && c<='9'; zSize++){
1034
size = oldsize*10 + c - '0';
1035
if( size<oldsize ) SYNTAX("size overflow on W-card");
1036
oldsize = size;
1037
}
1038
if( p->zWiki!=0 ) SYNTAX("more than one W-card");
1039
blob_zero(&wiki);
1040
if( (&x.z[size+1])>=x.zEnd )SYNTAX("not enough content after W-card");
1041
p->zWiki = x.z;
1042
x.z += size;
1043
if( x.z[0]!='\n' ) SYNTAX("W-card content no \\n terminated");
1044
x.z[0] = 0;
1045
x.z++;
1046
break;
1047
}
1048
1049
1050
/*
1051
** Z <md5sum>
1052
**
1053
** MD5 checksum on this control file. The checksum is over all
1054
** lines (other than PGP-signature lines) prior to the current
1055
** line. This must be the last record.
1056
**
1057
** This card is required for all control file types except for
1058
** Manifest. It is not required for manifest only for historical
1059
** compatibility reasons.
1060
*/
1061
case 'Z': {
1062
zUuid = next_token(&x, &sz);
1063
if( sz!=32 ) SYNTAX("wrong size for Z-card cksum");
1064
if( !validate16(zUuid, 32) ) SYNTAX("malformed Z-card cksum");
1065
break;
1066
}
1067
default: {
1068
SYNTAX("unrecognized card");
1069
}
1070
}
1071
}
1072
if( x.z<x.zEnd ) SYNTAX("extra characters at end of card");
1073
1074
/* If the artifact type has not yet been determined, then compute
1075
** it now. */
1076
if( p->type==0 ){
1077
if( p->zComment!=0 || p->nFile>0 || p->nParent>0 ){
1078
p->type = CFTYPE_MANIFEST;
1079
}else{
1080
p->type = CFTYPE_CONTROL;
1081
}
1082
}
1083
1084
/* Verify that no disallowed cards are present for this artifact type */
1085
m = manifest_card_mask(manifestCardTypes[p->type-1].zAllowed);
1086
if( seenCard & ~m ){
1087
sqlite3_snprintf(sizeof(zErrBuf), zErrBuf, "%c-card in %s",
1088
maskToType(seenCard & ~m),
1089
azNameOfMType[p->type-1]);
1090
zErr = zErrBuf;
1091
goto manifest_syntax_error;
1092
}
1093
1094
/* Verify that all required cards are present for this artifact type */
1095
m = manifest_card_mask(manifestCardTypes[p->type-1].zRequired);
1096
if( ~seenCard & m ){
1097
sqlite3_snprintf(sizeof(zErrBuf), zErrBuf, "%c-card missing in %s",
1098
maskToType(~seenCard & m),
1099
azNameOfMType[p->type-1]);
1100
zErr = zErrBuf;
1101
goto manifest_syntax_error;
1102
}
1103
1104
/* Additional checks based on artifact type */
1105
switch( p->type ){
1106
case CFTYPE_CONTROL: {
1107
if( nSelfTag ) SYNTAX("self-referential T-card in control artifact");
1108
break;
1109
}
1110
case CFTYPE_EVENT: {
1111
if( p->nTag!=nSelfTag ){
1112
SYNTAX("non-self-referential T-card in technote");
1113
}
1114
if( p->nTag!=nSimpleTag ){
1115
SYNTAX("T-card with '*' or '-' in technote");
1116
}
1117
break;
1118
}
1119
case CFTYPE_FORUM: {
1120
if( p->zThreadTitle && p->zInReplyTo ){
1121
SYNTAX("cannot have I-card and H-card in a forum post");
1122
}
1123
if( p->nParent>1 ) SYNTAX("too many arguments to P-card");
1124
break;
1125
}
1126
}
1127
1128
md5sum_init();
1129
if( !isRepeat ) g.parseCnt[p->type]++;
1130
return p;
1131
1132
manifest_syntax_error:
1133
if(pErr!=0){
1134
char *zUuid = rid>0 ? rid_to_uuid(rid) : 0;
1135
if( zUuid ){
1136
blob_appendf(pErr, "artifact [%s] ", zUuid);
1137
fossil_free(zUuid);
1138
}
1139
if( zErr ){
1140
blob_appendf(pErr, "line %d: %s", lineNo, zErr);
1141
}else{
1142
blob_appendf(pErr, "unknown error on line %d", lineNo);
1143
}
1144
}
1145
md5sum_init();
1146
manifest_destroy(p);
1147
return 0;
1148
}
1149
1150
/*
1151
** Get a manifest given the rid for the control artifact. Return
1152
** a pointer to the manifest on success or NULL if there is a failure.
1153
*/
1154
Manifest *manifest_get(int rid, int cfType, Blob *pErr){
1155
Blob content;
1156
Manifest *p;
1157
if( !rid ) return 0;
1158
p = manifest_cache_find(rid);
1159
if( p ){
1160
if( cfType!=CFTYPE_ANY && cfType!=p->type ){
1161
manifest_cache_insert(p);
1162
p = 0;
1163
}
1164
return p;
1165
}
1166
content_get(rid, &content);
1167
p = manifest_parse(&content, rid, pErr);
1168
if( p && cfType!=CFTYPE_ANY && cfType!=p->type ){
1169
manifest_destroy(p);
1170
p = 0;
1171
}
1172
return p;
1173
}
1174
1175
/*
1176
** Given a check-in name, load and parse the manifest for that check-in.
1177
** Throw a fatal error if anything goes wrong.
1178
*/
1179
Manifest *manifest_get_by_name(const char *zName, int *pRid){
1180
int rid;
1181
Manifest *p;
1182
1183
rid = name_to_typed_rid(zName, "ci");
1184
if( !is_a_version(rid) ){
1185
fossil_fatal("no such check-in: %s", zName);
1186
}
1187
if( pRid ) *pRid = rid;
1188
p = manifest_get(rid, CFTYPE_MANIFEST, 0);
1189
if( p==0 ){
1190
fossil_fatal("cannot parse manifest for check-in: %s", zName);
1191
}
1192
return p;
1193
}
1194
1195
/*
1196
** The input blob is text that may or may not be a valid Fossil
1197
** control artifact of some kind. This routine returns true if
1198
** the input is a well-formed control artifact and false if it
1199
** is not.
1200
**
1201
** This routine is optimized to return false quickly and with minimal
1202
** work in the common case where the input is some random file.
1203
*/
1204
int manifest_is_well_formed(const char *zIn, int nIn){
1205
int i;
1206
int iRes;
1207
Manifest *pManifest;
1208
Blob copy, errmsg;
1209
remove_pgp_signature(&zIn, &nIn);
1210
1211
/* Check to see that the file begins with a "card" */
1212
if( nIn<3 ) return 0;
1213
if( zIn[0]<'A' || zIn[0]>'M' || zIn[1]!=' ' ) return 0;
1214
1215
/* Check to see that the first card is followed by one more card */
1216
for(i=2; i<nIn && zIn[i]!='\n'; i++){}
1217
if( i>=nIn-3 ) return 0;
1218
i++;
1219
if( !fossil_isupper(zIn[i]) || zIn[i]<zIn[0] || zIn[i+1]!=' ' ) return 0;
1220
1221
/* The checks above will eliminate most random inputs. If these
1222
** quick checks pass, then we could be dealing with a well-formed
1223
** control artifact. Make a copy, and run it through the official
1224
** artifact parser. This is the slow path, but it is rarely taken.
1225
*/
1226
blob_init(&copy, 0, 0);
1227
blob_init(&errmsg, 0, 0);
1228
blob_append(&copy, zIn, nIn);
1229
pManifest = manifest_parse(&copy, 0, &errmsg);
1230
iRes = pManifest!=0;
1231
manifest_destroy(pManifest);
1232
blob_reset(&errmsg);
1233
return iRes;
1234
}
1235
1236
/*
1237
** COMMAND: test-parse-manifest
1238
**
1239
** Usage: %fossil test-parse-manifest FILENAME ?N?
1240
**
1241
** Parse the manifest(s) given on the command-line and report any
1242
** errors. If the N argument is given, run the parsing N times.
1243
*/
1244
void manifest_test_parse_cmd(void){
1245
Manifest *p;
1246
Blob b;
1247
int i;
1248
int n = 1;
1249
int isWF;
1250
db_find_and_open_repository(OPEN_SUBSTITUTE|OPEN_OK_NOT_FOUND,0);
1251
verify_all_options();
1252
if( g.argc!=3 && g.argc!=4 ){
1253
usage("FILENAME");
1254
}
1255
blob_read_from_file(&b, g.argv[2], ExtFILE);
1256
if( g.argc>3 ) n = atoi(g.argv[3]);
1257
isWF = manifest_is_well_formed(blob_buffer(&b), blob_size(&b));
1258
fossil_print("manifest_is_well_formed() reports the input %s\n",
1259
isWF ? "is ok" : "contains errors");
1260
for(i=0; i<n; i++){
1261
Blob b2;
1262
Blob err;
1263
blob_copy(&b2, &b);
1264
blob_zero(&err);
1265
p = manifest_parse(&b2, 0, &err);
1266
if( p==0 ){
1267
fossil_print("ERROR: %s\n", blob_str(&err));
1268
}else if( i==0 || (n==2 && i==1) ){
1269
fossil_print("manifest_parse() worked\n");
1270
}else if( i==n-1 ){
1271
fossil_print("manifest_parse() worked %d more times\n", n-1);
1272
}
1273
if( (p==0 && isWF) || (p!=0 && !isWF) ){
1274
fossil_print("ERROR: manifest_is_well_formed() and "
1275
"manifest_parse() disagree!\n");
1276
}
1277
blob_reset(&err);
1278
manifest_destroy(p);
1279
}
1280
blob_reset(&b);
1281
}
1282
1283
/*
1284
** COMMAND: test-parse-all-blobs
1285
**
1286
** Usage: %fossil test-parse-all-blobs ?OPTIONS?
1287
**
1288
** Parse all entries in the BLOB table that are believed to be non-data
1289
** artifacts and report any errors. Run this test command on historical
1290
** repositories after making any changes to the manifest_parse()
1291
** implementation to confirm that the changes did not break anything.
1292
**
1293
** Options:
1294
** --limit N Parse no more than N artifacts before stopping
1295
** --wellformed Use all BLOB table entries as input, not just
1296
** those entries that are believed to be valid
1297
** artifacts, and verify that the result the
1298
** manifest_is_well_formed() agrees with the
1299
** result of manifest_parse().
1300
*/
1301
void manifest_test_parse_all_blobs_cmd(void){
1302
Manifest *p;
1303
Blob err;
1304
Stmt q;
1305
int nTest = 0;
1306
int nErr = 0;
1307
int N = 1000000000;
1308
int bWellFormed;
1309
const char *z;
1310
db_find_and_open_repository(0, 0);
1311
z = find_option("limit", 0, 1);
1312
if( z ) N = atoi(z);
1313
bWellFormed = find_option("wellformed",0,0)!=0;
1314
verify_all_options();
1315
if( bWellFormed ){
1316
db_prepare(&q, "SELECT rid FROM blob ORDER BY rid");
1317
}else{
1318
db_prepare(&q, "SELECT DISTINCT objid FROM EVENT ORDER BY objid");
1319
}
1320
while( (N--)>0 && db_step(&q)==SQLITE_ROW ){
1321
int id = db_column_int(&q,0);
1322
fossil_print("Checking %d \r", id);
1323
nTest++;
1324
fflush(stdout);
1325
blob_init(&err, 0, 0);
1326
if( bWellFormed ){
1327
Blob content;
1328
int isWF;
1329
content_get(id, &content);
1330
isWF = manifest_is_well_formed(blob_buffer(&content),blob_size(&content));
1331
p = manifest_parse(&content, id, &err);
1332
if( isWF && p==0 ){
1333
fossil_print("%d ERROR: manifest_is_well_formed() reported true "
1334
"but manifest_parse() reports an error: %s\n",
1335
id, blob_str(&err));
1336
nErr++;
1337
}else if( !isWF && p!=0 ){
1338
fossil_print("%d ERROR: manifest_is_well_formed() reported false "
1339
"but manifest_parse() found nothing wrong.\n", id);
1340
nErr++;
1341
}
1342
}else{
1343
p = manifest_get(id, CFTYPE_ANY, &err);
1344
if( p==0 ){
1345
fossil_print("%d ERROR: %s\n", id, blob_str(&err));
1346
nErr++;
1347
}
1348
}
1349
blob_reset(&err);
1350
manifest_destroy(p);
1351
}
1352
db_finalize(&q);
1353
fossil_print("%d tests with %d errors\n", nTest, nErr);
1354
}
1355
1356
/*
1357
** Fetch the baseline associated with the delta-manifest p.
1358
** Return 0 on success. If unable to parse the baseline,
1359
** throw an error. If the baseline is a manifest, throw an
1360
** error if throwError is true, or record that p is an orphan
1361
** and return 1 if throwError is false.
1362
*/
1363
static int fetch_baseline(Manifest *p, int throwError){
1364
if( p->zBaseline!=0 && p->pBaseline==0 ){
1365
int rid = uuid_to_rid(p->zBaseline, 1);
1366
p->pBaseline = manifest_get(rid, CFTYPE_MANIFEST, 0);
1367
if( p->pBaseline==0 ){
1368
if( !throwError ){
1369
db_multi_exec(
1370
"INSERT OR IGNORE INTO orphan(rid, baseline) VALUES(%d,%d)",
1371
p->rid, rid
1372
);
1373
return 1;
1374
}
1375
fossil_fatal("cannot access baseline manifest %S", p->zBaseline);
1376
}
1377
}
1378
return 0;
1379
}
1380
1381
/*
1382
** Rewind a manifest-file iterator back to the beginning of the manifest.
1383
*/
1384
void manifest_file_rewind(Manifest *p){
1385
p->iFile = 0;
1386
fetch_baseline(p, 1);
1387
if( p->pBaseline ){
1388
p->pBaseline->iFile = 0;
1389
}
1390
}
1391
1392
/*
1393
** Advance to the next manifest-file.
1394
**
1395
** Return NULL for end-of-records or if there is an error. If an error
1396
** occurs and pErr!=0 then store 1 in *pErr.
1397
*/
1398
ManifestFile *manifest_file_next(
1399
Manifest *p,
1400
int *pErr
1401
){
1402
ManifestFile *pOut = 0;
1403
if( pErr ) *pErr = 0;
1404
if( p->pBaseline==0 ){
1405
/* Manifest p is a baseline-manifest. Just scan down the list
1406
** of files. */
1407
if( p->iFile<p->nFile ) pOut = &p->aFile[p->iFile++];
1408
}else{
1409
/* Manifest p is a delta-manifest. Scan the baseline but amend the
1410
** file list in the baseline with changes described by p.
1411
*/
1412
Manifest *pB = p->pBaseline;
1413
int cmp;
1414
while(1){
1415
if( pB->iFile>=pB->nFile ){
1416
/* We have used all entries out of the baseline. Return the next
1417
** entry from the delta. */
1418
if( p->iFile<p->nFile ) pOut = &p->aFile[p->iFile++];
1419
break;
1420
}else if( p->iFile>=p->nFile ){
1421
/* We have used all entries from the delta. Return the next
1422
** entry from the baseline. */
1423
if( pB->iFile<pB->nFile ) pOut = &pB->aFile[pB->iFile++];
1424
break;
1425
}else if( (cmp = fossil_strcmp(pB->aFile[pB->iFile].zName,
1426
p->aFile[p->iFile].zName)) < 0 ){
1427
/* The next baseline entry comes before the next delta entry.
1428
** So return the baseline entry. */
1429
pOut = &pB->aFile[pB->iFile++];
1430
break;
1431
}else if( cmp>0 ){
1432
/* The next delta entry comes before the next baseline
1433
** entry so return the delta entry */
1434
pOut = &p->aFile[p->iFile++];
1435
break;
1436
}else if( p->aFile[p->iFile].zUuid ){
1437
/* The next delta entry is a replacement for the next baseline
1438
** entry. Skip the baseline entry and return the delta entry */
1439
pB->iFile++;
1440
pOut = &p->aFile[p->iFile++];
1441
break;
1442
}else{
1443
/* The next delta entry is a delete of the next baseline
1444
** entry. Skip them both. Repeat the loop to find the next
1445
** non-delete entry. */
1446
pB->iFile++;
1447
p->iFile++;
1448
continue;
1449
}
1450
}
1451
}
1452
return pOut;
1453
}
1454
1455
/*
1456
** Translate a filename into a filename-id (fnid). Create a new fnid
1457
** if no previously exists.
1458
*/
1459
static int filename_to_fnid(const char *zFilename){
1460
static Stmt q1, s1;
1461
int fnid;
1462
db_static_prepare(&q1, "SELECT fnid FROM filename WHERE name=:fn");
1463
db_bind_text(&q1, ":fn", zFilename);
1464
fnid = 0;
1465
if( db_step(&q1)==SQLITE_ROW ){
1466
fnid = db_column_int(&q1, 0);
1467
}
1468
db_reset(&q1);
1469
if( fnid==0 ){
1470
db_static_prepare(&s1, "INSERT INTO filename(name) VALUES(:fn)");
1471
db_bind_text(&s1, ":fn", zFilename);
1472
db_exec(&s1);
1473
fnid = db_last_insert_rowid();
1474
}
1475
return fnid;
1476
}
1477
1478
/*
1479
** Compute an appropriate mlink.mperm integer for the permission string
1480
** of a file.
1481
*/
1482
int manifest_file_mperm(const ManifestFile *pFile){
1483
int mperm = PERM_REG;
1484
if( pFile && pFile->zPerm){
1485
if( strstr(pFile->zPerm,"x")!=0 ){
1486
mperm = PERM_EXE;
1487
}else if( strstr(pFile->zPerm,"l")!=0 ){
1488
mperm = PERM_LNK;
1489
}
1490
}
1491
return mperm;
1492
}
1493
1494
/*
1495
** Add a single entry to the mlink table. Also add the filename to
1496
** the filename table if it is not there already.
1497
**
1498
** An mlink entry is always created if isPrimary is true. But if
1499
** isPrimary is false (meaning that pmid is a merge parent of mid)
1500
** then the mlink entry is only created if there is already an mlink
1501
** from primary parent for the same file.
1502
*/
1503
static void add_one_mlink(
1504
int pmid, /* The parent manifest */
1505
const char *zFromUuid, /* Artifact hash for content in parent */
1506
int mid, /* The record ID of the manifest */
1507
const char *zToUuid, /* artifact hash for content in child */
1508
const char *zFilename, /* Filename */
1509
const char *zPrior, /* Previous filename. NULL if unchanged */
1510
int isPublic, /* True if mid is not a private manifest */
1511
int isPrimary, /* pmid is the primary parent of mid */
1512
int mperm /* 1: exec, 2: symlink */
1513
){
1514
int fnid, pfnid, pid, fid;
1515
int doInsert;
1516
static Stmt s1, s2;
1517
1518
fnid = filename_to_fnid(zFilename);
1519
if( zPrior==0 ){
1520
pfnid = 0;
1521
}else{
1522
pfnid = filename_to_fnid(zPrior);
1523
}
1524
if( zFromUuid==0 || zFromUuid[0]==0 ){
1525
pid = 0;
1526
}else{
1527
pid = uuid_to_rid(zFromUuid, 1);
1528
}
1529
if( zToUuid==0 || zToUuid[0]==0 ){
1530
fid = 0;
1531
}else{
1532
fid = uuid_to_rid(zToUuid, 1);
1533
if( isPublic ) content_make_public(fid);
1534
}
1535
if( isPrimary ){
1536
doInsert = 1;
1537
}else{
1538
db_static_prepare(&s2,
1539
"SELECT 1 FROM mlink WHERE mid=:m AND fnid=:n AND NOT isaux"
1540
);
1541
db_bind_int(&s2, ":m", mid);
1542
db_bind_int(&s2, ":n", fnid);
1543
doInsert = db_step(&s2)==SQLITE_ROW;
1544
db_reset(&s2);
1545
}
1546
if( doInsert ){
1547
db_static_prepare(&s1,
1548
"INSERT INTO mlink(mid,fid,pmid,pid,fnid,pfnid,mperm,isaux)"
1549
"VALUES(:m,:f,:pm,:p,:n,:pfn,:mp,:isaux)"
1550
);
1551
db_bind_int(&s1, ":m", mid);
1552
db_bind_int(&s1, ":f", fid);
1553
db_bind_int(&s1, ":pm", pmid);
1554
db_bind_int(&s1, ":p", pid);
1555
db_bind_int(&s1, ":n", fnid);
1556
db_bind_int(&s1, ":pfn", pfnid);
1557
db_bind_int(&s1, ":mp", mperm);
1558
db_bind_int(&s1, ":isaux", isPrimary==0);
1559
db_exec(&s1);
1560
}
1561
if( pid && fid ){
1562
content_deltify(pid, &fid, 1, 0);
1563
}
1564
}
1565
1566
/*
1567
** Do a binary search to find a file in the p->aFile[] array.
1568
**
1569
** As an optimization, guess that the file we seek is at index p->iFile.
1570
** That will usually be the case. If it is not found there, then do the
1571
** actual binary search.
1572
**
1573
** Update p->iFile to be the index of the file that is found.
1574
*/
1575
static ManifestFile *manifest_file_seek_base(
1576
Manifest *p, /* Manifest to search */
1577
const char *zName, /* Name of the file we are looking for */
1578
int bBest /* 0: exact match only. 1: closest match */
1579
){
1580
int lwr, upr;
1581
int c;
1582
int i;
1583
if( p->aFile==0 ){
1584
return 0;
1585
}
1586
lwr = 0;
1587
upr = p->nFile - 1;
1588
if( p->iFile>=lwr && p->iFile<upr ){
1589
c = fossil_strcmp(p->aFile[p->iFile+1].zName, zName);
1590
if( c==0 ){
1591
return &p->aFile[++p->iFile];
1592
}else if( c>0 ){
1593
upr = p->iFile;
1594
}else{
1595
lwr = p->iFile+1;
1596
}
1597
}
1598
while( lwr<=upr ){
1599
i = (lwr+upr)/2;
1600
c = fossil_strcmp(p->aFile[i].zName, zName);
1601
if( c<0 ){
1602
lwr = i+1;
1603
}else if( c>0 ){
1604
upr = i-1;
1605
}else{
1606
p->iFile = i;
1607
return &p->aFile[i];
1608
}
1609
}
1610
if( bBest ){
1611
if( lwr>=p->nFile ) lwr = p->nFile-1;
1612
i = (int)strlen(zName);
1613
if( strncmp(zName, p->aFile[lwr].zName, i)==0 ) return &p->aFile[lwr];
1614
}
1615
return 0;
1616
}
1617
1618
/*
1619
** Locate a file named zName in the aFile[] array of the given manifest.
1620
** Return a pointer to the appropriate ManifestFile object. Return NULL
1621
** if not found.
1622
**
1623
** This routine works even if p is a delta-manifest. The pointer
1624
** returned might be to the baseline.
1625
**
1626
** We assume that filenames are in sorted order and use a binary search.
1627
*/
1628
ManifestFile *manifest_file_seek(Manifest *p, const char *zName, int bBest){
1629
ManifestFile *pFile;
1630
1631
pFile = manifest_file_seek_base(p, zName, p->zBaseline ? 0 : bBest);
1632
if( pFile && pFile->zUuid==0 ) return 0;
1633
if( pFile==0 && p->zBaseline ){
1634
fetch_baseline(p, 1);
1635
pFile = manifest_file_seek_base(p->pBaseline, zName,bBest);
1636
}
1637
return pFile;
1638
}
1639
1640
/*
1641
** Look for a file in a manifest, taking the case-sensitive option
1642
** into account. If case-sensitive is off, then files in any case
1643
** will match.
1644
*/
1645
ManifestFile *manifest_file_find(Manifest *p, const char *zName){
1646
int i;
1647
Manifest *pBase;
1648
if( filenames_are_case_sensitive() ){
1649
return manifest_file_seek(p, zName, 0);
1650
}
1651
for(i=0; i<p->nFile; i++){
1652
if( fossil_stricmp(zName, p->aFile[i].zName)==0 ){
1653
return &p->aFile[i];
1654
}
1655
}
1656
if( p->zBaseline==0 ) return 0;
1657
fetch_baseline(p, 1);
1658
pBase = p->pBaseline;
1659
if( pBase==0 ) return 0;
1660
for(i=0; i<pBase->nFile; i++){
1661
if( fossil_stricmp(zName, pBase->aFile[i].zName)==0 ){
1662
return &pBase->aFile[i];
1663
}
1664
}
1665
return 0;
1666
}
1667
1668
/*
1669
** Add mlink table entries associated with manifest cid, pChild. The
1670
** parent manifest is pid, pParent. One of either pChild or pParent
1671
** will be NULL and it will be computed based on cid/pid.
1672
**
1673
** A single mlink entry is added for every file that changed content,
1674
** name, and/or permissions going from pid to cid.
1675
**
1676
** Deleted files have mlink.fid=0.
1677
** Added files have mlink.pid=0.
1678
** File added by merge have mlink.pid=-1
1679
** Edited files have both mlink.pid!=0 and mlink.fid!=0
1680
**
1681
** Many mlink entries for merge parents will only be added if another mlink
1682
** entry already exists for the same file from the primary parent. Therefore,
1683
** to ensure that all merge-parent mlink entries are properly created:
1684
**
1685
** (1) Make this routine a no-op if pParent is a merge parent and the
1686
** primary parent is a phantom.
1687
** (2) Invoke this routine recursively for merge-parents if pParent is the
1688
** primary parent.
1689
*/
1690
static void add_mlink(
1691
int pmid, Manifest *pParent, /* Parent check-in */
1692
int mid, Manifest *pChild, /* The child check-in */
1693
int isPrim /* TRUE if pmid is the primary parent of mid */
1694
){
1695
Blob otherContent;
1696
int otherRid;
1697
int i, rc;
1698
ManifestFile *pChildFile, *pParentFile;
1699
Manifest **ppOther;
1700
static Stmt eq;
1701
int isPublic; /* True if pChild is non-private */
1702
1703
/* If mlink table entires are already exist for the pmid-to-mid transition,
1704
** then abort early doing no work.
1705
*/
1706
db_static_prepare(&eq, "SELECT 1 FROM mlink WHERE mid=:mid AND pmid=:pmid");
1707
db_bind_int(&eq, ":mid", mid);
1708
db_bind_int(&eq, ":pmid", pmid);
1709
rc = db_step(&eq);
1710
db_reset(&eq);
1711
if( rc==SQLITE_ROW ) return;
1712
1713
/* Compute the value of the missing pParent or pChild parameter.
1714
** Fetch the baseline check-ins for both.
1715
*/
1716
assert( pParent==0 || pChild==0 );
1717
if( pParent==0 ){
1718
ppOther = &pParent;
1719
otherRid = pmid;
1720
}else{
1721
ppOther = &pChild;
1722
otherRid = mid;
1723
}
1724
if( (*ppOther = manifest_cache_find(otherRid))==0 ){
1725
content_get(otherRid, &otherContent);
1726
if( blob_size(&otherContent)==0 ) return;
1727
*ppOther = manifest_parse(&otherContent, otherRid, 0);
1728
if( *ppOther==0 ) return;
1729
}
1730
if( fetch_baseline(pParent, 0) || fetch_baseline(pChild, 0) ){
1731
manifest_destroy(*ppOther);
1732
return;
1733
}
1734
isPublic = !content_is_private(mid);
1735
1736
/* If pParent is not the primary parent of pChild, and the primary
1737
** parent of pChild is a phantom, then abort this routine without
1738
** doing any work. The mlink entries will be computed when the
1739
** primary parent dephantomizes.
1740
*/
1741
if( !isPrim && otherRid==mid
1742
&& !db_exists("SELECT 1 FROM blob WHERE uuid=%Q AND size>0",
1743
pChild->azParent[0])
1744
){
1745
manifest_cache_insert(*ppOther);
1746
return;
1747
}
1748
1749
/* Try to make the parent manifest a delta from the child, if that
1750
** is an appropriate thing to do. For a new baseline, make the
1751
** previous baseline a delta from the current baseline.
1752
*/
1753
if( (pParent->zBaseline==0)==(pChild->zBaseline==0) ){
1754
content_deltify(pmid, &mid, 1, 0);
1755
}else if( pChild->zBaseline==0 && pParent->zBaseline!=0 ){
1756
content_deltify(pParent->pBaseline->rid, &mid, 1, 0);
1757
}
1758
1759
/* Remember all children less than a few seconds younger than their parent,
1760
** as we might want to fudge the times for those children.
1761
*/
1762
if( pChild->rDate<pParent->rDate+AGE_FUDGE_WINDOW
1763
&& manifest_crosslink_busy
1764
){
1765
db_multi_exec(
1766
"INSERT OR REPLACE INTO time_fudge VALUES(%d, %.17g, %d, %.17g);",
1767
pParent->rid, pParent->rDate, pChild->rid, pChild->rDate
1768
);
1769
}
1770
1771
/* First look at all files in pChild, ignoring its baseline. This
1772
** is where most of the changes will be found.
1773
*/
1774
for(i=0, pChildFile=pChild->aFile; i<pChild->nFile; i++, pChildFile++){
1775
int mperm = manifest_file_mperm(pChildFile);
1776
if( pChildFile->zPrior ){
1777
pParentFile = manifest_file_seek(pParent, pChildFile->zPrior, 0);
1778
if( pParentFile ){
1779
/* File with name change */
1780
add_one_mlink(pmid, pParentFile->zUuid, mid, pChildFile->zUuid,
1781
pChildFile->zName, pChildFile->zPrior,
1782
isPublic, isPrim, mperm);
1783
}else{
1784
/* File name changed, but the old name is not found in the parent!
1785
** Treat this like a new file. */
1786
add_one_mlink(pmid, 0, mid, pChildFile->zUuid, pChildFile->zName, 0,
1787
isPublic, isPrim, mperm);
1788
}
1789
}else{
1790
pParentFile = manifest_file_seek(pParent, pChildFile->zName, 0);
1791
if( pParentFile==0 ){
1792
if( pChildFile->zUuid ){
1793
/* A new file */
1794
add_one_mlink(pmid, 0, mid, pChildFile->zUuid, pChildFile->zName, 0,
1795
isPublic, isPrim, mperm);
1796
}
1797
}else if( fossil_strcmp(pChildFile->zUuid, pParentFile->zUuid)!=0
1798
|| manifest_file_mperm(pParentFile)!=mperm ){
1799
/* Changes in file content or permissions */
1800
add_one_mlink(pmid, pParentFile->zUuid, mid, pChildFile->zUuid,
1801
pChildFile->zName, 0, isPublic, isPrim, mperm);
1802
}
1803
}
1804
}
1805
if( pParent->zBaseline && pChild->zBaseline ){
1806
/* Both parent and child are delta manifests. Look for files that
1807
** are deleted or modified in the parent but which reappear or revert
1808
** to baseline in the child and show such files as being added or changed
1809
** in the child. */
1810
for(i=0, pParentFile=pParent->aFile; i<pParent->nFile; i++, pParentFile++){
1811
if( pParentFile->zUuid ){
1812
pChildFile = manifest_file_seek_base(pChild, pParentFile->zName, 0);
1813
if( pChildFile==0 ){
1814
/* The child file reverts to baseline. Show this as a change */
1815
pChildFile = manifest_file_seek(pChild, pParentFile->zName, 0);
1816
if( pChildFile ){
1817
add_one_mlink(pmid, pParentFile->zUuid, mid, pChildFile->zUuid,
1818
pChildFile->zName, 0, isPublic, isPrim,
1819
manifest_file_mperm(pChildFile));
1820
}
1821
}
1822
}else{
1823
pChildFile = manifest_file_seek(pChild, pParentFile->zName, 0);
1824
if( pChildFile ){
1825
/* File resurrected in the child after having been deleted in
1826
** the parent. Show this as an added file. */
1827
add_one_mlink(pmid, 0, mid, pChildFile->zUuid, pChildFile->zName, 0,
1828
isPublic, isPrim, manifest_file_mperm(pChildFile));
1829
}
1830
}
1831
}
1832
}else if( pChild->zBaseline==0 ){
1833
/* pChild is a baseline. Look for files that are present in pParent
1834
** but are missing from pChild and mark them as having been deleted. */
1835
manifest_file_rewind(pParent);
1836
while( (pParentFile = manifest_file_next(pParent,0))!=0 ){
1837
pChildFile = manifest_file_seek(pChild, pParentFile->zName, 0);
1838
if( pChildFile==0 && pParentFile->zUuid!=0 ){
1839
add_one_mlink(pmid, pParentFile->zUuid, mid, 0, pParentFile->zName, 0,
1840
isPublic, isPrim, 0);
1841
}
1842
}
1843
}
1844
manifest_cache_insert(*ppOther);
1845
1846
/* If pParent is the primary parent of pChild, also run this analysis
1847
** for all merge parents of pChild
1848
*/
1849
if( isPrim ){
1850
for(i=1; i<pChild->nParent; i++){
1851
pmid = uuid_to_rid(pChild->azParent[i], 0);
1852
if( pmid<=0 ) continue;
1853
add_mlink(pmid, 0, mid, pChild, 0);
1854
}
1855
for(i=0; i<pChild->nCherrypick; i++){
1856
if( pChild->aCherrypick[i].zCPTarget[0]=='+'
1857
&& (pmid = uuid_to_rid(pChild->aCherrypick[i].zCPTarget+1, 0))>0
1858
){
1859
add_mlink(pmid, 0, mid, pChild, 0);
1860
}
1861
}
1862
}
1863
}
1864
1865
/*
1866
** For a check-in with RID "rid" that has nParent parent check-ins given
1867
** by the hashes in azParent[], create all appropriate plink and mlink table
1868
** entries.
1869
**
1870
** The primary parent is the first hash on the azParent[] list.
1871
**
1872
** Return the RID of the primary parent.
1873
*/
1874
static int manifest_add_checkin_linkages(
1875
int rid, /* The RID of the check-in */
1876
Manifest *p, /* Manifest for this check-in */
1877
int nParent, /* Number of parents for this check-in */
1878
char * const * azParent /* hashes for each parent */
1879
){
1880
int i;
1881
int parentid = 0;
1882
char zBaseId[30]; /* Baseline manifest RID for deltas. "NULL" otherwise */
1883
Stmt q;
1884
int nLink;
1885
1886
if( p->zBaseline ){
1887
sqlite3_snprintf(sizeof(zBaseId), zBaseId, "%d",
1888
uuid_to_rid(p->zBaseline,1));
1889
}else{
1890
sqlite3_snprintf(sizeof(zBaseId), zBaseId, "NULL");
1891
}
1892
for(i=0; i<nParent; i++){
1893
int pid = uuid_to_rid(azParent[i], 1);
1894
db_multi_exec(
1895
"INSERT OR IGNORE INTO plink(pid, cid, isprim, mtime, baseid)"
1896
"VALUES(%d, %d, %d, %.17g, %s)",
1897
pid, rid, i==0, p->rDate, zBaseId/*safe-for-%s*/);
1898
if( i==0 ) parentid = pid;
1899
}
1900
add_mlink(parentid, 0, rid, p, 1);
1901
nLink = nParent;
1902
for(i=0; i<p->nCherrypick; i++){
1903
if( p->aCherrypick[i].zCPTarget[0]=='+' ) nLink++;
1904
}
1905
if( nLink>1 ){
1906
/* Change MLINK.PID from 0 to -1 for files that are added by merge. */
1907
db_multi_exec(
1908
"UPDATE mlink SET pid=-1"
1909
" WHERE mid=%d"
1910
" AND pid=0"
1911
" AND fnid IN "
1912
" (SELECT fnid FROM mlink WHERE mid=%d GROUP BY fnid"
1913
" HAVING count(*)<%d)",
1914
rid, rid, nLink
1915
);
1916
}
1917
db_prepare(&q, "SELECT cid, isprim FROM plink WHERE pid=%d", rid);
1918
while( db_step(&q)==SQLITE_ROW ){
1919
int cid = db_column_int(&q, 0);
1920
int isprim = db_column_int(&q, 1);
1921
add_mlink(rid, p, cid, 0, isprim);
1922
}
1923
db_finalize(&q);
1924
if( nParent==0 ){
1925
/* For root files (files without parents) add mlink entries
1926
** showing all content as new. */
1927
int isPublic = !content_is_private(rid);
1928
for(i=0; i<p->nFile; i++){
1929
add_one_mlink(0, 0, rid, p->aFile[i].zUuid, p->aFile[i].zName, 0,
1930
isPublic, 1, manifest_file_mperm(&p->aFile[i]));
1931
}
1932
}
1933
return parentid;
1934
}
1935
1936
/*
1937
** There exists a "parent" tag against check-in rid that has value zValue.
1938
** If value is well-formed (meaning that it is a list of hashes), then use
1939
** zValue to reparent check-in rid.
1940
*/
1941
void manifest_reparent_checkin(int rid, const char *zValue){
1942
int nParent = 0;
1943
char *zCopy = 0;
1944
char **azParent = 0;
1945
Manifest *p = 0;
1946
int i, j;
1947
int n = (int)strlen(zValue);
1948
int mxParent = (n+1)/(HNAME_MIN+1);
1949
1950
if( mxParent<1 ) return;
1951
zCopy = fossil_strdup(zValue);
1952
azParent = fossil_malloc( sizeof(azParent[0])*mxParent );
1953
for(nParent=0, i=0; zCopy[i]; i++){
1954
char *z = &zCopy[i];
1955
azParent[nParent++] = z;
1956
if( nParent>mxParent ) goto reparent_abort;
1957
for(j=HNAME_MIN; z[j]>' '; j++){}
1958
if( !hname_validate(z, j) ) goto reparent_abort;
1959
if( z[j]==0 ) break;
1960
z[j] = 0;
1961
i += j;
1962
}
1963
p = manifest_get(rid, CFTYPE_MANIFEST, 0);
1964
if( p!=0 ){
1965
db_multi_exec(
1966
"DELETE FROM plink WHERE cid=%d;"
1967
"DELETE FROM mlink WHERE mid=%d;",
1968
rid, rid
1969
);
1970
manifest_add_checkin_linkages(rid,p,nParent,azParent);
1971
manifest_destroy(p);
1972
}
1973
reparent_abort:
1974
fossil_free(azParent);
1975
fossil_free(zCopy);
1976
}
1977
1978
/*
1979
** Setup to do multiple manifest_crosslink() calls.
1980
**
1981
** This routine creates TEMP tables for holding information for
1982
** processing that must be deferred until all artifacts have been
1983
** seen at least once. The deferred processing is accomplished
1984
** by the call to manifest_crosslink_end().
1985
*/
1986
void manifest_crosslink_begin(void){
1987
assert( manifest_crosslink_busy==0 );
1988
manifest_crosslink_busy = 1;
1989
manifest_create_event_triggers();
1990
db_begin_transaction();
1991
db_multi_exec(
1992
"CREATE TEMP TABLE pending_xlink(id TEXT PRIMARY KEY)WITHOUT ROWID;"
1993
"CREATE TEMP TABLE time_fudge("
1994
" mid INTEGER PRIMARY KEY," /* The rid of a manifest */
1995
" m1 REAL," /* The timestamp on mid */
1996
" cid INTEGER," /* A child or mid */
1997
" m2 REAL" /* Timestamp on the child */
1998
");"
1999
);
2000
}
2001
2002
/*
2003
** Add a new entry to the pending_xlink table.
2004
*/
2005
static void add_pending_crosslink(char cType, const char *zId){
2006
assert( manifest_crosslink_busy==1 );
2007
db_multi_exec(
2008
"INSERT OR IGNORE INTO pending_xlink VALUES('%c%q')",
2009
cType, zId
2010
);
2011
}
2012
2013
#if INTERFACE
2014
/* Timestamps might be adjusted slightly to ensure that check-ins appear
2015
** on the timeline in chronological order. This is the maximum amount
2016
** of the adjustment window, in days.
2017
*/
2018
#define AGE_FUDGE_WINDOW (2.0/86400.0) /* 2 seconds */
2019
2020
/* This is increment (in days) by which timestamps are adjusted for
2021
** use on the timeline.
2022
*/
2023
#define AGE_ADJUST_INCREMENT (25.0/86400000.0) /* 25 milliseconds */
2024
2025
#endif /* LOCAL_INTERFACE */
2026
2027
/*
2028
** Finish up a sequence of manifest_crosslink calls.
2029
*/
2030
int manifest_crosslink_end(int flags){
2031
Stmt q, u;
2032
int i;
2033
int rc = TH_OK;
2034
int permitHooks = (flags & MC_PERMIT_HOOKS);
2035
const char *zScript = 0;
2036
assert( manifest_crosslink_busy==1 );
2037
if( permitHooks ){
2038
rc = xfer_run_common_script();
2039
if( rc==TH_OK ){
2040
zScript = xfer_ticket_code();
2041
}
2042
}
2043
db_prepare(&q,
2044
"SELECT rid, value FROM tagxref"
2045
" WHERE tagid=%d AND tagtype=1",
2046
TAG_PARENT
2047
);
2048
while( db_step(&q)==SQLITE_ROW ){
2049
int rid = db_column_int(&q,0);
2050
const char *zValue = db_column_text(&q,1);
2051
manifest_reparent_checkin(rid, zValue);
2052
}
2053
db_finalize(&q);
2054
db_prepare(&q, "SELECT id FROM pending_xlink");
2055
while( db_step(&q)==SQLITE_ROW ){
2056
const char *zId = db_column_text(&q, 0);
2057
char cType;
2058
if( zId==0 || zId[0]==0 ) continue;
2059
cType = zId[0];
2060
zId++;
2061
if( cType=='t' ){
2062
ticket_rebuild_entry(zId);
2063
if( permitHooks && rc==TH_OK ){
2064
rc = xfer_run_script(zScript, zId, 0);
2065
}
2066
}else if( cType=='w' ){
2067
backlink_wiki_refresh(zId);
2068
}
2069
}
2070
db_finalize(&q);
2071
db_multi_exec("DROP TABLE pending_xlink");
2072
2073
/* If multiple check-ins happen close together in time, adjust their
2074
** times by a few milliseconds to make sure they appear in chronological
2075
** order.
2076
*/
2077
db_prepare(&q,
2078
"UPDATE time_fudge SET m1=m2-:incr WHERE m1>=m2 AND m1<m2+:window"
2079
);
2080
db_bind_double(&q, ":incr", AGE_ADJUST_INCREMENT);
2081
db_bind_double(&q, ":window", AGE_FUDGE_WINDOW);
2082
db_prepare(&u,
2083
"UPDATE time_fudge SET m2="
2084
"(SELECT x.m1 FROM time_fudge AS x WHERE x.mid=time_fudge.cid)"
2085
);
2086
for(i=0; i<30; i++){
2087
db_step(&q);
2088
db_reset(&q);
2089
if( sqlite3_changes(g.db)==0 ) break;
2090
db_step(&u);
2091
db_reset(&u);
2092
}
2093
db_finalize(&q);
2094
db_finalize(&u);
2095
if( db_exists("SELECT 1 FROM time_fudge") ){
2096
db_multi_exec(
2097
"UPDATE event SET mtime=(SELECT m1 FROM time_fudge WHERE mid=objid)"
2098
" WHERE objid IN (SELECT mid FROM time_fudge)"
2099
" AND (mtime=omtime OR omtime IS NULL)"
2100
);
2101
}
2102
db_multi_exec("DROP TABLE time_fudge;");
2103
2104
db_end_transaction(0);
2105
manifest_crosslink_busy = 0;
2106
return ( rc!=TH_ERROR );
2107
}
2108
2109
/*
2110
** Activate EVENT triggers if they do not already exist.
2111
*/
2112
void manifest_create_event_triggers(void){
2113
if( manifest_event_triggers_are_enabled ){
2114
return; /* Triggers already exists. No-op. */
2115
}
2116
alert_create_trigger();
2117
manifest_event_triggers_are_enabled = 1;
2118
}
2119
2120
/*
2121
** Disable manifest event triggers. Drop them if they exist, but mark
2122
** them has having been created so that they won't be recreated. This
2123
** is used during "rebuild" to prevent triggers from firing then.
2124
*/
2125
void manifest_disable_event_triggers(void){
2126
alert_drop_trigger();
2127
manifest_event_triggers_are_enabled = 1;
2128
}
2129
2130
2131
/*
2132
** Make an entry in the event table for a ticket change artifact.
2133
*/
2134
void manifest_ticket_event(
2135
int rid, /* Artifact ID of the change ticket artifact */
2136
const Manifest *pManifest, /* Parsed content of the artifact */
2137
int isNew, /* True if this is the first event */
2138
int tktTagId /* Ticket tag ID */
2139
){
2140
int i;
2141
char *zTitle;
2142
Blob comment;
2143
Blob brief;
2144
char *zNewStatus = 0;
2145
static char *zTitleExpr = 0;
2146
static char *zStatusColumn = 0;
2147
static int once = 1;
2148
2149
blob_zero(&comment);
2150
blob_zero(&brief);
2151
if( once ){
2152
once = 0;
2153
zTitleExpr = db_get("ticket-title-expr", "title");
2154
zStatusColumn = db_get("ticket-status-column", "status");
2155
}
2156
zTitle = db_text("unknown",
2157
"SELECT \"%w\" FROM ticket WHERE tkt_uuid=%Q",
2158
zTitleExpr, pManifest->zTicketUuid
2159
);
2160
if( !isNew ){
2161
for(i=0; i<pManifest->nField; i++){
2162
if( fossil_strcmp(pManifest->aField[i].zName, zStatusColumn)==0 ){
2163
zNewStatus = pManifest->aField[i].zValue;
2164
}
2165
}
2166
if( zNewStatus ){
2167
blob_appendf(&comment, "%h ticket [%!S|%S]: <i>%h</i>",
2168
zNewStatus, pManifest->zTicketUuid, pManifest->zTicketUuid, zTitle
2169
);
2170
if( pManifest->nField>1 ){
2171
blob_appendf(&comment, " plus %d other change%s",
2172
pManifest->nField-1, pManifest->nField==2 ? "" : "s");
2173
}
2174
blob_appendf(&brief, "%h ticket [%!S|%S].",
2175
zNewStatus, pManifest->zTicketUuid, pManifest->zTicketUuid);
2176
}else{
2177
zNewStatus = db_text("unknown",
2178
"SELECT \"%w\" FROM ticket WHERE tkt_uuid=%Q",
2179
zStatusColumn, pManifest->zTicketUuid
2180
);
2181
blob_appendf(&comment, "Ticket [%!S|%S] <i>%h</i> status still %h with "
2182
"%d other change%s",
2183
pManifest->zTicketUuid, pManifest->zTicketUuid, zTitle, zNewStatus,
2184
pManifest->nField, pManifest->nField==1 ? "" : "s"
2185
);
2186
fossil_free(zNewStatus);
2187
blob_appendf(&brief, "Ticket [%!S|%S]: %d change%s",
2188
pManifest->zTicketUuid, pManifest->zTicketUuid, pManifest->nField,
2189
pManifest->nField==1 ? "" : "s"
2190
);
2191
}
2192
}else{
2193
blob_appendf(&comment, "New ticket [%!S|%S] <i>%h</i>.",
2194
pManifest->zTicketUuid, pManifest->zTicketUuid, zTitle
2195
);
2196
blob_appendf(&brief, "New ticket [%!S|%S].", pManifest->zTicketUuid,
2197
pManifest->zTicketUuid);
2198
}
2199
fossil_free(zTitle);
2200
manifest_create_event_triggers();
2201
if( db_exists("SELECT 1 FROM event WHERE type='t' AND objid=%d", rid) ){
2202
/* The ticket_rebuild_entry() function redoes all of the event entries
2203
** for a ticket whenever a new event appears. Be careful to only UPDATE
2204
** existing events, so that they do not get turned into alerts by
2205
** the alert trigger. */
2206
db_multi_exec(
2207
"UPDATE event SET tagid=%d, mtime=%.17g, user=%Q, comment=%Q, brief=%Q"
2208
" WHERE objid=%d",
2209
tktTagId, pManifest->rDate, pManifest->zUser,
2210
blob_str(&comment), blob_str(&brief), rid
2211
);
2212
}else{
2213
db_multi_exec(
2214
"REPLACE INTO event(type,tagid,mtime,objid,user,comment,brief)"
2215
"VALUES('t',%d,%.17g,%d,%Q,%Q,%Q)",
2216
tktTagId, pManifest->rDate, rid, pManifest->zUser,
2217
blob_str(&comment), blob_str(&brief)
2218
);
2219
}
2220
blob_reset(&comment);
2221
blob_reset(&brief);
2222
}
2223
2224
/*
2225
** Add an extra line of text to the end of a manifest to prevent it being
2226
** recognized as a valid manifest.
2227
**
2228
** This routine is called prior to writing out the text of a manifest as
2229
** the "manifest" file in the root of a repository when
2230
** "fossil setting manifest on" is enabled. That way, if the files of
2231
** the project are imported into a different Fossil project, the manifest
2232
** file will not be interpreted as a control artifact in that other project.
2233
**
2234
** Normally it is sufficient to simply append the extra line of text.
2235
** However, if the manifest is PGP signed then the extra line has to be
2236
** inserted before the PGP signature (thus invalidating the signature).
2237
*/
2238
void sterilize_manifest(Blob *p, int eType){
2239
char *z, *zOrig;
2240
int n, nOrig;
2241
static const char zExtraLine[] =
2242
"# Remove this line to create a well-formed Fossil %s.\n";
2243
const char *zType = eType==CFTYPE_MANIFEST ? "manifest" : "control artifact";
2244
2245
z = zOrig = blob_materialize(p);
2246
n = nOrig = blob_size(p);
2247
remove_pgp_signature((const char **)&z, &n);
2248
if( z==zOrig ){
2249
blob_appendf(p, zExtraLine/*works-like:"%s"*/, zType);
2250
}else{
2251
int iEnd;
2252
Blob copy;
2253
memcpy(&copy, p, sizeof(copy));
2254
blob_init(p, 0, 0);
2255
iEnd = (int)(&z[n] - zOrig);
2256
blob_append(p, zOrig, iEnd);
2257
blob_appendf(p, zExtraLine/*works-like:"%s"*/, zType);
2258
blob_append(p, &zOrig[iEnd], -1);
2259
blob_zero(&copy);
2260
}
2261
}
2262
2263
/*
2264
** This is the comparison function used to sort the tag array.
2265
*/
2266
static int tag_compare(const void *a, const void *b){
2267
struct TagType *pA = (struct TagType*)a;
2268
struct TagType *pB = (struct TagType*)b;
2269
int c;
2270
c = fossil_strcmp(pA->zUuid, pB->zUuid);
2271
if( c==0 ){
2272
c = fossil_strcmp(pA->zName, pB->zName);
2273
}
2274
return c;
2275
}
2276
2277
/*
2278
** Inserts plink entries for FORUM, WIKI, and TECHNOTE manifests. May
2279
** assert for other manifest types. If a parent entry exists, it also
2280
** propagates any tags for that parent. This is a no-op if
2281
** p->nParent==0.
2282
*/
2283
static void manifest_add_fwt_plink(int rid, Manifest *p){
2284
int i;
2285
int parentId = 0;
2286
assert(p->type==CFTYPE_WIKI ||
2287
p->type==CFTYPE_FORUM ||
2288
p->type==CFTYPE_EVENT);
2289
for(i=0; i<p->nParent; ++i){
2290
int const pid = uuid_to_rid(p->azParent[i], 1);
2291
if(0==i){
2292
parentId = pid;
2293
}
2294
db_multi_exec(
2295
"INSERT OR IGNORE INTO plink"
2296
"(pid, cid, isprim, mtime, baseid)"
2297
"VALUES(%d, %d, %d, %.17g, NULL)",
2298
pid, rid, i==0, p->rDate);
2299
}
2300
if(parentId){
2301
tag_propagate_all(parentId);
2302
}
2303
}
2304
2305
/*
2306
** Scan artifact rid/pContent to see if it is a control artifact of
2307
** any type:
2308
**
2309
** * Manifest
2310
** * Control
2311
** * Wiki Page
2312
** * Ticket Change
2313
** * Cluster
2314
** * Attachment
2315
** * Event
2316
** * Forum post
2317
**
2318
** If the input is a control artifact, then make appropriate entries
2319
** in the auxiliary tables of the database in order to crosslink the
2320
** artifact.
2321
**
2322
** If global variable g.xlinkClusterOnly is true, then ignore all
2323
** control artifacts other than clusters.
2324
**
2325
** This routine always resets the pContent blob before returning.
2326
**
2327
** Historical note: This routine original processed manifests only.
2328
** Processing for other control artifacts was added later. The name
2329
** of the routine, "manifest_crosslink", and the name of this source
2330
** file, is a legacy of its original use.
2331
*/
2332
int manifest_crosslink(int rid, Blob *pContent, int flags){
2333
int i, rc = TH_OK;
2334
Manifest *p;
2335
int parentid = 0;
2336
int permitHooks = (flags & MC_PERMIT_HOOKS);
2337
const char *zScript = 0;
2338
char *zUuid = 0;
2339
2340
if( g.fSqlTrace ){
2341
fossil_trace("-- manifest_crosslink(%d)\n", rid);
2342
}
2343
manifest_create_event_triggers();
2344
if( (p = manifest_cache_find(rid))!=0 ){
2345
blob_reset(pContent);
2346
}else if( (p = manifest_parse(pContent, rid, 0))==0 ){
2347
assert( blob_is_reset(pContent) || pContent==0 );
2348
if( (flags & MC_NO_ERRORS)==0 ){
2349
char * zErrUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d",rid);
2350
fossil_error(1, "syntax error in manifest [%S]", zErrUuid);
2351
fossil_free(zErrUuid);
2352
}
2353
return 0;
2354
}
2355
if( g.xlinkClusterOnly && p->type!=CFTYPE_CLUSTER ){
2356
manifest_destroy(p);
2357
assert( blob_is_reset(pContent) );
2358
if( (flags & MC_NO_ERRORS)==0 ) fossil_error(1, "no manifest");
2359
return 0;
2360
}
2361
if( p->type==CFTYPE_MANIFEST && fetch_baseline(p, 0) ){
2362
manifest_destroy(p);
2363
assert( blob_is_reset(pContent) );
2364
if( (flags & MC_NO_ERRORS)==0 ){
2365
fossil_error(1, "cannot fetch baseline for manifest [%S]",
2366
db_text(0, "SELECT uuid FROM blob WHERE rid=%d",rid));
2367
}
2368
return 0;
2369
}
2370
db_begin_transaction();
2371
if( p->type==CFTYPE_MANIFEST ){
2372
if( permitHooks ){
2373
zScript = xfer_commit_code();
2374
zUuid = rid_to_uuid(rid);
2375
}
2376
if( p->nCherrypick && db_table_exists("repository","cherrypick") ){
2377
int i;
2378
for(i=0; i<p->nCherrypick; i++){
2379
db_multi_exec(
2380
"REPLACE INTO cherrypick(parentid,childid,isExclude)"
2381
" SELECT rid, %d, %d FROM blob WHERE uuid=%Q",
2382
rid, p->aCherrypick[i].zCPTarget[0]=='-',
2383
p->aCherrypick[i].zCPTarget+1
2384
);
2385
}
2386
}
2387
if( !db_exists("SELECT 1 FROM mlink WHERE mid=%d", rid) ){
2388
char *zCom;
2389
parentid = manifest_add_checkin_linkages(rid,p,p->nParent,p->azParent);
2390
search_doc_touch('c', rid, 0);
2391
assert( manifest_event_triggers_are_enabled );
2392
zCom = db_text(0,
2393
"REPLACE INTO event(type,mtime,objid,user,comment,"
2394
"bgcolor,euser,ecomment,omtime)"
2395
"VALUES('ci',"
2396
" coalesce("
2397
" (SELECT julianday(value) FROM tagxref WHERE tagid=%d AND rid=%d),"
2398
" %.17g"
2399
" ),"
2400
" %d,%Q,%Q,"
2401
" (SELECT value FROM tagxref WHERE tagid=%d AND rid=%d AND tagtype>0),"
2402
" (SELECT value FROM tagxref WHERE tagid=%d AND rid=%d),"
2403
" (SELECT value FROM tagxref WHERE tagid=%d AND rid=%d),%.17g)"
2404
"RETURNING coalesce(ecomment,comment);",
2405
TAG_DATE, rid, p->rDate,
2406
rid, p->zUser, p->zComment,
2407
TAG_BGCOLOR, rid,
2408
TAG_USER, rid,
2409
TAG_COMMENT, rid, p->rDate
2410
);
2411
backlink_extract(zCom, MT_NONE, rid, BKLNK_COMMENT, p->rDate, 1);
2412
fossil_free(zCom);
2413
2414
/* If this is a delta-manifest, record the fact that this repository
2415
** contains delta manifests, to free the "commit" logic to generate
2416
** new delta manifests.
2417
*/
2418
if( p->zBaseline!=0 ){
2419
static int once = 1;
2420
if( once ){
2421
db_set_int("seen-delta-manifest", 1, 0);
2422
once = 0;
2423
}
2424
}
2425
}
2426
}
2427
if( p->type==CFTYPE_CLUSTER ){
2428
static Stmt del1;
2429
tag_insert("cluster", 1, 0, rid, p->rDate, rid);
2430
db_static_prepare(&del1, "DELETE FROM unclustered WHERE rid=:rid");
2431
for(i=0; i<p->nCChild; i++){
2432
int mid;
2433
mid = uuid_to_rid(p->azCChild[i], 1);
2434
if( mid>0 ){
2435
db_bind_int(&del1, ":rid", mid);
2436
db_step(&del1);
2437
db_reset(&del1);
2438
}
2439
}
2440
}
2441
if( p->type==CFTYPE_CONTROL
2442
|| p->type==CFTYPE_MANIFEST
2443
|| p->type==CFTYPE_EVENT
2444
){
2445
for(i=0; i<p->nTag; i++){
2446
int tid;
2447
int type;
2448
if( p->aTag[i].zUuid ){
2449
tid = uuid_to_rid(p->aTag[i].zUuid, 1);
2450
}else{
2451
tid = rid;
2452
}
2453
if( tid ){
2454
switch( p->aTag[i].zName[0] ){
2455
case '-': type = 0; break; /* Cancel prior occurrences */
2456
case '+': type = 1; break; /* Apply to target only */
2457
case '*': type = 2; break; /* Propagate to descendants */
2458
default:
2459
fossil_error(1, "unknown tag type in manifest: %s", p->aTag);
2460
manifest_destroy(p);
2461
return 0;
2462
}
2463
tag_insert(&p->aTag[i].zName[1], type, p->aTag[i].zValue,
2464
rid, p->rDate, tid);
2465
}
2466
}
2467
if( parentid ){
2468
tag_propagate_all(parentid);
2469
}
2470
}
2471
if(p->type==CFTYPE_WIKI || p->type==CFTYPE_FORUM
2472
|| p->type==CFTYPE_EVENT){
2473
manifest_add_fwt_plink(rid, p);
2474
}
2475
if( p->type==CFTYPE_WIKI ){
2476
char *zTag = mprintf("wiki-%s", p->zWikiTitle);
2477
int prior = 0;
2478
char cPrefix;
2479
int nWiki;
2480
char zLength[40];
2481
2482
while( fossil_isspace(p->zWiki[0]) ) p->zWiki++;
2483
nWiki = strlen(p->zWiki);
2484
sqlite3_snprintf(sizeof(zLength), zLength, "%d", nWiki);
2485
tag_insert(zTag, 1, zLength, rid, p->rDate, rid);
2486
fossil_free(zTag);
2487
if(p->nParent){
2488
prior = fast_uuid_to_rid(p->azParent[0]);
2489
}
2490
if( prior ){
2491
content_deltify(prior, &rid, 1, 0);
2492
}
2493
if( nWiki<=0 ){
2494
cPrefix = '-';
2495
}else if( !prior ){
2496
cPrefix = '+';
2497
}else{
2498
cPrefix = ':';
2499
}
2500
search_doc_touch('w',rid,p->zWikiTitle);
2501
if( manifest_crosslink_busy ){
2502
add_pending_crosslink('w',p->zWikiTitle);
2503
}else{
2504
backlink_wiki_refresh(p->zWikiTitle);
2505
}
2506
assert( manifest_event_triggers_are_enabled );
2507
db_multi_exec(
2508
"REPLACE INTO event(type,mtime,objid,user,comment)"
2509
"VALUES('w',%.17g,%d,%Q,'%c%q');",
2510
p->rDate, rid, p->zUser, cPrefix, p->zWikiTitle
2511
);
2512
}
2513
if( p->type==CFTYPE_EVENT ){
2514
char *zTag = mprintf("event-%s", p->zEventId);
2515
int tagid = tag_findid(zTag, 1);
2516
int prior = 0, subsequent;
2517
int nWiki;
2518
char zLength[40];
2519
Stmt qatt;
2520
while( fossil_isspace(p->zWiki[0]) ) p->zWiki++;
2521
nWiki = strlen(p->zWiki);
2522
sqlite3_snprintf(sizeof(zLength), zLength, "%d", nWiki);
2523
tag_insert(zTag, 1, zLength, rid, p->rDate, rid);
2524
fossil_free(zTag);
2525
if(p->nParent){
2526
prior = fast_uuid_to_rid(p->azParent[0]);
2527
}
2528
subsequent = db_int(0,
2529
/* BUG: this check is only correct if subsequent
2530
version has already been crosslinked. */
2531
"SELECT rid FROM tagxref"
2532
" WHERE tagid=%d AND mtime>=%.17g AND rid!=%d"
2533
" ORDER BY mtime",
2534
tagid, p->rDate, rid
2535
);
2536
if( prior ){
2537
content_deltify(prior, &rid, 1, 0);
2538
if( !subsequent ){
2539
db_multi_exec(
2540
"DELETE FROM event"
2541
" WHERE type='e'"
2542
" AND tagid=%d"
2543
" AND objid IN (SELECT rid FROM tagxref WHERE tagid=%d)",
2544
tagid, tagid
2545
);
2546
}
2547
}
2548
if( subsequent ){
2549
content_deltify(rid, &subsequent, 1, 0);
2550
}else{
2551
search_doc_touch('e',rid,0);
2552
assert( manifest_event_triggers_are_enabled );
2553
db_multi_exec(
2554
"REPLACE INTO event(type,mtime,objid,tagid,user,comment,bgcolor)"
2555
"VALUES('e',%.17g,%d,%d,%Q,%Q,"
2556
" (SELECT value FROM tagxref WHERE tagid=%d AND rid=%d));",
2557
p->rEventDate, rid, tagid, p->zUser, p->zComment,
2558
TAG_BGCOLOR, rid
2559
);
2560
}
2561
/* Locate and update comment for any attachments */
2562
db_prepare(&qatt,
2563
"SELECT attachid, src, target, filename FROM attachment"
2564
" WHERE target=%Q",
2565
p->zEventId
2566
);
2567
while( db_step(&qatt)==SQLITE_ROW ){
2568
const char *zAttachId = db_column_text(&qatt, 0);
2569
const char *zSrc = db_column_text(&qatt, 1);
2570
const char *zTarget = db_column_text(&qatt, 2);
2571
const char *zName = db_column_text(&qatt, 3);
2572
const char isAdd = (zSrc && zSrc[0]) ? 1 : 0;
2573
char *zComment;
2574
if( isAdd ){
2575
zComment = mprintf(
2576
"Add attachment [/artifact/%!S|%h] to"
2577
" tech note [/technote/%!S|%S]",
2578
zSrc, zName, zTarget, zTarget);
2579
}else{
2580
zComment = mprintf(
2581
"Delete attachment \"%h\" from"
2582
" tech note [/technote/%!S|%S]",
2583
zName, zTarget, zTarget);
2584
}
2585
db_multi_exec("UPDATE event SET comment=%Q, type='e'"
2586
" WHERE objid=%Q",
2587
zComment, zAttachId);
2588
fossil_free(zComment);
2589
}
2590
db_finalize(&qatt);
2591
}
2592
if( p->type==CFTYPE_TICKET ){
2593
char *zTag;
2594
Stmt qatt;
2595
assert( manifest_crosslink_busy==1 );
2596
zTag = mprintf("tkt-%s", p->zTicketUuid);
2597
tag_insert(zTag, 1, 0, rid, p->rDate, rid);
2598
fossil_free(zTag);
2599
add_pending_crosslink('t',p->zTicketUuid);
2600
/* Locate and update comment for any attachments */
2601
db_prepare(&qatt,
2602
"SELECT attachid, src, target, filename FROM attachment"
2603
" WHERE target=%Q",
2604
p->zTicketUuid
2605
);
2606
while( db_step(&qatt)==SQLITE_ROW ){
2607
const char *zAttachId = db_column_text(&qatt, 0);
2608
const char *zSrc = db_column_text(&qatt, 1);
2609
const char *zTarget = db_column_text(&qatt, 2);
2610
const char *zName = db_column_text(&qatt, 3);
2611
const char isAdd = (zSrc && zSrc[0]) ? 1 : 0;
2612
char *zComment;
2613
if( isAdd ){
2614
zComment = mprintf(
2615
"Add attachment [/artifact/%!S|%h] to ticket [%!S|%S]",
2616
zSrc, zName, zTarget, zTarget);
2617
}else{
2618
zComment = mprintf("Delete attachment \"%h\" from ticket [%!S|%S]",
2619
zName, zTarget, zTarget);
2620
}
2621
db_multi_exec("UPDATE event SET comment=%Q, type='t'"
2622
" WHERE objid=%Q",
2623
zComment, zAttachId);
2624
fossil_free(zComment);
2625
}
2626
db_finalize(&qatt);
2627
}
2628
if( p->type==CFTYPE_ATTACHMENT ){
2629
char *zComment = 0;
2630
const char isAdd = (p->zAttachSrc && p->zAttachSrc[0]) ? 1 : 0;
2631
char attachToType = 0;
2632
db_multi_exec(
2633
"INSERT INTO attachment(attachid, mtime, src, target,"
2634
"filename, comment, user)"
2635
"VALUES(%d,%.17g,%Q,%Q,%Q,%Q,%Q);",
2636
rid, p->rDate, p->zAttachSrc, p->zAttachTarget, p->zAttachName,
2637
(p->zComment ? p->zComment : ""), p->zUser
2638
);
2639
db_multi_exec(
2640
"UPDATE attachment SET isLatest = (mtime=="
2641
"(SELECT max(mtime) FROM attachment"
2642
" WHERE target=%Q AND filename=%Q))"
2643
" WHERE target=%Q AND filename=%Q",
2644
p->zAttachTarget, p->zAttachName,
2645
p->zAttachTarget, p->zAttachName
2646
);
2647
switch( attachment_target_type(p->zAttachTarget, 1) ){
2648
case 0:
2649
/* It is possible that p->zAttachTarget is not yet in this
2650
** copy of the repository. If we cannot identify it yet,
2651
** generate a generic /artifact link to it instead of a
2652
** type-specific link or an error message. */
2653
attachToType = 'a';
2654
if( isAdd ){
2655
zComment = mprintf(
2656
"Add attachment [/artifact/%!S|%h] to [/artifact/%!S|%h]",
2657
p->zAttachSrc, p->zAttachName,
2658
p->zAttachTarget, p->zAttachTarget);
2659
}else{
2660
zComment = mprintf("Delete attachment \"%h\" from "
2661
"[/artifact/%!S|%h",
2662
p->zAttachName, p->zAttachTarget,
2663
p->zAttachTarget);
2664
}
2665
break;
2666
case CFTYPE_WIKI:
2667
attachToType = 'w';
2668
if( isAdd ){
2669
zComment = mprintf(
2670
"Add attachment [/artifact/%!S|%h] to wiki page [%h]",
2671
p->zAttachSrc, p->zAttachName, p->zAttachTarget);
2672
}else{
2673
zComment = mprintf("Delete attachment \"%h\" from "
2674
"wiki page [%h]",
2675
p->zAttachName, p->zAttachTarget);
2676
}
2677
break;
2678
case CFTYPE_EVENT:
2679
attachToType = 'e';
2680
if( isAdd ){
2681
zComment = mprintf(
2682
"Add attachment [/artifact/%!S|%h] to tech note "
2683
"[/technote/%!S|%S]",
2684
p->zAttachSrc, p->zAttachName, p->zAttachTarget, p->zAttachTarget);
2685
}else{
2686
zComment = mprintf(
2687
"Delete attachment \"/artifact/%!S|%h\" from"
2688
" tech note [/technote/%!S|%S]",
2689
p->zAttachName, p->zAttachName,
2690
p->zAttachTarget,p->zAttachTarget);
2691
}
2692
break;
2693
case CFTYPE_TICKET:
2694
attachToType = 't';
2695
if( isAdd ){
2696
zComment = mprintf(
2697
"Add attachment [/artifact/%!S|%h] to ticket [%!S|%S]",
2698
p->zAttachSrc, p->zAttachName, p->zAttachTarget, p->zAttachTarget);
2699
}else{
2700
zComment = mprintf(
2701
"Delete attachment \"%h\" from ticket [%!S|%S]",
2702
p->zAttachName, p->zAttachTarget, p->zAttachTarget);
2703
}
2704
break;
2705
case CFTYPE_FORUM:
2706
attachToType = 'f';
2707
if( isAdd ){
2708
zComment = mprintf(
2709
"Add attachment [/artifact/%!S|%h] to forum post "
2710
"[/forumpost/%!S|%S]",
2711
p->zAttachSrc, p->zAttachName, p->zAttachTarget, p->zAttachTarget);
2712
}else{
2713
zComment = mprintf(
2714
"Delete attachment \"%h\" from forum post "
2715
"[/forumpost/%!S|%S]",
2716
p->zAttachName, p->zAttachTarget, p->zAttachTarget);
2717
}
2718
break;
2719
}
2720
assert( manifest_event_triggers_are_enabled );
2721
db_multi_exec(
2722
"REPLACE INTO event(type,mtime,objid,user,comment)"
2723
"VALUES('%c',%.17g,%d,%Q,%Q)",
2724
attachToType, p->rDate, rid, p->zUser, zComment
2725
);
2726
fossil_free(zComment);
2727
}
2728
if( p->type==CFTYPE_CONTROL ){
2729
Blob comment;
2730
int i;
2731
const char *zName;
2732
const char *zValue;
2733
const char *zTagUuid;
2734
int branchMove = 0;
2735
blob_zero(&comment);
2736
if( p->zComment ){
2737
blob_appendf(&comment, " %s.", p->zComment);
2738
}
2739
/* Next loop expects tags to be sorted on hash, so sort it. */
2740
qsort(p->aTag, p->nTag, sizeof(p->aTag[0]), tag_compare);
2741
for(i=0; i<p->nTag; i++){
2742
zTagUuid = p->aTag[i].zUuid;
2743
if( !zTagUuid ) continue;
2744
if( i==0 || fossil_strcmp(zTagUuid, p->aTag[i-1].zUuid)!=0 ){
2745
blob_appendf(&comment,
2746
" Edit [%!S|%S]:",
2747
zTagUuid, zTagUuid);
2748
branchMove = 0;
2749
if( permitHooks && db_exists("SELECT 1 FROM event, blob"
2750
" WHERE event.type='ci' AND event.objid=blob.rid"
2751
" AND blob.uuid=%Q", zTagUuid) ){
2752
zScript = xfer_commit_code();
2753
fossil_free(zUuid);
2754
zUuid = fossil_strdup(zTagUuid);
2755
}
2756
}
2757
zName = p->aTag[i].zName;
2758
zValue = p->aTag[i].zValue;
2759
if( strcmp(zName, "*branch")==0 ){
2760
blob_appendf(&comment,
2761
" Move to branch [/timeline?r=%t&nd&dp=%!S&unhide | %h].",
2762
zValue, zTagUuid, zValue);
2763
branchMove = 1;
2764
continue;
2765
}else if( strcmp(zName, "*bgcolor")==0 ){
2766
blob_appendf(&comment,
2767
" Change branch background color to \"%h\".", zValue);
2768
continue;
2769
}else if( strcmp(zName, "+bgcolor")==0 ){
2770
blob_appendf(&comment,
2771
" Change background color to \"%h\".", zValue);
2772
continue;
2773
}else if( strcmp(zName, "-bgcolor")==0 ){
2774
blob_appendf(&comment, " Cancel background color");
2775
}else if( strcmp(zName, "+comment")==0 ){
2776
blob_appendf(&comment, " Edit check-in comment.");
2777
continue;
2778
}else if( strcmp(zName, "+user")==0 ){
2779
blob_appendf(&comment, " Change user to \"%h\".", zValue);
2780
continue;
2781
}else if( strcmp(zName, "+date")==0 ){
2782
blob_appendf(&comment, " Timestamp %h.", zValue);
2783
continue;
2784
}else if( memcmp(zName, "-sym-",5)==0 ){
2785
if( !branchMove ){
2786
blob_appendf(&comment, " Cancel tag \"%h\"", &zName[5]);
2787
}else{
2788
continue;
2789
}
2790
}else if( memcmp(zName, "*sym-",5)==0 ){
2791
if( !branchMove ){
2792
blob_appendf(&comment, " Add propagating tag \"%h\"", &zName[5]);
2793
}else{
2794
continue;
2795
}
2796
}else if( memcmp(zName, "+sym-",5)==0 ){
2797
blob_appendf(&comment, " Add tag \"%h\"", &zName[5]);
2798
}else if( strcmp(zName, "+closed")==0 ){
2799
blob_append(&comment, " Mark \"Closed\"", -1);
2800
}else if( strcmp(zName, "-closed")==0 ){
2801
blob_append(&comment, " Remove the \"Closed\" mark", -1);
2802
}else {
2803
if( zName[0]=='-' ){
2804
blob_appendf(&comment, " Cancel \"%h\"", &zName[1]);
2805
}else if( zName[0]=='+' ){
2806
blob_appendf(&comment, " Add \"%h\"", &zName[1]);
2807
}else{
2808
blob_appendf(&comment, " Add propagating \"%h\"", &zName[1]);
2809
}
2810
if( zValue && zValue[0] ){
2811
blob_appendf(&comment, " with value \"%h\".", zValue);
2812
}else{
2813
blob_appendf(&comment, ".");
2814
}
2815
continue;
2816
}
2817
if( zValue && zValue[0] ){
2818
blob_appendf(&comment, " with note \"%h\".", zValue);
2819
}else{
2820
blob_appendf(&comment, ".");
2821
}
2822
}
2823
/*blob_appendf(&comment, " &#91;[/info/%S | details]&#93;");*/
2824
if( blob_size(&comment)==0 ) blob_append(&comment, " ", 1);
2825
assert( manifest_event_triggers_are_enabled );
2826
db_multi_exec(
2827
"REPLACE INTO event(type,mtime,objid,user,comment)"
2828
"VALUES('g',%.17g,%d,%Q,%Q)",
2829
p->rDate, rid, p->zUser, blob_str(&comment)+1
2830
);
2831
blob_reset(&comment);
2832
}
2833
if( p->type==CFTYPE_FORUM ){
2834
int froot, fprev, firt;
2835
char *zFType;
2836
char *zTitle;
2837
2838
assert( 0==zUuid );
2839
schema_forum();
2840
search_doc_touch('f', rid, 0);
2841
froot = p->zThreadRoot ? uuid_to_rid(p->zThreadRoot, 1) : rid;
2842
fprev = p->nParent ? uuid_to_rid(p->azParent[0],1) : 0;
2843
firt = p->zInReplyTo ? uuid_to_rid(p->zInReplyTo,1) : 0;
2844
db_multi_exec(
2845
"REPLACE INTO forumpost(fpid,froot,fprev,firt,fmtime)"
2846
"VALUES(%d,%d,nullif(%d,0),nullif(%d,0),%.17g)",
2847
p->rid, froot, fprev, firt, p->rDate
2848
);
2849
if( firt==0 ){
2850
/* This is the start of a new thread, either the initial entry
2851
** or an edit of the initial entry. */
2852
zTitle = p->zThreadTitle;
2853
if( zTitle==0 || zTitle[0]==0 ){
2854
zTitle = "(Deleted)";
2855
}
2856
zFType = fprev ? "Edit" : "Post";
2857
assert( manifest_event_triggers_are_enabled );
2858
db_multi_exec(
2859
"REPLACE INTO event(type,mtime,objid,user,comment)"
2860
"VALUES('f',%.17g,%d,%Q,'%q: %q')",
2861
p->rDate, rid, p->zUser, zFType, zTitle
2862
);
2863
/*
2864
** If this edit is the most recent, then make it the title for
2865
** all other entries for the same thread
2866
*/
2867
if( !db_exists("SELECT 1 FROM forumpost WHERE froot=%d AND firt=0"
2868
" AND fpid!=%d AND fmtime>%.17g", froot, rid, p->rDate)
2869
){
2870
/* This entry establishes a new title for all entries on the thread */
2871
db_multi_exec(
2872
"UPDATE event"
2873
" SET comment=substr(comment,1,instr(comment,':')) || ' %q'"
2874
" WHERE objid IN (SELECT fpid FROM forumpost WHERE froot=%d)",
2875
zTitle, froot
2876
);
2877
}
2878
}else{
2879
/* This is a reply to a prior post. Take the title from the root. */
2880
zTitle = db_text(0, "SELECT substr(comment,instr(comment,':')+2)"
2881
" FROM event WHERE objid=%d", froot);
2882
if( zTitle==0 ) zTitle = fossil_strdup("<i>Unknown</i>");
2883
if( p->zWiki[0]==0 ){
2884
zFType = "Delete reply";
2885
}else if( fprev ){
2886
zFType = "Edit reply";
2887
}else{
2888
zFType = "Reply";
2889
}
2890
assert( manifest_event_triggers_are_enabled );
2891
db_multi_exec(
2892
"REPLACE INTO event(type,mtime,objid,user,comment)"
2893
"VALUES('f',%.17g,%d,%Q,'%q: %q')",
2894
p->rDate, rid, p->zUser, zFType, zTitle
2895
);
2896
fossil_free(zTitle);
2897
}
2898
if( p->zWiki[0] ){
2899
int mimetype = parse_mimetype(p->zMimetype);
2900
backlink_extract(p->zWiki, mimetype, rid, BKLNK_FORUM, p->rDate, 1);
2901
}
2902
}
2903
2904
db_end_transaction(0);
2905
if( permitHooks ){
2906
rc = xfer_run_common_script();
2907
if( rc==TH_OK ){
2908
rc = xfer_run_script(zScript, zUuid, 0);
2909
}
2910
}
2911
fossil_free(zUuid);
2912
if( p->type==CFTYPE_MANIFEST ){
2913
manifest_cache_insert(p);
2914
}else{
2915
manifest_destroy(p);
2916
}
2917
assert( blob_is_reset(pContent) );
2918
return ( rc!=TH_ERROR );
2919
}
2920
2921
/*
2922
** COMMAND: test-crosslink
2923
**
2924
** Usage: %fossil test-crosslink RECORDID
2925
**
2926
** Run the manifest_crosslink() routine on the artifact with the given
2927
** record ID. This is typically done in the debugger.
2928
*/
2929
void test_crosslink_cmd(void){
2930
int rid;
2931
Blob content;
2932
db_find_and_open_repository(0, 0);
2933
if( g.argc!=3 ) usage("RECORDID");
2934
rid = name_to_rid(g.argv[2]);
2935
content_get(rid, &content);
2936
manifest_crosslink(rid, &content, MC_NONE);
2937
}
2938
2939
/*
2940
** For a given CATYPE_... value, returns a human-friendly name, or
2941
** NULL if typeId is unknown or is CFTYPE_ANY. The names returned by
2942
** this function are geared towards use with artifact_to_json(), and
2943
** may differ from some historical uses. e.g. CFTYPE_CONTROL artifacts
2944
** are called "tag" artifacts by this function.
2945
*/
2946
const char * artifact_type_to_name(int typeId){
2947
switch(typeId){
2948
case CFTYPE_MANIFEST: return "checkin";
2949
case CFTYPE_CLUSTER: return "cluster";
2950
case CFTYPE_CONTROL: return "tag";
2951
case CFTYPE_WIKI: return "wiki";
2952
case CFTYPE_TICKET: return "ticket";
2953
case CFTYPE_ATTACHMENT: return "attachment";
2954
case CFTYPE_EVENT: return "technote";
2955
case CFTYPE_FORUM: return "forumpost";
2956
}
2957
return NULL;
2958
}
2959
2960
/*
2961
** Creates a JSON representation of p, appending it to b.
2962
**
2963
** b is not cleared before rendering, so the caller needs to do that
2964
** if it's important for their use case.
2965
**
2966
** Pedantic note: this routine traverses p->aFile directly, rather
2967
** than using manifest_file_next(), so that delta manifests are
2968
** rendered as-is instead of containing their derived F-cards. If that
2969
** policy is ever changed, p will need to be non-const.
2970
*/
2971
void artifact_to_json(Manifest const *p, Blob *b){
2972
int i;
2973
2974
blob_append_literal(b, "{");
2975
blob_appendf(b, "\"uuid\":\"%z\"", rid_to_uuid(p->rid));
2976
/*blob_appendf(b, ", \"rid\": %d", p->rid); not portable across repos*/
2977
blob_appendf(b, ",\"type\":%!j", artifact_type_to_name(p->type));
2978
#define ISA(TYPE) if( p->type==TYPE )
2979
#define CARD_LETTER(LETTER) \
2980
blob_append_literal(b, ",\"" #LETTER "\":")
2981
#define CARD_STR(LETTER, VAL) \
2982
assert( VAL ); CARD_LETTER(LETTER); blob_appendf(b, "%!j", VAL)
2983
#define CARD_STR2(LETTER, VAL) \
2984
if( VAL ) { CARD_STR(LETTER, VAL); } (void)0
2985
#define STR_OR_NULL(VAL) \
2986
if( VAL ) blob_appendf(b, "%!j", VAL); \
2987
else blob_append(b, "null", 4)
2988
#define KVP_STR(ADDCOMMA, KEY,VAL) \
2989
if(ADDCOMMA) blob_append_char(b, ','); \
2990
blob_appendf(b, "%!j:", #KEY); \
2991
STR_OR_NULL(VAL)
2992
2993
ISA( CFTYPE_ATTACHMENT ){
2994
CARD_LETTER(A);
2995
blob_append_char(b, '{');
2996
KVP_STR(0, filename, p->zAttachName);
2997
KVP_STR(1, target, p->zAttachTarget);
2998
KVP_STR(1, source, p->zAttachSrc);
2999
blob_append_char(b, '}');
3000
}
3001
CARD_STR2(B, p->zBaseline);
3002
CARD_STR2(C, p->zComment);
3003
CARD_LETTER(D); blob_appendf(b, "%f", p->rDate);
3004
ISA( CFTYPE_EVENT ){
3005
blob_appendf(b, ", \"E\":{\"time\":%f,\"id\":%!j}",
3006
p->rEventDate, p->zEventId);
3007
}
3008
ISA( CFTYPE_MANIFEST ){
3009
CARD_LETTER(F);
3010
blob_append_char(b, '[');
3011
for( i = 0; i < p->nFile; ++i ){
3012
ManifestFile const * const pF = &p->aFile[i];
3013
if( i>0 ) blob_append_char(b, ',');
3014
blob_append_char(b, '{');
3015
KVP_STR(0, name, pF->zName);
3016
KVP_STR(1, uuid, pF->zUuid);
3017
KVP_STR(1, perm, pF->zPerm);
3018
KVP_STR(1, rename, pF->zPrior);
3019
blob_append_char(b, '}');
3020
}
3021
/* Special case: model check-ins with no F-card as having an empty
3022
** array, rather than no F-cards, to hypothetically simplify
3023
** handling in JSON queries. */
3024
blob_append_char(b, ']');
3025
}
3026
CARD_STR2(G, p->zThreadRoot);
3027
ISA( CFTYPE_FORUM ){
3028
CARD_LETTER(H);
3029
STR_OR_NULL( (p->zThreadTitle && *p->zThreadTitle) ? p->zThreadTitle : NULL);
3030
CARD_STR2(I, p->zInReplyTo);
3031
}
3032
if( p->nField ){
3033
CARD_LETTER(J);
3034
blob_append_char(b, '[');
3035
for( i = 0; i < p->nField; ++i ){
3036
const char * zName = p->aField[i].zName;
3037
if( i>0 ) blob_append_char(b, ',');
3038
blob_append_char(b, '{');
3039
KVP_STR(0, name, '+'==*zName ? &zName[1] : zName);
3040
KVP_STR(1, value, p->aField[i].zValue);
3041
blob_appendf(b, ",\"append\":%s", '+'==*zName ? "true" : "false");
3042
blob_append_char(b, '}');
3043
}
3044
blob_append_char(b, ']');
3045
}
3046
CARD_STR2(K, p->zTicketUuid);
3047
CARD_STR2(L, p->zWikiTitle);
3048
ISA( CFTYPE_CLUSTER ){
3049
CARD_LETTER(M);
3050
blob_append_char(b, '[');
3051
for( i = 0; i < p->nCChild; ++i ){
3052
if( i>0 ) blob_append_char(b, ',');
3053
blob_appendf(b, "%!j", p->azCChild[i]);
3054
}
3055
blob_append_char(b, ']');
3056
}
3057
CARD_STR2(N, p->zMimetype);
3058
ISA( CFTYPE_MANIFEST || p->nParent>0 ){
3059
CARD_LETTER(P);
3060
blob_append_char(b, '[');
3061
for( i = 0; i < p->nParent; ++i ){
3062
if( i>0 ) blob_append_char(b, ',');
3063
blob_appendf(b, "%!j", p->azParent[i]);
3064
}
3065
/* Special case: model check-ins with no P-card as having an empty
3066
** array, as per F-cards. */
3067
blob_append_char(b, ']');
3068
}
3069
if( p->nCherrypick ){
3070
CARD_LETTER(Q);
3071
blob_append_char(b, '[');
3072
for( i = 0; i < p->nCherrypick; ++i ){
3073
if( i>0 ) blob_append_char(b, ',');
3074
blob_append_char(b, '{');
3075
blob_appendf(b, "\"type\":\"%c\"", p->aCherrypick[i].zCPTarget[0]);
3076
KVP_STR(1, target, &p->aCherrypick[i].zCPTarget[1]);
3077
KVP_STR(1, base, p->aCherrypick[i].zCPBase);
3078
blob_append_char(b, '}');
3079
}
3080
blob_append_char(b, ']');
3081
}
3082
CARD_STR2(R, p->zRepoCksum);
3083
if( p->nTag ){
3084
CARD_LETTER(T);
3085
blob_append_char(b, '[');
3086
for( i = 0; i < p->nTag; ++i ){
3087
const char *zName = p->aTag[i].zName;
3088
if( i>0 ) blob_append_char(b, ',');
3089
blob_append_char(b, '{');
3090
blob_appendf(b, "\"type\":\"%c\"", *zName);
3091
KVP_STR(1, name, &zName[1]);
3092
KVP_STR(1, target, p->aTag[i].zUuid ? p->aTag[i].zUuid : "*")
3093
/* We could arguably resolve the "*" as null or p's uuid. */;
3094
KVP_STR(1, value, p->aTag[i].zValue);
3095
blob_append_char(b, '}');
3096
}
3097
blob_append_char(b, ']');
3098
}
3099
CARD_STR2(U, p->zUser);
3100
if( p->zWiki || CFTYPE_WIKI==p->type || CFTYPE_FORUM==p->type
3101
|| CFTYPE_EVENT==p->type ){
3102
CARD_LETTER(W);
3103
STR_OR_NULL((p->zWiki && *p->zWiki) ? p->zWiki : NULL);
3104
}
3105
blob_append_literal(b, "}");
3106
#undef CARD_FMT
3107
#undef CARD_LETTER
3108
#undef CARD_STR
3109
#undef CARD_STR2
3110
#undef ISA
3111
#undef KVP_STR
3112
#undef STR_OR_NULL
3113
}
3114
3115
/*
3116
** Convenience wrapper around artifact_to_json() which expects rid to
3117
** be the blob.rid of any artifact type. If it can load a Manifest
3118
** with that rid, it returns rid, else it returns 0.
3119
*/
3120
int artifact_to_json_by_rid(int rid, Blob *pOut){
3121
Manifest * const p = manifest_get(rid, CFTYPE_ANY, 0);
3122
if( p ){
3123
artifact_to_json(p, pOut);
3124
manifest_destroy(p);
3125
}else{
3126
rid = 0;
3127
}
3128
return rid;
3129
}
3130
3131
/*
3132
** Convenience wrapper around artifact_to_json() which accepts any
3133
** artifact name which is legal for symbolic_name_to_rid(). On success
3134
** it returns the rid of the artifact. Returns 0 if no such artifact
3135
** exists and a negative value if the name is ambiguous.
3136
**
3137
** pOut is not cleared before rendering, so the caller needs to do
3138
** that if it's important for their use case.
3139
*/
3140
int artifact_to_json_by_name(const char *zName, Blob *pOut){
3141
const int rid = symbolic_name_to_rid(zName, 0);
3142
return rid>0
3143
? artifact_to_json_by_rid(rid, pOut)
3144
: rid;
3145
}
3146
3147
/*
3148
** SQLite UDF for artifact_to_json(). Its single argument should be
3149
** either an INTEGER (blob.rid value) or a TEXT symbolic artifact
3150
** name, as per symbolic_name_to_rid(). If an artifact is found then
3151
** the result of the UDF is that JSON as a string, else it evaluates
3152
** to NULL.
3153
*/
3154
void artifact_to_json_sql_func(
3155
sqlite3_context *context,
3156
int argc,
3157
sqlite3_value **argv
3158
){
3159
int rid = 0;
3160
Blob b = empty_blob;
3161
3162
if(1 != argc){
3163
goto error_usage;
3164
}
3165
switch( sqlite3_value_type(argv[0]) ){
3166
case SQLITE_INTEGER:
3167
rid = artifact_to_json_by_rid(sqlite3_value_int(argv[0]), &b);
3168
break;
3169
case SQLITE_TEXT:{
3170
const char * z = (const char *)sqlite3_value_text(argv[0]);
3171
if( z ){
3172
rid = artifact_to_json_by_name(z, &b);
3173
}
3174
break;
3175
}
3176
default:
3177
goto error_usage;
3178
}
3179
if( rid>0 ){
3180
sqlite3_result_text(context, blob_str(&b), blob_size(&b),
3181
SQLITE_TRANSIENT);
3182
blob_reset(&b);
3183
}else{
3184
/* We should arguably error out if rid<0 (ambiguous name) */
3185
sqlite3_result_null(context);
3186
}
3187
return;
3188
error_usage:
3189
sqlite3_result_error(context, "Expecting one argument: blob.rid or "
3190
"artifact symbolic name", -1);
3191
}
3192
3193
3194
3195
/*
3196
** COMMAND: test-artifact-to-json
3197
**
3198
** Usage: %fossil test-artifact-to-json ?-pretty|-p? symbolic-name [...names]
3199
**
3200
** Tests the artifact_to_json() and artifact_to_json_by_name() APIs.
3201
*/
3202
void test_manifest_to_json(void){
3203
int i;
3204
Blob b = empty_blob;
3205
Stmt q;
3206
const int bPretty = find_option("pretty","p",0)!=0;
3207
int nErr = 0;
3208
3209
db_find_and_open_repository(0,0);
3210
db_prepare(&q, "select json_pretty(:json)");
3211
for( i=2; i<g.argc; ++i ){
3212
char const *zName = g.argv[i];
3213
const int rc = artifact_to_json_by_name(zName, &b);
3214
if( rc<=0 ){
3215
++nErr;
3216
fossil_warning("Error reading artifact %Q", zName);
3217
continue;
3218
}else if( bPretty ){
3219
db_bind_blob(&q, ":json", &b);
3220
b.nUsed = 0;
3221
db_step(&q);
3222
db_column_blob(&q, 0, &b);
3223
db_reset(&q);
3224
}
3225
fossil_print("%b\n", &b);
3226
blob_reset(&b);
3227
}
3228
db_finalize(&q);
3229
if( nErr ){
3230
fossil_warning("Error count: %d", nErr);
3231
}
3232
}
3233
3234
/*
3235
** Given the RID of an artifact, if that artifact type supports
3236
** P-cards then this returns the root-most RID of the P-card chain by
3237
** traversing the plink table, considering only primary parents. If
3238
** rid refers to anything other than such an artifact, or if the
3239
** artifact has no primary parent, rid is returned as-is.
3240
*/
3241
int rid_root_parent(int rid){
3242
Stmt q;
3243
db_prepare(
3244
&q, "SELECT pid FROM plink WHERE isprim AND cid=:cid"
3245
);
3246
db_bind_int(&q, ":cid", rid);
3247
while( SQLITE_ROW==db_step(&q) ){
3248
rid = db_column_int(&q, 0);
3249
db_reset(&q);
3250
db_bind_int(&q, ":cid", rid);
3251
}
3252
db_finalize(&q);
3253
return rid;
3254
}
3255

Keyboard Shortcuts

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