|
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
|
{ |
|
1134
|
char *zUuid = rid_to_uuid(rid); |
|
1135
|
if( zUuid ){ |
|
1136
|
if(pErr!=0){ |
|
1137
|
blob_appendf(pErr, "artifact [%s] ", zUuid); |
|
1138
|
} |
|
1139
|
fossil_free(zUuid); |
|
1140
|
} |
|
1141
|
} |
|
1142
|
if(pErr!=0){ |
|
1143
|
if( zErr ){ |
|
1144
|
blob_appendf(pErr, "line %d: %s", lineNo, zErr); |
|
1145
|
}else{ |
|
1146
|
blob_appendf(pErr, "unknown error on line %d", lineNo); |
|
1147
|
} |
|
1148
|
} |
|
1149
|
md5sum_init(); |
|
1150
|
manifest_destroy(p); |
|
1151
|
return 0; |
|
1152
|
} |
|
1153
|
|
|
1154
|
/* |
|
1155
|
** Get a manifest given the rid for the control artifact. Return |
|
1156
|
** a pointer to the manifest on success or NULL if there is a failure. |
|
1157
|
*/ |
|
1158
|
Manifest *manifest_get(int rid, int cfType, Blob *pErr){ |
|
1159
|
Blob content; |
|
1160
|
Manifest *p; |
|
1161
|
if( !rid ) return 0; |
|
1162
|
p = manifest_cache_find(rid); |
|
1163
|
if( p ){ |
|
1164
|
if( cfType!=CFTYPE_ANY && cfType!=p->type ){ |
|
1165
|
manifest_cache_insert(p); |
|
1166
|
p = 0; |
|
1167
|
} |
|
1168
|
return p; |
|
1169
|
} |
|
1170
|
content_get(rid, &content); |
|
1171
|
p = manifest_parse(&content, rid, pErr); |
|
1172
|
if( p && cfType!=CFTYPE_ANY && cfType!=p->type ){ |
|
1173
|
manifest_destroy(p); |
|
1174
|
p = 0; |
|
1175
|
} |
|
1176
|
return p; |
|
1177
|
} |
|
1178
|
|
|
1179
|
/* |
|
1180
|
** Given a check-in name, load and parse the manifest for that check-in. |
|
1181
|
** Throw a fatal error if anything goes wrong. |
|
1182
|
*/ |
|
1183
|
Manifest *manifest_get_by_name(const char *zName, int *pRid){ |
|
1184
|
int rid; |
|
1185
|
Manifest *p; |
|
1186
|
|
|
1187
|
rid = name_to_typed_rid(zName, "ci"); |
|
1188
|
if( !is_a_version(rid) ){ |
|
1189
|
fossil_fatal("no such check-in: %s", zName); |
|
1190
|
} |
|
1191
|
if( pRid ) *pRid = rid; |
|
1192
|
p = manifest_get(rid, CFTYPE_MANIFEST, 0); |
|
1193
|
if( p==0 ){ |
|
1194
|
fossil_fatal("cannot parse manifest for check-in: %s", zName); |
|
1195
|
} |
|
1196
|
return p; |
|
1197
|
} |
|
1198
|
|
|
1199
|
/* |
|
1200
|
** The input blob is text that may or may not be a valid Fossil |
|
1201
|
** control artifact of some kind. This routine returns true if |
|
1202
|
** the input is a well-formed control artifact and false if it |
|
1203
|
** is not. |
|
1204
|
** |
|
1205
|
** This routine is optimized to return false quickly and with minimal |
|
1206
|
** work in the common case where the input is some random file. |
|
1207
|
*/ |
|
1208
|
int manifest_is_well_formed(const char *zIn, int nIn){ |
|
1209
|
int i; |
|
1210
|
int iRes; |
|
1211
|
Manifest *pManifest; |
|
1212
|
Blob copy, errmsg; |
|
1213
|
remove_pgp_signature(&zIn, &nIn); |
|
1214
|
|
|
1215
|
/* Check to see that the file begins with a "card" */ |
|
1216
|
if( nIn<3 ) return 0; |
|
1217
|
if( zIn[0]<'A' || zIn[0]>'M' || zIn[1]!=' ' ) return 0; |
|
1218
|
|
|
1219
|
/* Check to see that the first card is followed by one more card */ |
|
1220
|
for(i=2; i<nIn && zIn[i]!='\n'; i++){} |
|
1221
|
if( i>=nIn-3 ) return 0; |
|
1222
|
i++; |
|
1223
|
if( !fossil_isupper(zIn[i]) || zIn[i]<zIn[0] || zIn[i+1]!=' ' ) return 0; |
|
1224
|
|
|
1225
|
/* The checks above will eliminate most random inputs. If these |
|
1226
|
** quick checks pass, then we could be dealing with a well-formed |
|
1227
|
** control artifact. Make a copy, and run it through the official |
|
1228
|
** artifact parser. This is the slow path, but it is rarely taken. |
|
1229
|
*/ |
|
1230
|
blob_init(©, 0, 0); |
|
1231
|
blob_init(&errmsg, 0, 0); |
|
1232
|
blob_append(©, zIn, nIn); |
|
1233
|
pManifest = manifest_parse(©, 0, &errmsg); |
|
1234
|
iRes = pManifest!=0; |
|
1235
|
manifest_destroy(pManifest); |
|
1236
|
blob_reset(&errmsg); |
|
1237
|
return iRes; |
|
1238
|
} |
|
1239
|
|
|
1240
|
/* |
|
1241
|
** COMMAND: test-parse-manifest |
|
1242
|
** |
|
1243
|
** Usage: %fossil test-parse-manifest FILENAME ?N? |
|
1244
|
** |
|
1245
|
** Parse the manifest(s) given on the command-line and report any |
|
1246
|
** errors. If the N argument is given, run the parsing N times. |
|
1247
|
*/ |
|
1248
|
void manifest_test_parse_cmd(void){ |
|
1249
|
Manifest *p; |
|
1250
|
Blob b; |
|
1251
|
int i; |
|
1252
|
int n = 1; |
|
1253
|
int isWF; |
|
1254
|
db_find_and_open_repository(OPEN_SUBSTITUTE|OPEN_OK_NOT_FOUND,0); |
|
1255
|
verify_all_options(); |
|
1256
|
if( g.argc!=3 && g.argc!=4 ){ |
|
1257
|
usage("FILENAME"); |
|
1258
|
} |
|
1259
|
blob_read_from_file(&b, g.argv[2], ExtFILE); |
|
1260
|
if( g.argc>3 ) n = atoi(g.argv[3]); |
|
1261
|
isWF = manifest_is_well_formed(blob_buffer(&b), blob_size(&b)); |
|
1262
|
fossil_print("manifest_is_well_formed() reports the input %s\n", |
|
1263
|
isWF ? "is ok" : "contains errors"); |
|
1264
|
for(i=0; i<n; i++){ |
|
1265
|
Blob b2; |
|
1266
|
Blob err; |
|
1267
|
blob_copy(&b2, &b); |
|
1268
|
blob_zero(&err); |
|
1269
|
p = manifest_parse(&b2, 0, &err); |
|
1270
|
if( p==0 ){ |
|
1271
|
fossil_print("ERROR: %s\n", blob_str(&err)); |
|
1272
|
}else if( i==0 || (n==2 && i==1) ){ |
|
1273
|
fossil_print("manifest_parse() worked\n"); |
|
1274
|
}else if( i==n-1 ){ |
|
1275
|
fossil_print("manifest_parse() worked %d more times\n", n-1); |
|
1276
|
} |
|
1277
|
if( (p==0 && isWF) || (p!=0 && !isWF) ){ |
|
1278
|
fossil_print("ERROR: manifest_is_well_formed() and " |
|
1279
|
"manifest_parse() disagree!\n"); |
|
1280
|
} |
|
1281
|
blob_reset(&err); |
|
1282
|
manifest_destroy(p); |
|
1283
|
} |
|
1284
|
blob_reset(&b); |
|
1285
|
} |
|
1286
|
|
|
1287
|
/* |
|
1288
|
** COMMAND: test-parse-all-blobs |
|
1289
|
** |
|
1290
|
** Usage: %fossil test-parse-all-blobs ?OPTIONS? |
|
1291
|
** |
|
1292
|
** Parse all entries in the BLOB table that are believed to be non-data |
|
1293
|
** artifacts and report any errors. Run this test command on historical |
|
1294
|
** repositories after making any changes to the manifest_parse() |
|
1295
|
** implementation to confirm that the changes did not break anything. |
|
1296
|
** |
|
1297
|
** Options: |
|
1298
|
** --limit N Parse no more than N artifacts before stopping |
|
1299
|
** --wellformed Use all BLOB table entries as input, not just |
|
1300
|
** those entries that are believed to be valid |
|
1301
|
** artifacts, and verify that the result the |
|
1302
|
** manifest_is_well_formed() agrees with the |
|
1303
|
** result of manifest_parse(). |
|
1304
|
*/ |
|
1305
|
void manifest_test_parse_all_blobs_cmd(void){ |
|
1306
|
Manifest *p; |
|
1307
|
Blob err; |
|
1308
|
Stmt q; |
|
1309
|
int nTest = 0; |
|
1310
|
int nErr = 0; |
|
1311
|
int N = 1000000000; |
|
1312
|
int bWellFormed; |
|
1313
|
const char *z; |
|
1314
|
db_find_and_open_repository(0, 0); |
|
1315
|
z = find_option("limit", 0, 1); |
|
1316
|
if( z ) N = atoi(z); |
|
1317
|
bWellFormed = find_option("wellformed",0,0)!=0; |
|
1318
|
verify_all_options(); |
|
1319
|
if( bWellFormed ){ |
|
1320
|
db_prepare(&q, "SELECT rid FROM blob ORDER BY rid"); |
|
1321
|
}else{ |
|
1322
|
db_prepare(&q, "SELECT DISTINCT objid FROM EVENT ORDER BY objid"); |
|
1323
|
} |
|
1324
|
while( (N--)>0 && db_step(&q)==SQLITE_ROW ){ |
|
1325
|
int id = db_column_int(&q,0); |
|
1326
|
fossil_print("Checking %d \r", id); |
|
1327
|
nTest++; |
|
1328
|
fflush(stdout); |
|
1329
|
blob_init(&err, 0, 0); |
|
1330
|
if( bWellFormed ){ |
|
1331
|
Blob content; |
|
1332
|
int isWF; |
|
1333
|
content_get(id, &content); |
|
1334
|
isWF = manifest_is_well_formed(blob_buffer(&content),blob_size(&content)); |
|
1335
|
p = manifest_parse(&content, id, &err); |
|
1336
|
if( isWF && p==0 ){ |
|
1337
|
fossil_print("%d ERROR: manifest_is_well_formed() reported true " |
|
1338
|
"but manifest_parse() reports an error: %s\n", |
|
1339
|
id, blob_str(&err)); |
|
1340
|
nErr++; |
|
1341
|
}else if( !isWF && p!=0 ){ |
|
1342
|
fossil_print("%d ERROR: manifest_is_well_formed() reported false " |
|
1343
|
"but manifest_parse() found nothing wrong.\n", id); |
|
1344
|
nErr++; |
|
1345
|
} |
|
1346
|
}else{ |
|
1347
|
p = manifest_get(id, CFTYPE_ANY, &err); |
|
1348
|
if( p==0 ){ |
|
1349
|
fossil_print("%d ERROR: %s\n", id, blob_str(&err)); |
|
1350
|
nErr++; |
|
1351
|
} |
|
1352
|
} |
|
1353
|
blob_reset(&err); |
|
1354
|
manifest_destroy(p); |
|
1355
|
} |
|
1356
|
db_finalize(&q); |
|
1357
|
fossil_print("%d tests with %d errors\n", nTest, nErr); |
|
1358
|
} |
|
1359
|
|
|
1360
|
/* |
|
1361
|
** Fetch the baseline associated with the delta-manifest p. |
|
1362
|
** Return 0 on success. If unable to parse the baseline, |
|
1363
|
** throw an error. If the baseline is a manifest, throw an |
|
1364
|
** error if throwError is true, or record that p is an orphan |
|
1365
|
** and return 1 if throwError is false. |
|
1366
|
*/ |
|
1367
|
static int fetch_baseline(Manifest *p, int throwError){ |
|
1368
|
if( p->zBaseline!=0 && p->pBaseline==0 ){ |
|
1369
|
int rid = uuid_to_rid(p->zBaseline, 1); |
|
1370
|
p->pBaseline = manifest_get(rid, CFTYPE_MANIFEST, 0); |
|
1371
|
if( p->pBaseline==0 ){ |
|
1372
|
if( !throwError ){ |
|
1373
|
db_multi_exec( |
|
1374
|
"INSERT OR IGNORE INTO orphan(rid, baseline) VALUES(%d,%d)", |
|
1375
|
p->rid, rid |
|
1376
|
); |
|
1377
|
return 1; |
|
1378
|
} |
|
1379
|
fossil_fatal("cannot access baseline manifest %S", p->zBaseline); |
|
1380
|
} |
|
1381
|
} |
|
1382
|
return 0; |
|
1383
|
} |
|
1384
|
|
|
1385
|
/* |
|
1386
|
** Rewind a manifest-file iterator back to the beginning of the manifest. |
|
1387
|
*/ |
|
1388
|
void manifest_file_rewind(Manifest *p){ |
|
1389
|
p->iFile = 0; |
|
1390
|
fetch_baseline(p, 1); |
|
1391
|
if( p->pBaseline ){ |
|
1392
|
p->pBaseline->iFile = 0; |
|
1393
|
} |
|
1394
|
} |
|
1395
|
|
|
1396
|
/* |
|
1397
|
** Advance to the next manifest-file. |
|
1398
|
** |
|
1399
|
** Return NULL for end-of-records or if there is an error. If an error |
|
1400
|
** occurs and pErr!=0 then store 1 in *pErr. |
|
1401
|
*/ |
|
1402
|
ManifestFile *manifest_file_next( |
|
1403
|
Manifest *p, |
|
1404
|
int *pErr |
|
1405
|
){ |
|
1406
|
ManifestFile *pOut = 0; |
|
1407
|
if( pErr ) *pErr = 0; |
|
1408
|
if( p->pBaseline==0 ){ |
|
1409
|
/* Manifest p is a baseline-manifest. Just scan down the list |
|
1410
|
** of files. */ |
|
1411
|
if( p->iFile<p->nFile ) pOut = &p->aFile[p->iFile++]; |
|
1412
|
}else{ |
|
1413
|
/* Manifest p is a delta-manifest. Scan the baseline but amend the |
|
1414
|
** file list in the baseline with changes described by p. |
|
1415
|
*/ |
|
1416
|
Manifest *pB = p->pBaseline; |
|
1417
|
int cmp; |
|
1418
|
while(1){ |
|
1419
|
if( pB->iFile>=pB->nFile ){ |
|
1420
|
/* We have used all entries out of the baseline. Return the next |
|
1421
|
** entry from the delta. */ |
|
1422
|
if( p->iFile<p->nFile ) pOut = &p->aFile[p->iFile++]; |
|
1423
|
break; |
|
1424
|
}else if( p->iFile>=p->nFile ){ |
|
1425
|
/* We have used all entries from the delta. Return the next |
|
1426
|
** entry from the baseline. */ |
|
1427
|
if( pB->iFile<pB->nFile ) pOut = &pB->aFile[pB->iFile++]; |
|
1428
|
break; |
|
1429
|
}else if( (cmp = fossil_strcmp(pB->aFile[pB->iFile].zName, |
|
1430
|
p->aFile[p->iFile].zName)) < 0 ){ |
|
1431
|
/* The next baseline entry comes before the next delta entry. |
|
1432
|
** So return the baseline entry. */ |
|
1433
|
pOut = &pB->aFile[pB->iFile++]; |
|
1434
|
break; |
|
1435
|
}else if( cmp>0 ){ |
|
1436
|
/* The next delta entry comes before the next baseline |
|
1437
|
** entry so return the delta entry */ |
|
1438
|
pOut = &p->aFile[p->iFile++]; |
|
1439
|
break; |
|
1440
|
}else if( p->aFile[p->iFile].zUuid ){ |
|
1441
|
/* The next delta entry is a replacement for the next baseline |
|
1442
|
** entry. Skip the baseline entry and return the delta entry */ |
|
1443
|
pB->iFile++; |
|
1444
|
pOut = &p->aFile[p->iFile++]; |
|
1445
|
break; |
|
1446
|
}else{ |
|
1447
|
/* The next delta entry is a delete of the next baseline |
|
1448
|
** entry. Skip them both. Repeat the loop to find the next |
|
1449
|
** non-delete entry. */ |
|
1450
|
pB->iFile++; |
|
1451
|
p->iFile++; |
|
1452
|
continue; |
|
1453
|
} |
|
1454
|
} |
|
1455
|
} |
|
1456
|
return pOut; |
|
1457
|
} |
|
1458
|
|
|
1459
|
/* |
|
1460
|
** Translate a filename into a filename-id (fnid). Create a new fnid |
|
1461
|
** if no previously exists. |
|
1462
|
*/ |
|
1463
|
static int filename_to_fnid(const char *zFilename){ |
|
1464
|
static Stmt q1, s1; |
|
1465
|
int fnid; |
|
1466
|
db_static_prepare(&q1, "SELECT fnid FROM filename WHERE name=:fn"); |
|
1467
|
db_bind_text(&q1, ":fn", zFilename); |
|
1468
|
fnid = 0; |
|
1469
|
if( db_step(&q1)==SQLITE_ROW ){ |
|
1470
|
fnid = db_column_int(&q1, 0); |
|
1471
|
} |
|
1472
|
db_reset(&q1); |
|
1473
|
if( fnid==0 ){ |
|
1474
|
db_static_prepare(&s1, "INSERT INTO filename(name) VALUES(:fn)"); |
|
1475
|
db_bind_text(&s1, ":fn", zFilename); |
|
1476
|
db_exec(&s1); |
|
1477
|
fnid = db_last_insert_rowid(); |
|
1478
|
} |
|
1479
|
return fnid; |
|
1480
|
} |
|
1481
|
|
|
1482
|
/* |
|
1483
|
** Compute an appropriate mlink.mperm integer for the permission string |
|
1484
|
** of a file. |
|
1485
|
*/ |
|
1486
|
int manifest_file_mperm(const ManifestFile *pFile){ |
|
1487
|
int mperm = PERM_REG; |
|
1488
|
if( pFile && pFile->zPerm){ |
|
1489
|
if( strstr(pFile->zPerm,"x")!=0 ){ |
|
1490
|
mperm = PERM_EXE; |
|
1491
|
}else if( strstr(pFile->zPerm,"l")!=0 ){ |
|
1492
|
mperm = PERM_LNK; |
|
1493
|
} |
|
1494
|
} |
|
1495
|
return mperm; |
|
1496
|
} |
|
1497
|
|
|
1498
|
/* |
|
1499
|
** Add a single entry to the mlink table. Also add the filename to |
|
1500
|
** the filename table if it is not there already. |
|
1501
|
** |
|
1502
|
** An mlink entry is always created if isPrimary is true. But if |
|
1503
|
** isPrimary is false (meaning that pmid is a merge parent of mid) |
|
1504
|
** then the mlink entry is only created if there is already an mlink |
|
1505
|
** from primary parent for the same file. |
|
1506
|
*/ |
|
1507
|
static void add_one_mlink( |
|
1508
|
int pmid, /* The parent manifest */ |
|
1509
|
const char *zFromUuid, /* Artifact hash for content in parent */ |
|
1510
|
int mid, /* The record ID of the manifest */ |
|
1511
|
const char *zToUuid, /* artifact hash for content in child */ |
|
1512
|
const char *zFilename, /* Filename */ |
|
1513
|
const char *zPrior, /* Previous filename. NULL if unchanged */ |
|
1514
|
int isPublic, /* True if mid is not a private manifest */ |
|
1515
|
int isPrimary, /* pmid is the primary parent of mid */ |
|
1516
|
int mperm /* 1: exec, 2: symlink */ |
|
1517
|
){ |
|
1518
|
int fnid, pfnid, pid, fid; |
|
1519
|
int doInsert; |
|
1520
|
static Stmt s1, s2; |
|
1521
|
|
|
1522
|
fnid = filename_to_fnid(zFilename); |
|
1523
|
if( zPrior==0 ){ |
|
1524
|
pfnid = 0; |
|
1525
|
}else{ |
|
1526
|
pfnid = filename_to_fnid(zPrior); |
|
1527
|
} |
|
1528
|
if( zFromUuid==0 || zFromUuid[0]==0 ){ |
|
1529
|
pid = 0; |
|
1530
|
}else{ |
|
1531
|
pid = uuid_to_rid(zFromUuid, 1); |
|
1532
|
} |
|
1533
|
if( zToUuid==0 || zToUuid[0]==0 ){ |
|
1534
|
fid = 0; |
|
1535
|
}else{ |
|
1536
|
fid = uuid_to_rid(zToUuid, 1); |
|
1537
|
if( isPublic ) content_make_public(fid); |
|
1538
|
} |
|
1539
|
if( isPrimary ){ |
|
1540
|
doInsert = 1; |
|
1541
|
}else{ |
|
1542
|
db_static_prepare(&s2, |
|
1543
|
"SELECT 1 FROM mlink WHERE mid=:m AND fnid=:n AND NOT isaux" |
|
1544
|
); |
|
1545
|
db_bind_int(&s2, ":m", mid); |
|
1546
|
db_bind_int(&s2, ":n", fnid); |
|
1547
|
doInsert = db_step(&s2)==SQLITE_ROW; |
|
1548
|
db_reset(&s2); |
|
1549
|
} |
|
1550
|
if( doInsert ){ |
|
1551
|
db_static_prepare(&s1, |
|
1552
|
"INSERT INTO mlink(mid,fid,pmid,pid,fnid,pfnid,mperm,isaux)" |
|
1553
|
"VALUES(:m,:f,:pm,:p,:n,:pfn,:mp,:isaux)" |
|
1554
|
); |
|
1555
|
db_bind_int(&s1, ":m", mid); |
|
1556
|
db_bind_int(&s1, ":f", fid); |
|
1557
|
db_bind_int(&s1, ":pm", pmid); |
|
1558
|
db_bind_int(&s1, ":p", pid); |
|
1559
|
db_bind_int(&s1, ":n", fnid); |
|
1560
|
db_bind_int(&s1, ":pfn", pfnid); |
|
1561
|
db_bind_int(&s1, ":mp", mperm); |
|
1562
|
db_bind_int(&s1, ":isaux", isPrimary==0); |
|
1563
|
db_exec(&s1); |
|
1564
|
} |
|
1565
|
if( pid && fid ){ |
|
1566
|
content_deltify(pid, &fid, 1, 0); |
|
1567
|
} |
|
1568
|
} |
|
1569
|
|
|
1570
|
/* |
|
1571
|
** Do a binary search to find a file in the p->aFile[] array. |
|
1572
|
** |
|
1573
|
** As an optimization, guess that the file we seek is at index p->iFile. |
|
1574
|
** That will usually be the case. If it is not found there, then do the |
|
1575
|
** actual binary search. |
|
1576
|
** |
|
1577
|
** Update p->iFile to be the index of the file that is found. |
|
1578
|
*/ |
|
1579
|
static ManifestFile *manifest_file_seek_base( |
|
1580
|
Manifest *p, /* Manifest to search */ |
|
1581
|
const char *zName, /* Name of the file we are looking for */ |
|
1582
|
int bBest /* 0: exact match only. 1: closest match */ |
|
1583
|
){ |
|
1584
|
int lwr, upr; |
|
1585
|
int c; |
|
1586
|
int i; |
|
1587
|
if( p->aFile==0 ){ |
|
1588
|
return 0; |
|
1589
|
} |
|
1590
|
lwr = 0; |
|
1591
|
upr = p->nFile - 1; |
|
1592
|
if( p->iFile>=lwr && p->iFile<upr ){ |
|
1593
|
c = fossil_strcmp(p->aFile[p->iFile+1].zName, zName); |
|
1594
|
if( c==0 ){ |
|
1595
|
return &p->aFile[++p->iFile]; |
|
1596
|
}else if( c>0 ){ |
|
1597
|
upr = p->iFile; |
|
1598
|
}else{ |
|
1599
|
lwr = p->iFile+1; |
|
1600
|
} |
|
1601
|
} |
|
1602
|
while( lwr<=upr ){ |
|
1603
|
i = (lwr+upr)/2; |
|
1604
|
c = fossil_strcmp(p->aFile[i].zName, zName); |
|
1605
|
if( c<0 ){ |
|
1606
|
lwr = i+1; |
|
1607
|
}else if( c>0 ){ |
|
1608
|
upr = i-1; |
|
1609
|
}else{ |
|
1610
|
p->iFile = i; |
|
1611
|
return &p->aFile[i]; |
|
1612
|
} |
|
1613
|
} |
|
1614
|
if( bBest ){ |
|
1615
|
if( lwr>=p->nFile ) lwr = p->nFile-1; |
|
1616
|
i = (int)strlen(zName); |
|
1617
|
if( strncmp(zName, p->aFile[lwr].zName, i)==0 ) return &p->aFile[lwr]; |
|
1618
|
} |
|
1619
|
return 0; |
|
1620
|
} |
|
1621
|
|
|
1622
|
/* |
|
1623
|
** Locate a file named zName in the aFile[] array of the given manifest. |
|
1624
|
** Return a pointer to the appropriate ManifestFile object. Return NULL |
|
1625
|
** if not found. |
|
1626
|
** |
|
1627
|
** This routine works even if p is a delta-manifest. The pointer |
|
1628
|
** returned might be to the baseline. |
|
1629
|
** |
|
1630
|
** We assume that filenames are in sorted order and use a binary search. |
|
1631
|
*/ |
|
1632
|
ManifestFile *manifest_file_seek(Manifest *p, const char *zName, int bBest){ |
|
1633
|
ManifestFile *pFile; |
|
1634
|
|
|
1635
|
pFile = manifest_file_seek_base(p, zName, p->zBaseline ? 0 : bBest); |
|
1636
|
if( pFile && pFile->zUuid==0 ) return 0; |
|
1637
|
if( pFile==0 && p->zBaseline ){ |
|
1638
|
fetch_baseline(p, 1); |
|
1639
|
pFile = manifest_file_seek_base(p->pBaseline, zName,bBest); |
|
1640
|
} |
|
1641
|
return pFile; |
|
1642
|
} |
|
1643
|
|
|
1644
|
/* |
|
1645
|
** Look for a file in a manifest, taking the case-sensitive option |
|
1646
|
** into account. If case-sensitive is off, then files in any case |
|
1647
|
** will match. |
|
1648
|
*/ |
|
1649
|
ManifestFile *manifest_file_find(Manifest *p, const char *zName){ |
|
1650
|
int i; |
|
1651
|
Manifest *pBase; |
|
1652
|
if( filenames_are_case_sensitive() ){ |
|
1653
|
return manifest_file_seek(p, zName, 0); |
|
1654
|
} |
|
1655
|
for(i=0; i<p->nFile; i++){ |
|
1656
|
if( fossil_stricmp(zName, p->aFile[i].zName)==0 ){ |
|
1657
|
return &p->aFile[i]; |
|
1658
|
} |
|
1659
|
} |
|
1660
|
if( p->zBaseline==0 ) return 0; |
|
1661
|
fetch_baseline(p, 1); |
|
1662
|
pBase = p->pBaseline; |
|
1663
|
if( pBase==0 ) return 0; |
|
1664
|
for(i=0; i<pBase->nFile; i++){ |
|
1665
|
if( fossil_stricmp(zName, pBase->aFile[i].zName)==0 ){ |
|
1666
|
return &pBase->aFile[i]; |
|
1667
|
} |
|
1668
|
} |
|
1669
|
return 0; |
|
1670
|
} |
|
1671
|
|
|
1672
|
/* |
|
1673
|
** Add mlink table entries associated with manifest cid, pChild. The |
|
1674
|
** parent manifest is pid, pParent. One of either pChild or pParent |
|
1675
|
** will be NULL and it will be computed based on cid/pid. |
|
1676
|
** |
|
1677
|
** A single mlink entry is added for every file that changed content, |
|
1678
|
** name, and/or permissions going from pid to cid. |
|
1679
|
** |
|
1680
|
** Deleted files have mlink.fid=0. |
|
1681
|
** Added files have mlink.pid=0. |
|
1682
|
** File added by merge have mlink.pid=-1 |
|
1683
|
** Edited files have both mlink.pid!=0 and mlink.fid!=0 |
|
1684
|
** |
|
1685
|
** Many mlink entries for merge parents will only be added if another mlink |
|
1686
|
** entry already exists for the same file from the primary parent. Therefore, |
|
1687
|
** to ensure that all merge-parent mlink entries are properly created: |
|
1688
|
** |
|
1689
|
** (1) Make this routine a no-op if pParent is a merge parent and the |
|
1690
|
** primary parent is a phantom. |
|
1691
|
** (2) Invoke this routine recursively for merge-parents if pParent is the |
|
1692
|
** primary parent. |
|
1693
|
*/ |
|
1694
|
static void add_mlink( |
|
1695
|
int pmid, Manifest *pParent, /* Parent check-in */ |
|
1696
|
int mid, Manifest *pChild, /* The child check-in */ |
|
1697
|
int isPrim /* TRUE if pmid is the primary parent of mid */ |
|
1698
|
){ |
|
1699
|
Blob otherContent; |
|
1700
|
int otherRid; |
|
1701
|
int i, rc; |
|
1702
|
ManifestFile *pChildFile, *pParentFile; |
|
1703
|
Manifest **ppOther; |
|
1704
|
static Stmt eq; |
|
1705
|
int isPublic; /* True if pChild is non-private */ |
|
1706
|
|
|
1707
|
/* If mlink table entires are already exist for the pmid-to-mid transition, |
|
1708
|
** then abort early doing no work. |
|
1709
|
*/ |
|
1710
|
db_static_prepare(&eq, "SELECT 1 FROM mlink WHERE mid=:mid AND pmid=:pmid"); |
|
1711
|
db_bind_int(&eq, ":mid", mid); |
|
1712
|
db_bind_int(&eq, ":pmid", pmid); |
|
1713
|
rc = db_step(&eq); |
|
1714
|
db_reset(&eq); |
|
1715
|
if( rc==SQLITE_ROW ) return; |
|
1716
|
|
|
1717
|
/* Compute the value of the missing pParent or pChild parameter. |
|
1718
|
** Fetch the baseline check-ins for both. |
|
1719
|
*/ |
|
1720
|
assert( pParent==0 || pChild==0 ); |
|
1721
|
if( pParent==0 ){ |
|
1722
|
ppOther = &pParent; |
|
1723
|
otherRid = pmid; |
|
1724
|
}else{ |
|
1725
|
ppOther = &pChild; |
|
1726
|
otherRid = mid; |
|
1727
|
} |
|
1728
|
if( (*ppOther = manifest_cache_find(otherRid))==0 ){ |
|
1729
|
content_get(otherRid, &otherContent); |
|
1730
|
if( blob_size(&otherContent)==0 ) return; |
|
1731
|
*ppOther = manifest_parse(&otherContent, otherRid, 0); |
|
1732
|
if( *ppOther==0 ) return; |
|
1733
|
} |
|
1734
|
if( fetch_baseline(pParent, 0) || fetch_baseline(pChild, 0) ){ |
|
1735
|
manifest_destroy(*ppOther); |
|
1736
|
return; |
|
1737
|
} |
|
1738
|
isPublic = !content_is_private(mid); |
|
1739
|
|
|
1740
|
/* If pParent is not the primary parent of pChild, and the primary |
|
1741
|
** parent of pChild is a phantom, then abort this routine without |
|
1742
|
** doing any work. The mlink entries will be computed when the |
|
1743
|
** primary parent dephantomizes. |
|
1744
|
*/ |
|
1745
|
if( !isPrim && otherRid==mid |
|
1746
|
&& !db_exists("SELECT 1 FROM blob WHERE uuid=%Q AND size>0", |
|
1747
|
pChild->azParent[0]) |
|
1748
|
){ |
|
1749
|
manifest_cache_insert(*ppOther); |
|
1750
|
return; |
|
1751
|
} |
|
1752
|
|
|
1753
|
/* Try to make the parent manifest a delta from the child, if that |
|
1754
|
** is an appropriate thing to do. For a new baseline, make the |
|
1755
|
** previous baseline a delta from the current baseline. |
|
1756
|
*/ |
|
1757
|
if( (pParent->zBaseline==0)==(pChild->zBaseline==0) ){ |
|
1758
|
content_deltify(pmid, &mid, 1, 0); |
|
1759
|
}else if( pChild->zBaseline==0 && pParent->zBaseline!=0 ){ |
|
1760
|
content_deltify(pParent->pBaseline->rid, &mid, 1, 0); |
|
1761
|
} |
|
1762
|
|
|
1763
|
/* Remember all children less than a few seconds younger than their parent, |
|
1764
|
** as we might want to fudge the times for those children. |
|
1765
|
*/ |
|
1766
|
if( pChild->rDate<pParent->rDate+AGE_FUDGE_WINDOW |
|
1767
|
&& manifest_crosslink_busy |
|
1768
|
){ |
|
1769
|
db_multi_exec( |
|
1770
|
"INSERT OR REPLACE INTO time_fudge VALUES(%d, %.17g, %d, %.17g);", |
|
1771
|
pParent->rid, pParent->rDate, pChild->rid, pChild->rDate |
|
1772
|
); |
|
1773
|
} |
|
1774
|
|
|
1775
|
/* First look at all files in pChild, ignoring its baseline. This |
|
1776
|
** is where most of the changes will be found. |
|
1777
|
*/ |
|
1778
|
for(i=0, pChildFile=pChild->aFile; i<pChild->nFile; i++, pChildFile++){ |
|
1779
|
int mperm = manifest_file_mperm(pChildFile); |
|
1780
|
if( pChildFile->zPrior ){ |
|
1781
|
pParentFile = manifest_file_seek(pParent, pChildFile->zPrior, 0); |
|
1782
|
if( pParentFile ){ |
|
1783
|
/* File with name change */ |
|
1784
|
add_one_mlink(pmid, pParentFile->zUuid, mid, pChildFile->zUuid, |
|
1785
|
pChildFile->zName, pChildFile->zPrior, |
|
1786
|
isPublic, isPrim, mperm); |
|
1787
|
}else{ |
|
1788
|
/* File name changed, but the old name is not found in the parent! |
|
1789
|
** Treat this like a new file. */ |
|
1790
|
add_one_mlink(pmid, 0, mid, pChildFile->zUuid, pChildFile->zName, 0, |
|
1791
|
isPublic, isPrim, mperm); |
|
1792
|
} |
|
1793
|
}else{ |
|
1794
|
pParentFile = manifest_file_seek(pParent, pChildFile->zName, 0); |
|
1795
|
if( pParentFile==0 ){ |
|
1796
|
if( pChildFile->zUuid ){ |
|
1797
|
/* A new file */ |
|
1798
|
add_one_mlink(pmid, 0, mid, pChildFile->zUuid, pChildFile->zName, 0, |
|
1799
|
isPublic, isPrim, mperm); |
|
1800
|
} |
|
1801
|
}else if( fossil_strcmp(pChildFile->zUuid, pParentFile->zUuid)!=0 |
|
1802
|
|| manifest_file_mperm(pParentFile)!=mperm ){ |
|
1803
|
/* Changes in file content or permissions */ |
|
1804
|
add_one_mlink(pmid, pParentFile->zUuid, mid, pChildFile->zUuid, |
|
1805
|
pChildFile->zName, 0, isPublic, isPrim, mperm); |
|
1806
|
} |
|
1807
|
} |
|
1808
|
} |
|
1809
|
if( pParent->zBaseline && pChild->zBaseline ){ |
|
1810
|
/* Both parent and child are delta manifests. Look for files that |
|
1811
|
** are deleted or modified in the parent but which reappear or revert |
|
1812
|
** to baseline in the child and show such files as being added or changed |
|
1813
|
** in the child. */ |
|
1814
|
for(i=0, pParentFile=pParent->aFile; i<pParent->nFile; i++, pParentFile++){ |
|
1815
|
if( pParentFile->zUuid ){ |
|
1816
|
pChildFile = manifest_file_seek_base(pChild, pParentFile->zName, 0); |
|
1817
|
if( pChildFile==0 ){ |
|
1818
|
/* The child file reverts to baseline. Show this as a change */ |
|
1819
|
pChildFile = manifest_file_seek(pChild, pParentFile->zName, 0); |
|
1820
|
if( pChildFile ){ |
|
1821
|
add_one_mlink(pmid, pParentFile->zUuid, mid, pChildFile->zUuid, |
|
1822
|
pChildFile->zName, 0, isPublic, isPrim, |
|
1823
|
manifest_file_mperm(pChildFile)); |
|
1824
|
} |
|
1825
|
} |
|
1826
|
}else{ |
|
1827
|
pChildFile = manifest_file_seek(pChild, pParentFile->zName, 0); |
|
1828
|
if( pChildFile ){ |
|
1829
|
/* File resurrected in the child after having been deleted in |
|
1830
|
** the parent. Show this as an added file. */ |
|
1831
|
add_one_mlink(pmid, 0, mid, pChildFile->zUuid, pChildFile->zName, 0, |
|
1832
|
isPublic, isPrim, manifest_file_mperm(pChildFile)); |
|
1833
|
} |
|
1834
|
} |
|
1835
|
} |
|
1836
|
}else if( pChild->zBaseline==0 ){ |
|
1837
|
/* pChild is a baseline. Look for files that are present in pParent |
|
1838
|
** but are missing from pChild and mark them as having been deleted. */ |
|
1839
|
manifest_file_rewind(pParent); |
|
1840
|
while( (pParentFile = manifest_file_next(pParent,0))!=0 ){ |
|
1841
|
pChildFile = manifest_file_seek(pChild, pParentFile->zName, 0); |
|
1842
|
if( pChildFile==0 && pParentFile->zUuid!=0 ){ |
|
1843
|
add_one_mlink(pmid, pParentFile->zUuid, mid, 0, pParentFile->zName, 0, |
|
1844
|
isPublic, isPrim, 0); |
|
1845
|
} |
|
1846
|
} |
|
1847
|
} |
|
1848
|
manifest_cache_insert(*ppOther); |
|
1849
|
|
|
1850
|
/* If pParent is the primary parent of pChild, also run this analysis |
|
1851
|
** for all merge parents of pChild |
|
1852
|
*/ |
|
1853
|
if( isPrim ){ |
|
1854
|
for(i=1; i<pChild->nParent; i++){ |
|
1855
|
pmid = uuid_to_rid(pChild->azParent[i], 0); |
|
1856
|
if( pmid<=0 ) continue; |
|
1857
|
add_mlink(pmid, 0, mid, pChild, 0); |
|
1858
|
} |
|
1859
|
for(i=0; i<pChild->nCherrypick; i++){ |
|
1860
|
if( pChild->aCherrypick[i].zCPTarget[0]=='+' |
|
1861
|
&& (pmid = uuid_to_rid(pChild->aCherrypick[i].zCPTarget+1, 0))>0 |
|
1862
|
){ |
|
1863
|
add_mlink(pmid, 0, mid, pChild, 0); |
|
1864
|
} |
|
1865
|
} |
|
1866
|
} |
|
1867
|
} |
|
1868
|
|
|
1869
|
/* |
|
1870
|
** For a check-in with RID "rid" that has nParent parent check-ins given |
|
1871
|
** by the hashes in azParent[], create all appropriate plink and mlink table |
|
1872
|
** entries. |
|
1873
|
** |
|
1874
|
** The primary parent is the first hash on the azParent[] list. |
|
1875
|
** |
|
1876
|
** Return the RID of the primary parent. |
|
1877
|
*/ |
|
1878
|
static int manifest_add_checkin_linkages( |
|
1879
|
int rid, /* The RID of the check-in */ |
|
1880
|
Manifest *p, /* Manifest for this check-in */ |
|
1881
|
int nParent, /* Number of parents for this check-in */ |
|
1882
|
char * const * azParent /* hashes for each parent */ |
|
1883
|
){ |
|
1884
|
int i; |
|
1885
|
int parentid = 0; |
|
1886
|
char zBaseId[30]; /* Baseline manifest RID for deltas. "NULL" otherwise */ |
|
1887
|
Stmt q; |
|
1888
|
int nLink; |
|
1889
|
|
|
1890
|
if( p->zBaseline ){ |
|
1891
|
sqlite3_snprintf(sizeof(zBaseId), zBaseId, "%d", |
|
1892
|
uuid_to_rid(p->zBaseline,1)); |
|
1893
|
}else{ |
|
1894
|
sqlite3_snprintf(sizeof(zBaseId), zBaseId, "NULL"); |
|
1895
|
} |
|
1896
|
for(i=0; i<nParent; i++){ |
|
1897
|
int pid = uuid_to_rid(azParent[i], 1); |
|
1898
|
db_multi_exec( |
|
1899
|
"INSERT OR IGNORE INTO plink(pid, cid, isprim, mtime, baseid)" |
|
1900
|
"VALUES(%d, %d, %d, %.17g, %s)", |
|
1901
|
pid, rid, i==0, p->rDate, zBaseId/*safe-for-%s*/); |
|
1902
|
if( i==0 ) parentid = pid; |
|
1903
|
} |
|
1904
|
add_mlink(parentid, 0, rid, p, 1); |
|
1905
|
nLink = nParent; |
|
1906
|
for(i=0; i<p->nCherrypick; i++){ |
|
1907
|
if( p->aCherrypick[i].zCPTarget[0]=='+' ) nLink++; |
|
1908
|
} |
|
1909
|
if( nLink>1 ){ |
|
1910
|
/* Change MLINK.PID from 0 to -1 for files that are added by merge. */ |
|
1911
|
db_multi_exec( |
|
1912
|
"UPDATE mlink SET pid=-1" |
|
1913
|
" WHERE mid=%d" |
|
1914
|
" AND pid=0" |
|
1915
|
" AND fnid IN " |
|
1916
|
" (SELECT fnid FROM mlink WHERE mid=%d GROUP BY fnid" |
|
1917
|
" HAVING count(*)<%d)", |
|
1918
|
rid, rid, nLink |
|
1919
|
); |
|
1920
|
} |
|
1921
|
db_prepare(&q, "SELECT cid, isprim FROM plink WHERE pid=%d", rid); |
|
1922
|
while( db_step(&q)==SQLITE_ROW ){ |
|
1923
|
int cid = db_column_int(&q, 0); |
|
1924
|
int isprim = db_column_int(&q, 1); |
|
1925
|
add_mlink(rid, p, cid, 0, isprim); |
|
1926
|
} |
|
1927
|
db_finalize(&q); |
|
1928
|
if( nParent==0 ){ |
|
1929
|
/* For root files (files without parents) add mlink entries |
|
1930
|
** showing all content as new. */ |
|
1931
|
int isPublic = !content_is_private(rid); |
|
1932
|
for(i=0; i<p->nFile; i++){ |
|
1933
|
add_one_mlink(0, 0, rid, p->aFile[i].zUuid, p->aFile[i].zName, 0, |
|
1934
|
isPublic, 1, manifest_file_mperm(&p->aFile[i])); |
|
1935
|
} |
|
1936
|
} |
|
1937
|
return parentid; |
|
1938
|
} |
|
1939
|
|
|
1940
|
/* |
|
1941
|
** There exists a "parent" tag against check-in rid that has value zValue. |
|
1942
|
** If value is well-formed (meaning that it is a list of hashes), then use |
|
1943
|
** zValue to reparent check-in rid. |
|
1944
|
*/ |
|
1945
|
void manifest_reparent_checkin(int rid, const char *zValue){ |
|
1946
|
int nParent = 0; |
|
1947
|
char *zCopy = 0; |
|
1948
|
char **azParent = 0; |
|
1949
|
Manifest *p = 0; |
|
1950
|
int i, j; |
|
1951
|
int n = (int)strlen(zValue); |
|
1952
|
int mxParent = (n+1)/(HNAME_MIN+1); |
|
1953
|
|
|
1954
|
if( mxParent<1 ) return; |
|
1955
|
zCopy = fossil_strdup(zValue); |
|
1956
|
azParent = fossil_malloc( sizeof(azParent[0])*mxParent ); |
|
1957
|
for(nParent=0, i=0; zCopy[i]; i++){ |
|
1958
|
char *z = &zCopy[i]; |
|
1959
|
azParent[nParent++] = z; |
|
1960
|
if( nParent>mxParent ) goto reparent_abort; |
|
1961
|
for(j=HNAME_MIN; z[j]>' '; j++){} |
|
1962
|
if( !hname_validate(z, j) ) goto reparent_abort; |
|
1963
|
if( z[j]==0 ) break; |
|
1964
|
z[j] = 0; |
|
1965
|
i += j; |
|
1966
|
} |
|
1967
|
p = manifest_get(rid, CFTYPE_MANIFEST, 0); |
|
1968
|
if( p!=0 ){ |
|
1969
|
db_multi_exec( |
|
1970
|
"DELETE FROM plink WHERE cid=%d;" |
|
1971
|
"DELETE FROM mlink WHERE mid=%d;", |
|
1972
|
rid, rid |
|
1973
|
); |
|
1974
|
manifest_add_checkin_linkages(rid,p,nParent,azParent); |
|
1975
|
manifest_destroy(p); |
|
1976
|
} |
|
1977
|
reparent_abort: |
|
1978
|
fossil_free(azParent); |
|
1979
|
fossil_free(zCopy); |
|
1980
|
} |
|
1981
|
|
|
1982
|
/* |
|
1983
|
** Setup to do multiple manifest_crosslink() calls. |
|
1984
|
** |
|
1985
|
** This routine creates TEMP tables for holding information for |
|
1986
|
** processing that must be deferred until all artifacts have been |
|
1987
|
** seen at least once. The deferred processing is accomplished |
|
1988
|
** by the call to manifest_crosslink_end(). |
|
1989
|
*/ |
|
1990
|
void manifest_crosslink_begin(void){ |
|
1991
|
assert( manifest_crosslink_busy==0 ); |
|
1992
|
manifest_crosslink_busy = 1; |
|
1993
|
manifest_create_event_triggers(); |
|
1994
|
db_begin_transaction(); |
|
1995
|
db_multi_exec( |
|
1996
|
"CREATE TEMP TABLE pending_xlink(id TEXT PRIMARY KEY)WITHOUT ROWID;" |
|
1997
|
"CREATE TEMP TABLE time_fudge(" |
|
1998
|
" mid INTEGER PRIMARY KEY," /* The rid of a manifest */ |
|
1999
|
" m1 REAL," /* The timestamp on mid */ |
|
2000
|
" cid INTEGER," /* A child or mid */ |
|
2001
|
" m2 REAL" /* Timestamp on the child */ |
|
2002
|
");" |
|
2003
|
); |
|
2004
|
} |
|
2005
|
|
|
2006
|
/* |
|
2007
|
** Add a new entry to the pending_xlink table. |
|
2008
|
*/ |
|
2009
|
static void add_pending_crosslink(char cType, const char *zId){ |
|
2010
|
assert( manifest_crosslink_busy==1 ); |
|
2011
|
db_multi_exec( |
|
2012
|
"INSERT OR IGNORE INTO pending_xlink VALUES('%c%q')", |
|
2013
|
cType, zId |
|
2014
|
); |
|
2015
|
} |
|
2016
|
|
|
2017
|
#if INTERFACE |
|
2018
|
/* Timestamps might be adjusted slightly to ensure that check-ins appear |
|
2019
|
** on the timeline in chronological order. This is the maximum amount |
|
2020
|
** of the adjustment window, in days. |
|
2021
|
*/ |
|
2022
|
#define AGE_FUDGE_WINDOW (2.0/86400.0) /* 2 seconds */ |
|
2023
|
|
|
2024
|
/* This is increment (in days) by which timestamps are adjusted for |
|
2025
|
** use on the timeline. |
|
2026
|
*/ |
|
2027
|
#define AGE_ADJUST_INCREMENT (25.0/86400000.0) /* 25 milliseconds */ |
|
2028
|
|
|
2029
|
#endif /* LOCAL_INTERFACE */ |
|
2030
|
|
|
2031
|
/* |
|
2032
|
** Finish up a sequence of manifest_crosslink calls. |
|
2033
|
*/ |
|
2034
|
int manifest_crosslink_end(int flags){ |
|
2035
|
Stmt q, u; |
|
2036
|
int i; |
|
2037
|
int rc = TH_OK; |
|
2038
|
int permitHooks = (flags & MC_PERMIT_HOOKS); |
|
2039
|
const char *zScript = 0; |
|
2040
|
assert( manifest_crosslink_busy==1 ); |
|
2041
|
if( permitHooks ){ |
|
2042
|
rc = xfer_run_common_script(); |
|
2043
|
if( rc==TH_OK ){ |
|
2044
|
zScript = xfer_ticket_code(); |
|
2045
|
} |
|
2046
|
} |
|
2047
|
db_prepare(&q, |
|
2048
|
"SELECT rid, value FROM tagxref" |
|
2049
|
" WHERE tagid=%d AND tagtype=1", |
|
2050
|
TAG_PARENT |
|
2051
|
); |
|
2052
|
while( db_step(&q)==SQLITE_ROW ){ |
|
2053
|
int rid = db_column_int(&q,0); |
|
2054
|
const char *zValue = db_column_text(&q,1); |
|
2055
|
manifest_reparent_checkin(rid, zValue); |
|
2056
|
} |
|
2057
|
db_finalize(&q); |
|
2058
|
db_prepare(&q, "SELECT id FROM pending_xlink"); |
|
2059
|
while( db_step(&q)==SQLITE_ROW ){ |
|
2060
|
const char *zId = db_column_text(&q, 0); |
|
2061
|
char cType; |
|
2062
|
if( zId==0 || zId[0]==0 ) continue; |
|
2063
|
cType = zId[0]; |
|
2064
|
zId++; |
|
2065
|
if( cType=='t' ){ |
|
2066
|
ticket_rebuild_entry(zId); |
|
2067
|
if( permitHooks && rc==TH_OK ){ |
|
2068
|
rc = xfer_run_script(zScript, zId, 0); |
|
2069
|
} |
|
2070
|
}else if( cType=='w' ){ |
|
2071
|
backlink_wiki_refresh(zId); |
|
2072
|
} |
|
2073
|
} |
|
2074
|
db_finalize(&q); |
|
2075
|
db_multi_exec("DROP TABLE pending_xlink"); |
|
2076
|
|
|
2077
|
/* If multiple check-ins happen close together in time, adjust their |
|
2078
|
** times by a few milliseconds to make sure they appear in chronological |
|
2079
|
** order. |
|
2080
|
*/ |
|
2081
|
db_prepare(&q, |
|
2082
|
"UPDATE time_fudge SET m1=m2-:incr WHERE m1>=m2 AND m1<m2+:window" |
|
2083
|
); |
|
2084
|
db_bind_double(&q, ":incr", AGE_ADJUST_INCREMENT); |
|
2085
|
db_bind_double(&q, ":window", AGE_FUDGE_WINDOW); |
|
2086
|
db_prepare(&u, |
|
2087
|
"UPDATE time_fudge SET m2=" |
|
2088
|
"(SELECT x.m1 FROM time_fudge AS x WHERE x.mid=time_fudge.cid)" |
|
2089
|
); |
|
2090
|
for(i=0; i<30; i++){ |
|
2091
|
db_step(&q); |
|
2092
|
db_reset(&q); |
|
2093
|
if( sqlite3_changes(g.db)==0 ) break; |
|
2094
|
db_step(&u); |
|
2095
|
db_reset(&u); |
|
2096
|
} |
|
2097
|
db_finalize(&q); |
|
2098
|
db_finalize(&u); |
|
2099
|
if( db_exists("SELECT 1 FROM time_fudge") ){ |
|
2100
|
db_multi_exec( |
|
2101
|
"UPDATE event SET mtime=(SELECT m1 FROM time_fudge WHERE mid=objid)" |
|
2102
|
" WHERE objid IN (SELECT mid FROM time_fudge)" |
|
2103
|
" AND (mtime=omtime OR omtime IS NULL)" |
|
2104
|
); |
|
2105
|
} |
|
2106
|
db_multi_exec("DROP TABLE time_fudge;"); |
|
2107
|
|
|
2108
|
db_end_transaction(0); |
|
2109
|
manifest_crosslink_busy = 0; |
|
2110
|
return ( rc!=TH_ERROR ); |
|
2111
|
} |
|
2112
|
|
|
2113
|
/* |
|
2114
|
** Activate EVENT triggers if they do not already exist. |
|
2115
|
*/ |
|
2116
|
void manifest_create_event_triggers(void){ |
|
2117
|
if( manifest_event_triggers_are_enabled ){ |
|
2118
|
return; /* Triggers already exists. No-op. */ |
|
2119
|
} |
|
2120
|
alert_create_trigger(); |
|
2121
|
manifest_event_triggers_are_enabled = 1; |
|
2122
|
} |
|
2123
|
|
|
2124
|
/* |
|
2125
|
** Disable manifest event triggers. Drop them if they exist, but mark |
|
2126
|
** them has having been created so that they won't be recreated. This |
|
2127
|
** is used during "rebuild" to prevent triggers from firing then. |
|
2128
|
*/ |
|
2129
|
void manifest_disable_event_triggers(void){ |
|
2130
|
alert_drop_trigger(); |
|
2131
|
manifest_event_triggers_are_enabled = 1; |
|
2132
|
} |
|
2133
|
|
|
2134
|
|
|
2135
|
/* |
|
2136
|
** Make an entry in the event table for a ticket change artifact. |
|
2137
|
*/ |
|
2138
|
void manifest_ticket_event( |
|
2139
|
int rid, /* Artifact ID of the change ticket artifact */ |
|
2140
|
const Manifest *pManifest, /* Parsed content of the artifact */ |
|
2141
|
int isNew, /* True if this is the first event */ |
|
2142
|
int tktTagId /* Ticket tag ID */ |
|
2143
|
){ |
|
2144
|
int i; |
|
2145
|
char *zTitle; |
|
2146
|
Blob comment; |
|
2147
|
Blob brief; |
|
2148
|
char *zNewStatus = 0; |
|
2149
|
static char *zTitleExpr = 0; |
|
2150
|
static char *zStatusColumn = 0; |
|
2151
|
static int once = 1; |
|
2152
|
|
|
2153
|
blob_zero(&comment); |
|
2154
|
blob_zero(&brief); |
|
2155
|
if( once ){ |
|
2156
|
once = 0; |
|
2157
|
zTitleExpr = db_get("ticket-title-expr", "title"); |
|
2158
|
zStatusColumn = db_get("ticket-status-column", "status"); |
|
2159
|
} |
|
2160
|
zTitle = db_text("unknown", |
|
2161
|
"SELECT \"%w\" FROM ticket WHERE tkt_uuid=%Q", |
|
2162
|
zTitleExpr, pManifest->zTicketUuid |
|
2163
|
); |
|
2164
|
if( !isNew ){ |
|
2165
|
for(i=0; i<pManifest->nField; i++){ |
|
2166
|
if( fossil_strcmp(pManifest->aField[i].zName, zStatusColumn)==0 ){ |
|
2167
|
zNewStatus = pManifest->aField[i].zValue; |
|
2168
|
} |
|
2169
|
} |
|
2170
|
if( zNewStatus ){ |
|
2171
|
blob_appendf(&comment, "%h ticket [%!S|%S]: <i>%h</i>", |
|
2172
|
zNewStatus, pManifest->zTicketUuid, pManifest->zTicketUuid, zTitle |
|
2173
|
); |
|
2174
|
if( pManifest->nField>1 ){ |
|
2175
|
blob_appendf(&comment, " plus %d other change%s", |
|
2176
|
pManifest->nField-1, pManifest->nField==2 ? "" : "s"); |
|
2177
|
} |
|
2178
|
blob_appendf(&brief, "%h ticket [%!S|%S].", |
|
2179
|
zNewStatus, pManifest->zTicketUuid, pManifest->zTicketUuid); |
|
2180
|
}else{ |
|
2181
|
zNewStatus = db_text("unknown", |
|
2182
|
"SELECT \"%w\" FROM ticket WHERE tkt_uuid=%Q", |
|
2183
|
zStatusColumn, pManifest->zTicketUuid |
|
2184
|
); |
|
2185
|
blob_appendf(&comment, "Ticket [%!S|%S] <i>%h</i> status still %h with " |
|
2186
|
"%d other change%s", |
|
2187
|
pManifest->zTicketUuid, pManifest->zTicketUuid, zTitle, zNewStatus, |
|
2188
|
pManifest->nField, pManifest->nField==1 ? "" : "s" |
|
2189
|
); |
|
2190
|
fossil_free(zNewStatus); |
|
2191
|
blob_appendf(&brief, "Ticket [%!S|%S]: %d change%s", |
|
2192
|
pManifest->zTicketUuid, pManifest->zTicketUuid, pManifest->nField, |
|
2193
|
pManifest->nField==1 ? "" : "s" |
|
2194
|
); |
|
2195
|
} |
|
2196
|
}else{ |
|
2197
|
blob_appendf(&comment, "New ticket [%!S|%S] <i>%h</i>.", |
|
2198
|
pManifest->zTicketUuid, pManifest->zTicketUuid, zTitle |
|
2199
|
); |
|
2200
|
blob_appendf(&brief, "New ticket [%!S|%S].", pManifest->zTicketUuid, |
|
2201
|
pManifest->zTicketUuid); |
|
2202
|
} |
|
2203
|
fossil_free(zTitle); |
|
2204
|
manifest_create_event_triggers(); |
|
2205
|
if( db_exists("SELECT 1 FROM event WHERE type='t' AND objid=%d", rid) ){ |
|
2206
|
/* The ticket_rebuild_entry() function redoes all of the event entries |
|
2207
|
** for a ticket whenever a new event appears. Be careful to only UPDATE |
|
2208
|
** existing events, so that they do not get turned into alerts by |
|
2209
|
** the alert trigger. */ |
|
2210
|
db_multi_exec( |
|
2211
|
"UPDATE event SET tagid=%d, mtime=%.17g, user=%Q, comment=%Q, brief=%Q" |
|
2212
|
" WHERE objid=%d", |
|
2213
|
tktTagId, pManifest->rDate, pManifest->zUser, |
|
2214
|
blob_str(&comment), blob_str(&brief), rid |
|
2215
|
); |
|
2216
|
}else{ |
|
2217
|
db_multi_exec( |
|
2218
|
"REPLACE INTO event(type,tagid,mtime,objid,user,comment,brief)" |
|
2219
|
"VALUES('t',%d,%.17g,%d,%Q,%Q,%Q)", |
|
2220
|
tktTagId, pManifest->rDate, rid, pManifest->zUser, |
|
2221
|
blob_str(&comment), blob_str(&brief) |
|
2222
|
); |
|
2223
|
} |
|
2224
|
blob_reset(&comment); |
|
2225
|
blob_reset(&brief); |
|
2226
|
} |
|
2227
|
|
|
2228
|
/* |
|
2229
|
** Add an extra line of text to the end of a manifest to prevent it being |
|
2230
|
** recognized as a valid manifest. |
|
2231
|
** |
|
2232
|
** This routine is called prior to writing out the text of a manifest as |
|
2233
|
** the "manifest" file in the root of a repository when |
|
2234
|
** "fossil setting manifest on" is enabled. That way, if the files of |
|
2235
|
** the project are imported into a different Fossil project, the manifest |
|
2236
|
** file will not be interpreted as a control artifact in that other project. |
|
2237
|
** |
|
2238
|
** Normally it is sufficient to simply append the extra line of text. |
|
2239
|
** However, if the manifest is PGP signed then the extra line has to be |
|
2240
|
** inserted before the PGP signature (thus invalidating the signature). |
|
2241
|
*/ |
|
2242
|
void sterilize_manifest(Blob *p, int eType){ |
|
2243
|
char *z, *zOrig; |
|
2244
|
int n, nOrig; |
|
2245
|
static const char zExtraLine[] = |
|
2246
|
"# Remove this line to create a well-formed Fossil %s.\n"; |
|
2247
|
const char *zType = eType==CFTYPE_MANIFEST ? "manifest" : "control artifact"; |
|
2248
|
|
|
2249
|
z = zOrig = blob_materialize(p); |
|
2250
|
n = nOrig = blob_size(p); |
|
2251
|
remove_pgp_signature((const char **)&z, &n); |
|
2252
|
if( z==zOrig ){ |
|
2253
|
blob_appendf(p, zExtraLine/*works-like:"%s"*/, zType); |
|
2254
|
}else{ |
|
2255
|
int iEnd; |
|
2256
|
Blob copy; |
|
2257
|
memcpy(©, p, sizeof(copy)); |
|
2258
|
blob_init(p, 0, 0); |
|
2259
|
iEnd = (int)(&z[n] - zOrig); |
|
2260
|
blob_append(p, zOrig, iEnd); |
|
2261
|
blob_appendf(p, zExtraLine/*works-like:"%s"*/, zType); |
|
2262
|
blob_append(p, &zOrig[iEnd], -1); |
|
2263
|
blob_zero(©); |
|
2264
|
} |
|
2265
|
} |
|
2266
|
|
|
2267
|
/* |
|
2268
|
** This is the comparison function used to sort the tag array. |
|
2269
|
*/ |
|
2270
|
static int tag_compare(const void *a, const void *b){ |
|
2271
|
struct TagType *pA = (struct TagType*)a; |
|
2272
|
struct TagType *pB = (struct TagType*)b; |
|
2273
|
int c; |
|
2274
|
c = fossil_strcmp(pA->zUuid, pB->zUuid); |
|
2275
|
if( c==0 ){ |
|
2276
|
c = fossil_strcmp(pA->zName, pB->zName); |
|
2277
|
} |
|
2278
|
return c; |
|
2279
|
} |
|
2280
|
|
|
2281
|
/* |
|
2282
|
** Inserts plink entries for FORUM, WIKI, and TECHNOTE manifests. May |
|
2283
|
** assert for other manifest types. If a parent entry exists, it also |
|
2284
|
** propagates any tags for that parent. This is a no-op if |
|
2285
|
** p->nParent==0. |
|
2286
|
*/ |
|
2287
|
static void manifest_add_fwt_plink(int rid, Manifest *p){ |
|
2288
|
int i; |
|
2289
|
int parentId = 0; |
|
2290
|
assert(p->type==CFTYPE_WIKI || |
|
2291
|
p->type==CFTYPE_FORUM || |
|
2292
|
p->type==CFTYPE_EVENT); |
|
2293
|
for(i=0; i<p->nParent; ++i){ |
|
2294
|
int const pid = uuid_to_rid(p->azParent[i], 1); |
|
2295
|
if(0==i){ |
|
2296
|
parentId = pid; |
|
2297
|
} |
|
2298
|
db_multi_exec( |
|
2299
|
"INSERT OR IGNORE INTO plink" |
|
2300
|
"(pid, cid, isprim, mtime, baseid)" |
|
2301
|
"VALUES(%d, %d, %d, %.17g, NULL)", |
|
2302
|
pid, rid, i==0, p->rDate); |
|
2303
|
} |
|
2304
|
if(parentId){ |
|
2305
|
tag_propagate_all(parentId); |
|
2306
|
} |
|
2307
|
} |
|
2308
|
|
|
2309
|
/* |
|
2310
|
** Scan artifact rid/pContent to see if it is a control artifact of |
|
2311
|
** any type: |
|
2312
|
** |
|
2313
|
** * Manifest |
|
2314
|
** * Control |
|
2315
|
** * Wiki Page |
|
2316
|
** * Ticket Change |
|
2317
|
** * Cluster |
|
2318
|
** * Attachment |
|
2319
|
** * Event |
|
2320
|
** * Forum post |
|
2321
|
** |
|
2322
|
** If the input is a control artifact, then make appropriate entries |
|
2323
|
** in the auxiliary tables of the database in order to crosslink the |
|
2324
|
** artifact. |
|
2325
|
** |
|
2326
|
** If global variable g.xlinkClusterOnly is true, then ignore all |
|
2327
|
** control artifacts other than clusters. |
|
2328
|
** |
|
2329
|
** This routine always resets the pContent blob before returning. |
|
2330
|
** |
|
2331
|
** Historical note: This routine original processed manifests only. |
|
2332
|
** Processing for other control artifacts was added later. The name |
|
2333
|
** of the routine, "manifest_crosslink", and the name of this source |
|
2334
|
** file, is a legacy of its original use. |
|
2335
|
*/ |
|
2336
|
int manifest_crosslink(int rid, Blob *pContent, int flags){ |
|
2337
|
int i, rc = TH_OK; |
|
2338
|
Manifest *p; |
|
2339
|
int parentid = 0; |
|
2340
|
int permitHooks = (flags & MC_PERMIT_HOOKS); |
|
2341
|
const char *zScript = 0; |
|
2342
|
char *zUuid = 0; |
|
2343
|
|
|
2344
|
if( g.fSqlTrace ){ |
|
2345
|
fossil_trace("-- manifest_crosslink(%d)\n", rid); |
|
2346
|
} |
|
2347
|
manifest_create_event_triggers(); |
|
2348
|
if( (p = manifest_cache_find(rid))!=0 ){ |
|
2349
|
blob_reset(pContent); |
|
2350
|
}else if( (p = manifest_parse(pContent, rid, 0))==0 ){ |
|
2351
|
assert( blob_is_reset(pContent) || pContent==0 ); |
|
2352
|
if( (flags & MC_NO_ERRORS)==0 ){ |
|
2353
|
char * zErrUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d",rid); |
|
2354
|
fossil_error(1, "syntax error in manifest [%S]", zErrUuid); |
|
2355
|
fossil_free(zErrUuid); |
|
2356
|
} |
|
2357
|
return 0; |
|
2358
|
} |
|
2359
|
if( g.xlinkClusterOnly && p->type!=CFTYPE_CLUSTER ){ |
|
2360
|
manifest_destroy(p); |
|
2361
|
assert( blob_is_reset(pContent) ); |
|
2362
|
if( (flags & MC_NO_ERRORS)==0 ) fossil_error(1, "no manifest"); |
|
2363
|
return 0; |
|
2364
|
} |
|
2365
|
if( p->type==CFTYPE_MANIFEST && fetch_baseline(p, 0) ){ |
|
2366
|
manifest_destroy(p); |
|
2367
|
assert( blob_is_reset(pContent) ); |
|
2368
|
if( (flags & MC_NO_ERRORS)==0 ){ |
|
2369
|
fossil_error(1, "cannot fetch baseline for manifest [%S]", |
|
2370
|
db_text(0, "SELECT uuid FROM blob WHERE rid=%d",rid)); |
|
2371
|
} |
|
2372
|
return 0; |
|
2373
|
} |
|
2374
|
db_begin_transaction(); |
|
2375
|
if( p->type==CFTYPE_MANIFEST ){ |
|
2376
|
if( permitHooks ){ |
|
2377
|
zScript = xfer_commit_code(); |
|
2378
|
zUuid = rid_to_uuid(rid); |
|
2379
|
} |
|
2380
|
if( p->nCherrypick && db_table_exists("repository","cherrypick") ){ |
|
2381
|
int i; |
|
2382
|
for(i=0; i<p->nCherrypick; i++){ |
|
2383
|
db_multi_exec( |
|
2384
|
"REPLACE INTO cherrypick(parentid,childid,isExclude)" |
|
2385
|
" SELECT rid, %d, %d FROM blob WHERE uuid=%Q", |
|
2386
|
rid, p->aCherrypick[i].zCPTarget[0]=='-', |
|
2387
|
p->aCherrypick[i].zCPTarget+1 |
|
2388
|
); |
|
2389
|
} |
|
2390
|
} |
|
2391
|
if( !db_exists("SELECT 1 FROM mlink WHERE mid=%d", rid) ){ |
|
2392
|
char *zCom; |
|
2393
|
parentid = manifest_add_checkin_linkages(rid,p,p->nParent,p->azParent); |
|
2394
|
search_doc_touch('c', rid, 0); |
|
2395
|
assert( manifest_event_triggers_are_enabled ); |
|
2396
|
zCom = db_text(0, |
|
2397
|
"REPLACE INTO event(type,mtime,objid,user,comment," |
|
2398
|
"bgcolor,euser,ecomment,omtime)" |
|
2399
|
"VALUES('ci'," |
|
2400
|
" coalesce(" |
|
2401
|
" (SELECT julianday(value) FROM tagxref WHERE tagid=%d AND rid=%d)," |
|
2402
|
" %.17g" |
|
2403
|
" )," |
|
2404
|
" %d,%Q,%Q," |
|
2405
|
" (SELECT value FROM tagxref WHERE tagid=%d AND rid=%d AND tagtype>0)," |
|
2406
|
" (SELECT value FROM tagxref WHERE tagid=%d AND rid=%d)," |
|
2407
|
" (SELECT value FROM tagxref WHERE tagid=%d AND rid=%d),%.17g)" |
|
2408
|
"RETURNING coalesce(ecomment,comment);", |
|
2409
|
TAG_DATE, rid, p->rDate, |
|
2410
|
rid, p->zUser, p->zComment, |
|
2411
|
TAG_BGCOLOR, rid, |
|
2412
|
TAG_USER, rid, |
|
2413
|
TAG_COMMENT, rid, p->rDate |
|
2414
|
); |
|
2415
|
backlink_extract(zCom, MT_NONE, rid, BKLNK_COMMENT, p->rDate, 1); |
|
2416
|
fossil_free(zCom); |
|
2417
|
|
|
2418
|
/* If this is a delta-manifest, record the fact that this repository |
|
2419
|
** contains delta manifests, to free the "commit" logic to generate |
|
2420
|
** new delta manifests. |
|
2421
|
*/ |
|
2422
|
if( p->zBaseline!=0 ){ |
|
2423
|
static int once = 1; |
|
2424
|
if( once ){ |
|
2425
|
db_set_int("seen-delta-manifest", 1, 0); |
|
2426
|
once = 0; |
|
2427
|
} |
|
2428
|
} |
|
2429
|
} |
|
2430
|
} |
|
2431
|
if( p->type==CFTYPE_CLUSTER ){ |
|
2432
|
static Stmt del1; |
|
2433
|
tag_insert("cluster", 1, 0, rid, p->rDate, rid); |
|
2434
|
db_static_prepare(&del1, "DELETE FROM unclustered WHERE rid=:rid"); |
|
2435
|
for(i=0; i<p->nCChild; i++){ |
|
2436
|
int mid; |
|
2437
|
mid = uuid_to_rid(p->azCChild[i], 1); |
|
2438
|
if( mid>0 ){ |
|
2439
|
db_bind_int(&del1, ":rid", mid); |
|
2440
|
db_step(&del1); |
|
2441
|
db_reset(&del1); |
|
2442
|
} |
|
2443
|
} |
|
2444
|
} |
|
2445
|
if( p->type==CFTYPE_CONTROL |
|
2446
|
|| p->type==CFTYPE_MANIFEST |
|
2447
|
|| p->type==CFTYPE_EVENT |
|
2448
|
){ |
|
2449
|
for(i=0; i<p->nTag; i++){ |
|
2450
|
int tid; |
|
2451
|
int type; |
|
2452
|
if( p->aTag[i].zUuid ){ |
|
2453
|
tid = uuid_to_rid(p->aTag[i].zUuid, 1); |
|
2454
|
}else{ |
|
2455
|
tid = rid; |
|
2456
|
} |
|
2457
|
if( tid ){ |
|
2458
|
switch( p->aTag[i].zName[0] ){ |
|
2459
|
case '-': type = 0; break; /* Cancel prior occurrences */ |
|
2460
|
case '+': type = 1; break; /* Apply to target only */ |
|
2461
|
case '*': type = 2; break; /* Propagate to descendants */ |
|
2462
|
default: |
|
2463
|
fossil_error(1, "unknown tag type in manifest: %s", p->aTag); |
|
2464
|
manifest_destroy(p); |
|
2465
|
return 0; |
|
2466
|
} |
|
2467
|
tag_insert(&p->aTag[i].zName[1], type, p->aTag[i].zValue, |
|
2468
|
rid, p->rDate, tid); |
|
2469
|
} |
|
2470
|
} |
|
2471
|
if( parentid ){ |
|
2472
|
tag_propagate_all(parentid); |
|
2473
|
} |
|
2474
|
} |
|
2475
|
if(p->type==CFTYPE_WIKI || p->type==CFTYPE_FORUM |
|
2476
|
|| p->type==CFTYPE_EVENT){ |
|
2477
|
manifest_add_fwt_plink(rid, p); |
|
2478
|
} |
|
2479
|
if( p->type==CFTYPE_WIKI ){ |
|
2480
|
char *zTag = mprintf("wiki-%s", p->zWikiTitle); |
|
2481
|
int prior = 0; |
|
2482
|
char cPrefix; |
|
2483
|
int nWiki; |
|
2484
|
char zLength[40]; |
|
2485
|
|
|
2486
|
while( fossil_isspace(p->zWiki[0]) ) p->zWiki++; |
|
2487
|
nWiki = strlen(p->zWiki); |
|
2488
|
sqlite3_snprintf(sizeof(zLength), zLength, "%d", nWiki); |
|
2489
|
tag_insert(zTag, 1, zLength, rid, p->rDate, rid); |
|
2490
|
fossil_free(zTag); |
|
2491
|
if(p->nParent){ |
|
2492
|
prior = fast_uuid_to_rid(p->azParent[0]); |
|
2493
|
} |
|
2494
|
if( prior ){ |
|
2495
|
content_deltify(prior, &rid, 1, 0); |
|
2496
|
} |
|
2497
|
if( nWiki<=0 ){ |
|
2498
|
cPrefix = '-'; |
|
2499
|
}else if( !prior ){ |
|
2500
|
cPrefix = '+'; |
|
2501
|
}else{ |
|
2502
|
cPrefix = ':'; |
|
2503
|
} |
|
2504
|
search_doc_touch('w',rid,p->zWikiTitle); |
|
2505
|
if( manifest_crosslink_busy ){ |
|
2506
|
add_pending_crosslink('w',p->zWikiTitle); |
|
2507
|
}else{ |
|
2508
|
backlink_wiki_refresh(p->zWikiTitle); |
|
2509
|
} |
|
2510
|
assert( manifest_event_triggers_are_enabled ); |
|
2511
|
db_multi_exec( |
|
2512
|
"REPLACE INTO event(type,mtime,objid,user,comment)" |
|
2513
|
"VALUES('w',%.17g,%d,%Q,'%c%q');", |
|
2514
|
p->rDate, rid, p->zUser, cPrefix, p->zWikiTitle |
|
2515
|
); |
|
2516
|
} |
|
2517
|
if( p->type==CFTYPE_EVENT ){ |
|
2518
|
char *zTag = mprintf("event-%s", p->zEventId); |
|
2519
|
int tagid = tag_findid(zTag, 1); |
|
2520
|
int prior = 0, subsequent; |
|
2521
|
int nWiki; |
|
2522
|
char zLength[40]; |
|
2523
|
Stmt qatt; |
|
2524
|
while( fossil_isspace(p->zWiki[0]) ) p->zWiki++; |
|
2525
|
nWiki = strlen(p->zWiki); |
|
2526
|
sqlite3_snprintf(sizeof(zLength), zLength, "%d", nWiki); |
|
2527
|
tag_insert(zTag, 1, zLength, rid, p->rDate, rid); |
|
2528
|
fossil_free(zTag); |
|
2529
|
if(p->nParent){ |
|
2530
|
prior = fast_uuid_to_rid(p->azParent[0]); |
|
2531
|
} |
|
2532
|
subsequent = db_int(0, |
|
2533
|
/* BUG: this check is only correct if subsequent |
|
2534
|
version has already been crosslinked. */ |
|
2535
|
"SELECT rid FROM tagxref" |
|
2536
|
" WHERE tagid=%d AND mtime>=%.17g AND rid!=%d" |
|
2537
|
" ORDER BY mtime", |
|
2538
|
tagid, p->rDate, rid |
|
2539
|
); |
|
2540
|
if( prior ){ |
|
2541
|
content_deltify(prior, &rid, 1, 0); |
|
2542
|
if( !subsequent ){ |
|
2543
|
db_multi_exec( |
|
2544
|
"DELETE FROM event" |
|
2545
|
" WHERE type='e'" |
|
2546
|
" AND tagid=%d" |
|
2547
|
" AND objid IN (SELECT rid FROM tagxref WHERE tagid=%d)", |
|
2548
|
tagid, tagid |
|
2549
|
); |
|
2550
|
} |
|
2551
|
} |
|
2552
|
if( subsequent ){ |
|
2553
|
content_deltify(rid, &subsequent, 1, 0); |
|
2554
|
}else{ |
|
2555
|
search_doc_touch('e',rid,0); |
|
2556
|
assert( manifest_event_triggers_are_enabled ); |
|
2557
|
db_multi_exec( |
|
2558
|
"REPLACE INTO event(type,mtime,objid,tagid,user,comment,bgcolor)" |
|
2559
|
"VALUES('e',%.17g,%d,%d,%Q,%Q," |
|
2560
|
" (SELECT value FROM tagxref WHERE tagid=%d AND rid=%d));", |
|
2561
|
p->rEventDate, rid, tagid, p->zUser, p->zComment, |
|
2562
|
TAG_BGCOLOR, rid |
|
2563
|
); |
|
2564
|
} |
|
2565
|
/* Locate and update comment for any attachments */ |
|
2566
|
db_prepare(&qatt, |
|
2567
|
"SELECT attachid, src, target, filename FROM attachment" |
|
2568
|
" WHERE target=%Q", |
|
2569
|
p->zEventId |
|
2570
|
); |
|
2571
|
while( db_step(&qatt)==SQLITE_ROW ){ |
|
2572
|
const char *zAttachId = db_column_text(&qatt, 0); |
|
2573
|
const char *zSrc = db_column_text(&qatt, 1); |
|
2574
|
const char *zTarget = db_column_text(&qatt, 2); |
|
2575
|
const char *zName = db_column_text(&qatt, 3); |
|
2576
|
const char isAdd = (zSrc && zSrc[0]) ? 1 : 0; |
|
2577
|
char *zComment; |
|
2578
|
if( isAdd ){ |
|
2579
|
zComment = mprintf( |
|
2580
|
"Add attachment [/artifact/%!S|%h] to" |
|
2581
|
" tech note [/technote/%!S|%S]", |
|
2582
|
zSrc, zName, zTarget, zTarget); |
|
2583
|
}else{ |
|
2584
|
zComment = mprintf( |
|
2585
|
"Delete attachment \"%h\" from" |
|
2586
|
" tech note [/technote/%!S|%S]", |
|
2587
|
zName, zTarget, zTarget); |
|
2588
|
} |
|
2589
|
db_multi_exec("UPDATE event SET comment=%Q, type='e'" |
|
2590
|
" WHERE objid=%Q", |
|
2591
|
zComment, zAttachId); |
|
2592
|
fossil_free(zComment); |
|
2593
|
} |
|
2594
|
db_finalize(&qatt); |
|
2595
|
} |
|
2596
|
if( p->type==CFTYPE_TICKET ){ |
|
2597
|
char *zTag; |
|
2598
|
Stmt qatt; |
|
2599
|
assert( manifest_crosslink_busy==1 ); |
|
2600
|
zTag = mprintf("tkt-%s", p->zTicketUuid); |
|
2601
|
tag_insert(zTag, 1, 0, rid, p->rDate, rid); |
|
2602
|
fossil_free(zTag); |
|
2603
|
add_pending_crosslink('t',p->zTicketUuid); |
|
2604
|
/* Locate and update comment for any attachments */ |
|
2605
|
db_prepare(&qatt, |
|
2606
|
"SELECT attachid, src, target, filename FROM attachment" |
|
2607
|
" WHERE target=%Q", |
|
2608
|
p->zTicketUuid |
|
2609
|
); |
|
2610
|
while( db_step(&qatt)==SQLITE_ROW ){ |
|
2611
|
const char *zAttachId = db_column_text(&qatt, 0); |
|
2612
|
const char *zSrc = db_column_text(&qatt, 1); |
|
2613
|
const char *zTarget = db_column_text(&qatt, 2); |
|
2614
|
const char *zName = db_column_text(&qatt, 3); |
|
2615
|
const char isAdd = (zSrc && zSrc[0]) ? 1 : 0; |
|
2616
|
char *zComment; |
|
2617
|
if( isAdd ){ |
|
2618
|
zComment = mprintf( |
|
2619
|
"Add attachment [/artifact/%!S|%h] to ticket [%!S|%S]", |
|
2620
|
zSrc, zName, zTarget, zTarget); |
|
2621
|
}else{ |
|
2622
|
zComment = mprintf("Delete attachment \"%h\" from ticket [%!S|%S]", |
|
2623
|
zName, zTarget, zTarget); |
|
2624
|
} |
|
2625
|
db_multi_exec("UPDATE event SET comment=%Q, type='t'" |
|
2626
|
" WHERE objid=%Q", |
|
2627
|
zComment, zAttachId); |
|
2628
|
fossil_free(zComment); |
|
2629
|
} |
|
2630
|
db_finalize(&qatt); |
|
2631
|
} |
|
2632
|
if( p->type==CFTYPE_ATTACHMENT ){ |
|
2633
|
char *zComment = 0; |
|
2634
|
const char isAdd = (p->zAttachSrc && p->zAttachSrc[0]) ? 1 : 0; |
|
2635
|
/* We assume that we're attaching to a wiki page until we |
|
2636
|
** prove otherwise (which could on a later artifact if we |
|
2637
|
** process the attachment artifact before the artifact to |
|
2638
|
** which it is attached!) */ |
|
2639
|
char attachToType = 'w'; |
|
2640
|
if( fossil_is_artifact_hash(p->zAttachTarget) ){ |
|
2641
|
if( db_exists("SELECT 1 FROM tag WHERE tagname='tkt-%q'", |
|
2642
|
p->zAttachTarget) |
|
2643
|
){ |
|
2644
|
attachToType = 't'; /* Attaching to known ticket */ |
|
2645
|
}else if( db_exists("SELECT 1 FROM tag WHERE tagname='event-%q'", |
|
2646
|
p->zAttachTarget) |
|
2647
|
){ |
|
2648
|
attachToType = 'e'; /* Attaching to known tech note */ |
|
2649
|
} |
|
2650
|
} |
|
2651
|
db_multi_exec( |
|
2652
|
"INSERT INTO attachment(attachid, mtime, src, target," |
|
2653
|
"filename, comment, user)" |
|
2654
|
"VALUES(%d,%.17g,%Q,%Q,%Q,%Q,%Q);", |
|
2655
|
rid, p->rDate, p->zAttachSrc, p->zAttachTarget, p->zAttachName, |
|
2656
|
(p->zComment ? p->zComment : ""), p->zUser |
|
2657
|
); |
|
2658
|
db_multi_exec( |
|
2659
|
"UPDATE attachment SET isLatest = (mtime==" |
|
2660
|
"(SELECT max(mtime) FROM attachment" |
|
2661
|
" WHERE target=%Q AND filename=%Q))" |
|
2662
|
" WHERE target=%Q AND filename=%Q", |
|
2663
|
p->zAttachTarget, p->zAttachName, |
|
2664
|
p->zAttachTarget, p->zAttachName |
|
2665
|
); |
|
2666
|
if( 'w' == attachToType ){ |
|
2667
|
if( isAdd ){ |
|
2668
|
zComment = mprintf( |
|
2669
|
"Add attachment [/artifact/%!S|%h] to wiki page [%h]", |
|
2670
|
p->zAttachSrc, p->zAttachName, p->zAttachTarget); |
|
2671
|
}else{ |
|
2672
|
zComment = mprintf("Delete attachment \"%h\" from wiki page [%h]", |
|
2673
|
p->zAttachName, p->zAttachTarget); |
|
2674
|
} |
|
2675
|
}else if( 'e' == attachToType ){ |
|
2676
|
if( isAdd ){ |
|
2677
|
zComment = mprintf( |
|
2678
|
"Add attachment [/artifact/%!S|%h] to tech note [/technote/%!S|%S]", |
|
2679
|
p->zAttachSrc, p->zAttachName, p->zAttachTarget, p->zAttachTarget); |
|
2680
|
}else{ |
|
2681
|
zComment = mprintf( |
|
2682
|
"Delete attachment \"/artifact/%!S|%h\" from" |
|
2683
|
" tech note [/technote/%!S|%S]", |
|
2684
|
p->zAttachName, p->zAttachName, |
|
2685
|
p->zAttachTarget,p->zAttachTarget); |
|
2686
|
} |
|
2687
|
}else{ |
|
2688
|
if( isAdd ){ |
|
2689
|
zComment = mprintf( |
|
2690
|
"Add attachment [/artifact/%!S|%h] to ticket [%!S|%S]", |
|
2691
|
p->zAttachSrc, p->zAttachName, p->zAttachTarget, p->zAttachTarget); |
|
2692
|
}else{ |
|
2693
|
zComment = mprintf("Delete attachment \"%h\" from ticket [%!S|%S]", |
|
2694
|
p->zAttachName, p->zAttachTarget, p->zAttachTarget); |
|
2695
|
} |
|
2696
|
} |
|
2697
|
assert( manifest_event_triggers_are_enabled ); |
|
2698
|
db_multi_exec( |
|
2699
|
"REPLACE INTO event(type,mtime,objid,user,comment)" |
|
2700
|
"VALUES('%c',%.17g,%d,%Q,%Q)", |
|
2701
|
attachToType, p->rDate, rid, p->zUser, zComment |
|
2702
|
); |
|
2703
|
fossil_free(zComment); |
|
2704
|
} |
|
2705
|
if( p->type==CFTYPE_CONTROL ){ |
|
2706
|
Blob comment; |
|
2707
|
int i; |
|
2708
|
const char *zName; |
|
2709
|
const char *zValue; |
|
2710
|
const char *zTagUuid; |
|
2711
|
int branchMove = 0; |
|
2712
|
blob_zero(&comment); |
|
2713
|
if( p->zComment ){ |
|
2714
|
blob_appendf(&comment, " %s.", p->zComment); |
|
2715
|
} |
|
2716
|
/* Next loop expects tags to be sorted on hash, so sort it. */ |
|
2717
|
qsort(p->aTag, p->nTag, sizeof(p->aTag[0]), tag_compare); |
|
2718
|
for(i=0; i<p->nTag; i++){ |
|
2719
|
zTagUuid = p->aTag[i].zUuid; |
|
2720
|
if( !zTagUuid ) continue; |
|
2721
|
if( i==0 || fossil_strcmp(zTagUuid, p->aTag[i-1].zUuid)!=0 ){ |
|
2722
|
blob_appendf(&comment, |
|
2723
|
" Edit [%!S|%S]:", |
|
2724
|
zTagUuid, zTagUuid); |
|
2725
|
branchMove = 0; |
|
2726
|
if( permitHooks && db_exists("SELECT 1 FROM event, blob" |
|
2727
|
" WHERE event.type='ci' AND event.objid=blob.rid" |
|
2728
|
" AND blob.uuid=%Q", zTagUuid) ){ |
|
2729
|
zScript = xfer_commit_code(); |
|
2730
|
fossil_free(zUuid); |
|
2731
|
zUuid = fossil_strdup(zTagUuid); |
|
2732
|
} |
|
2733
|
} |
|
2734
|
zName = p->aTag[i].zName; |
|
2735
|
zValue = p->aTag[i].zValue; |
|
2736
|
if( strcmp(zName, "*branch")==0 ){ |
|
2737
|
blob_appendf(&comment, |
|
2738
|
" Move to branch [/timeline?r=%t&nd&dp=%!S&unhide | %h].", |
|
2739
|
zValue, zTagUuid, zValue); |
|
2740
|
branchMove = 1; |
|
2741
|
continue; |
|
2742
|
}else if( strcmp(zName, "*bgcolor")==0 ){ |
|
2743
|
blob_appendf(&comment, |
|
2744
|
" Change branch background color to \"%h\".", zValue); |
|
2745
|
continue; |
|
2746
|
}else if( strcmp(zName, "+bgcolor")==0 ){ |
|
2747
|
blob_appendf(&comment, |
|
2748
|
" Change background color to \"%h\".", zValue); |
|
2749
|
continue; |
|
2750
|
}else if( strcmp(zName, "-bgcolor")==0 ){ |
|
2751
|
blob_appendf(&comment, " Cancel background color"); |
|
2752
|
}else if( strcmp(zName, "+comment")==0 ){ |
|
2753
|
blob_appendf(&comment, " Edit check-in comment."); |
|
2754
|
continue; |
|
2755
|
}else if( strcmp(zName, "+user")==0 ){ |
|
2756
|
blob_appendf(&comment, " Change user to \"%h\".", zValue); |
|
2757
|
continue; |
|
2758
|
}else if( strcmp(zName, "+date")==0 ){ |
|
2759
|
blob_appendf(&comment, " Timestamp %h.", zValue); |
|
2760
|
continue; |
|
2761
|
}else if( memcmp(zName, "-sym-",5)==0 ){ |
|
2762
|
if( !branchMove ){ |
|
2763
|
blob_appendf(&comment, " Cancel tag \"%h\"", &zName[5]); |
|
2764
|
}else{ |
|
2765
|
continue; |
|
2766
|
} |
|
2767
|
}else if( memcmp(zName, "*sym-",5)==0 ){ |
|
2768
|
if( !branchMove ){ |
|
2769
|
blob_appendf(&comment, " Add propagating tag \"%h\"", &zName[5]); |
|
2770
|
}else{ |
|
2771
|
continue; |
|
2772
|
} |
|
2773
|
}else if( memcmp(zName, "+sym-",5)==0 ){ |
|
2774
|
blob_appendf(&comment, " Add tag \"%h\"", &zName[5]); |
|
2775
|
}else if( strcmp(zName, "+closed")==0 ){ |
|
2776
|
blob_append(&comment, " Mark \"Closed\"", -1); |
|
2777
|
}else if( strcmp(zName, "-closed")==0 ){ |
|
2778
|
blob_append(&comment, " Remove the \"Closed\" mark", -1); |
|
2779
|
}else { |
|
2780
|
if( zName[0]=='-' ){ |
|
2781
|
blob_appendf(&comment, " Cancel \"%h\"", &zName[1]); |
|
2782
|
}else if( zName[0]=='+' ){ |
|
2783
|
blob_appendf(&comment, " Add \"%h\"", &zName[1]); |
|
2784
|
}else{ |
|
2785
|
blob_appendf(&comment, " Add propagating \"%h\"", &zName[1]); |
|
2786
|
} |
|
2787
|
if( zValue && zValue[0] ){ |
|
2788
|
blob_appendf(&comment, " with value \"%h\".", zValue); |
|
2789
|
}else{ |
|
2790
|
blob_appendf(&comment, "."); |
|
2791
|
} |
|
2792
|
continue; |
|
2793
|
} |
|
2794
|
if( zValue && zValue[0] ){ |
|
2795
|
blob_appendf(&comment, " with note \"%h\".", zValue); |
|
2796
|
}else{ |
|
2797
|
blob_appendf(&comment, "."); |
|
2798
|
} |
|
2799
|
} |
|
2800
|
/*blob_appendf(&comment, " [[/info/%S | details]]");*/ |
|
2801
|
if( blob_size(&comment)==0 ) blob_append(&comment, " ", 1); |
|
2802
|
assert( manifest_event_triggers_are_enabled ); |
|
2803
|
db_multi_exec( |
|
2804
|
"REPLACE INTO event(type,mtime,objid,user,comment)" |
|
2805
|
"VALUES('g',%.17g,%d,%Q,%Q)", |
|
2806
|
p->rDate, rid, p->zUser, blob_str(&comment)+1 |
|
2807
|
); |
|
2808
|
blob_reset(&comment); |
|
2809
|
} |
|
2810
|
if( p->type==CFTYPE_FORUM ){ |
|
2811
|
int froot, fprev, firt; |
|
2812
|
char *zFType; |
|
2813
|
char *zTitle; |
|
2814
|
|
|
2815
|
assert( 0==zUuid ); |
|
2816
|
schema_forum(); |
|
2817
|
search_doc_touch('f', rid, 0); |
|
2818
|
froot = p->zThreadRoot ? uuid_to_rid(p->zThreadRoot, 1) : rid; |
|
2819
|
fprev = p->nParent ? uuid_to_rid(p->azParent[0],1) : 0; |
|
2820
|
firt = p->zInReplyTo ? uuid_to_rid(p->zInReplyTo,1) : 0; |
|
2821
|
db_multi_exec( |
|
2822
|
"REPLACE INTO forumpost(fpid,froot,fprev,firt,fmtime)" |
|
2823
|
"VALUES(%d,%d,nullif(%d,0),nullif(%d,0),%.17g)", |
|
2824
|
p->rid, froot, fprev, firt, p->rDate |
|
2825
|
); |
|
2826
|
if( firt==0 ){ |
|
2827
|
/* This is the start of a new thread, either the initial entry |
|
2828
|
** or an edit of the initial entry. */ |
|
2829
|
zTitle = p->zThreadTitle; |
|
2830
|
if( zTitle==0 || zTitle[0]==0 ){ |
|
2831
|
zTitle = "(Deleted)"; |
|
2832
|
} |
|
2833
|
zFType = fprev ? "Edit" : "Post"; |
|
2834
|
assert( manifest_event_triggers_are_enabled ); |
|
2835
|
db_multi_exec( |
|
2836
|
"REPLACE INTO event(type,mtime,objid,user,comment)" |
|
2837
|
"VALUES('f',%.17g,%d,%Q,'%q: %q')", |
|
2838
|
p->rDate, rid, p->zUser, zFType, zTitle |
|
2839
|
); |
|
2840
|
/* |
|
2841
|
** If this edit is the most recent, then make it the title for |
|
2842
|
** all other entries for the same thread |
|
2843
|
*/ |
|
2844
|
if( !db_exists("SELECT 1 FROM forumpost WHERE froot=%d AND firt=0" |
|
2845
|
" AND fpid!=%d AND fmtime>%.17g", froot, rid, p->rDate) |
|
2846
|
){ |
|
2847
|
/* This entry establishes a new title for all entries on the thread */ |
|
2848
|
db_multi_exec( |
|
2849
|
"UPDATE event" |
|
2850
|
" SET comment=substr(comment,1,instr(comment,':')) || ' %q'" |
|
2851
|
" WHERE objid IN (SELECT fpid FROM forumpost WHERE froot=%d)", |
|
2852
|
zTitle, froot |
|
2853
|
); |
|
2854
|
} |
|
2855
|
}else{ |
|
2856
|
/* This is a reply to a prior post. Take the title from the root. */ |
|
2857
|
zTitle = db_text(0, "SELECT substr(comment,instr(comment,':')+2)" |
|
2858
|
" FROM event WHERE objid=%d", froot); |
|
2859
|
if( zTitle==0 ) zTitle = fossil_strdup("<i>Unknown</i>"); |
|
2860
|
if( p->zWiki[0]==0 ){ |
|
2861
|
zFType = "Delete reply"; |
|
2862
|
}else if( fprev ){ |
|
2863
|
zFType = "Edit reply"; |
|
2864
|
}else{ |
|
2865
|
zFType = "Reply"; |
|
2866
|
} |
|
2867
|
assert( manifest_event_triggers_are_enabled ); |
|
2868
|
db_multi_exec( |
|
2869
|
"REPLACE INTO event(type,mtime,objid,user,comment)" |
|
2870
|
"VALUES('f',%.17g,%d,%Q,'%q: %q')", |
|
2871
|
p->rDate, rid, p->zUser, zFType, zTitle |
|
2872
|
); |
|
2873
|
fossil_free(zTitle); |
|
2874
|
} |
|
2875
|
if( p->zWiki[0] ){ |
|
2876
|
int mimetype = parse_mimetype(p->zMimetype); |
|
2877
|
backlink_extract(p->zWiki, mimetype, rid, BKLNK_FORUM, p->rDate, 1); |
|
2878
|
} |
|
2879
|
} |
|
2880
|
|
|
2881
|
db_end_transaction(0); |
|
2882
|
if( permitHooks ){ |
|
2883
|
rc = xfer_run_common_script(); |
|
2884
|
if( rc==TH_OK ){ |
|
2885
|
rc = xfer_run_script(zScript, zUuid, 0); |
|
2886
|
} |
|
2887
|
} |
|
2888
|
fossil_free(zUuid); |
|
2889
|
if( p->type==CFTYPE_MANIFEST ){ |
|
2890
|
manifest_cache_insert(p); |
|
2891
|
}else{ |
|
2892
|
manifest_destroy(p); |
|
2893
|
} |
|
2894
|
assert( blob_is_reset(pContent) ); |
|
2895
|
return ( rc!=TH_ERROR ); |
|
2896
|
} |
|
2897
|
|
|
2898
|
/* |
|
2899
|
** COMMAND: test-crosslink |
|
2900
|
** |
|
2901
|
** Usage: %fossil test-crosslink RECORDID |
|
2902
|
** |
|
2903
|
** Run the manifest_crosslink() routine on the artifact with the given |
|
2904
|
** record ID. This is typically done in the debugger. |
|
2905
|
*/ |
|
2906
|
void test_crosslink_cmd(void){ |
|
2907
|
int rid; |
|
2908
|
Blob content; |
|
2909
|
db_find_and_open_repository(0, 0); |
|
2910
|
if( g.argc!=3 ) usage("RECORDID"); |
|
2911
|
rid = name_to_rid(g.argv[2]); |
|
2912
|
content_get(rid, &content); |
|
2913
|
manifest_crosslink(rid, &content, MC_NONE); |
|
2914
|
} |
|
2915
|
|
|
2916
|
/* |
|
2917
|
** For a given CATYPE_... value, returns a human-friendly name, or |
|
2918
|
** NULL if typeId is unknown or is CFTYPE_ANY. The names returned by |
|
2919
|
** this function are geared towards use with artifact_to_json(), and |
|
2920
|
** may differ from some historical uses. e.g. CFTYPE_CONTROL artifacts |
|
2921
|
** are called "tag" artifacts by this function. |
|
2922
|
*/ |
|
2923
|
const char * artifact_type_to_name(int typeId){ |
|
2924
|
switch(typeId){ |
|
2925
|
case CFTYPE_MANIFEST: return "checkin"; |
|
2926
|
case CFTYPE_CLUSTER: return "cluster"; |
|
2927
|
case CFTYPE_CONTROL: return "tag"; |
|
2928
|
case CFTYPE_WIKI: return "wiki"; |
|
2929
|
case CFTYPE_TICKET: return "ticket"; |
|
2930
|
case CFTYPE_ATTACHMENT: return "attachment"; |
|
2931
|
case CFTYPE_EVENT: return "technote"; |
|
2932
|
case CFTYPE_FORUM: return "forumpost"; |
|
2933
|
} |
|
2934
|
return NULL; |
|
2935
|
} |
|
2936
|
|
|
2937
|
/* |
|
2938
|
** Creates a JSON representation of p, appending it to b. |
|
2939
|
** |
|
2940
|
** b is not cleared before rendering, so the caller needs to do that |
|
2941
|
** if it's important for their use case. |
|
2942
|
** |
|
2943
|
** Pedantic note: this routine traverses p->aFile directly, rather |
|
2944
|
** than using manifest_file_next(), so that delta manifests are |
|
2945
|
** rendered as-is instead of containing their derived F-cards. If that |
|
2946
|
** policy is ever changed, p will need to be non-const. |
|
2947
|
*/ |
|
2948
|
void artifact_to_json(Manifest const *p, Blob *b){ |
|
2949
|
int i; |
|
2950
|
|
|
2951
|
blob_append_literal(b, "{"); |
|
2952
|
blob_appendf(b, "\"uuid\":\"%z\"", rid_to_uuid(p->rid)); |
|
2953
|
/*blob_appendf(b, ", \"rid\": %d", p->rid); not portable across repos*/ |
|
2954
|
blob_appendf(b, ",\"type\":%!j", artifact_type_to_name(p->type)); |
|
2955
|
#define ISA(TYPE) if( p->type==TYPE ) |
|
2956
|
#define CARD_LETTER(LETTER) \ |
|
2957
|
blob_append_literal(b, ",\"" #LETTER "\":") |
|
2958
|
#define CARD_STR(LETTER, VAL) \ |
|
2959
|
assert( VAL ); CARD_LETTER(LETTER); blob_appendf(b, "%!j", VAL) |
|
2960
|
#define CARD_STR2(LETTER, VAL) \ |
|
2961
|
if( VAL ) { CARD_STR(LETTER, VAL); } (void)0 |
|
2962
|
#define STR_OR_NULL(VAL) \ |
|
2963
|
if( VAL ) blob_appendf(b, "%!j", VAL); \ |
|
2964
|
else blob_append(b, "null", 4) |
|
2965
|
#define KVP_STR(ADDCOMMA, KEY,VAL) \ |
|
2966
|
if(ADDCOMMA) blob_append_char(b, ','); \ |
|
2967
|
blob_appendf(b, "%!j:", #KEY); \ |
|
2968
|
STR_OR_NULL(VAL) |
|
2969
|
|
|
2970
|
ISA( CFTYPE_ATTACHMENT ){ |
|
2971
|
CARD_LETTER(A); |
|
2972
|
blob_append_char(b, '{'); |
|
2973
|
KVP_STR(0, filename, p->zAttachName); |
|
2974
|
KVP_STR(1, target, p->zAttachTarget); |
|
2975
|
KVP_STR(1, source, p->zAttachSrc); |
|
2976
|
blob_append_char(b, '}'); |
|
2977
|
} |
|
2978
|
CARD_STR2(B, p->zBaseline); |
|
2979
|
CARD_STR2(C, p->zComment); |
|
2980
|
CARD_LETTER(D); blob_appendf(b, "%f", p->rDate); |
|
2981
|
ISA( CFTYPE_EVENT ){ |
|
2982
|
blob_appendf(b, ", \"E\":{\"time\":%f,\"id\":%!j}", |
|
2983
|
p->rEventDate, p->zEventId); |
|
2984
|
} |
|
2985
|
ISA( CFTYPE_MANIFEST ){ |
|
2986
|
CARD_LETTER(F); |
|
2987
|
blob_append_char(b, '['); |
|
2988
|
for( i = 0; i < p->nFile; ++i ){ |
|
2989
|
ManifestFile const * const pF = &p->aFile[i]; |
|
2990
|
if( i>0 ) blob_append_char(b, ','); |
|
2991
|
blob_append_char(b, '{'); |
|
2992
|
KVP_STR(0, name, pF->zName); |
|
2993
|
KVP_STR(1, uuid, pF->zUuid); |
|
2994
|
KVP_STR(1, perm, pF->zPerm); |
|
2995
|
KVP_STR(1, rename, pF->zPrior); |
|
2996
|
blob_append_char(b, '}'); |
|
2997
|
} |
|
2998
|
/* Special case: model check-ins with no F-card as having an empty |
|
2999
|
** array, rather than no F-cards, to hypothetically simplify |
|
3000
|
** handling in JSON queries. */ |
|
3001
|
blob_append_char(b, ']'); |
|
3002
|
} |
|
3003
|
CARD_STR2(G, p->zThreadRoot); |
|
3004
|
ISA( CFTYPE_FORUM ){ |
|
3005
|
CARD_LETTER(H); |
|
3006
|
STR_OR_NULL( (p->zThreadTitle && *p->zThreadTitle) ? p->zThreadTitle : NULL); |
|
3007
|
CARD_STR2(I, p->zInReplyTo); |
|
3008
|
} |
|
3009
|
if( p->nField ){ |
|
3010
|
CARD_LETTER(J); |
|
3011
|
blob_append_char(b, '['); |
|
3012
|
for( i = 0; i < p->nField; ++i ){ |
|
3013
|
const char * zName = p->aField[i].zName; |
|
3014
|
if( i>0 ) blob_append_char(b, ','); |
|
3015
|
blob_append_char(b, '{'); |
|
3016
|
KVP_STR(0, name, '+'==*zName ? &zName[1] : zName); |
|
3017
|
KVP_STR(1, value, p->aField[i].zValue); |
|
3018
|
blob_appendf(b, ",\"append\":%s", '+'==*zName ? "true" : "false"); |
|
3019
|
blob_append_char(b, '}'); |
|
3020
|
} |
|
3021
|
blob_append_char(b, ']'); |
|
3022
|
} |
|
3023
|
CARD_STR2(K, p->zTicketUuid); |
|
3024
|
CARD_STR2(L, p->zWikiTitle); |
|
3025
|
ISA( CFTYPE_CLUSTER ){ |
|
3026
|
CARD_LETTER(M); |
|
3027
|
blob_append_char(b, '['); |
|
3028
|
for( i = 0; i < p->nCChild; ++i ){ |
|
3029
|
if( i>0 ) blob_append_char(b, ','); |
|
3030
|
blob_appendf(b, "%!j", p->azCChild[i]); |
|
3031
|
} |
|
3032
|
blob_append_char(b, ']'); |
|
3033
|
} |
|
3034
|
CARD_STR2(N, p->zMimetype); |
|
3035
|
ISA( CFTYPE_MANIFEST || p->nParent>0 ){ |
|
3036
|
CARD_LETTER(P); |
|
3037
|
blob_append_char(b, '['); |
|
3038
|
for( i = 0; i < p->nParent; ++i ){ |
|
3039
|
if( i>0 ) blob_append_char(b, ','); |
|
3040
|
blob_appendf(b, "%!j", p->azParent[i]); |
|
3041
|
} |
|
3042
|
/* Special case: model check-ins with no P-card as having an empty |
|
3043
|
** array, as per F-cards. */ |
|
3044
|
blob_append_char(b, ']'); |
|
3045
|
} |
|
3046
|
if( p->nCherrypick ){ |
|
3047
|
CARD_LETTER(Q); |
|
3048
|
blob_append_char(b, '['); |
|
3049
|
for( i = 0; i < p->nCherrypick; ++i ){ |
|
3050
|
if( i>0 ) blob_append_char(b, ','); |
|
3051
|
blob_append_char(b, '{'); |
|
3052
|
blob_appendf(b, "\"type\":\"%c\"", p->aCherrypick[i].zCPTarget[0]); |
|
3053
|
KVP_STR(1, target, &p->aCherrypick[i].zCPTarget[1]); |
|
3054
|
KVP_STR(1, base, p->aCherrypick[i].zCPBase); |
|
3055
|
blob_append_char(b, '}'); |
|
3056
|
} |
|
3057
|
blob_append_char(b, ']'); |
|
3058
|
} |
|
3059
|
CARD_STR2(R, p->zRepoCksum); |
|
3060
|
if( p->nTag ){ |
|
3061
|
CARD_LETTER(T); |
|
3062
|
blob_append_char(b, '['); |
|
3063
|
for( i = 0; i < p->nTag; ++i ){ |
|
3064
|
const char *zName = p->aTag[i].zName; |
|
3065
|
if( i>0 ) blob_append_char(b, ','); |
|
3066
|
blob_append_char(b, '{'); |
|
3067
|
blob_appendf(b, "\"type\":\"%c\"", *zName); |
|
3068
|
KVP_STR(1, name, &zName[1]); |
|
3069
|
KVP_STR(1, target, p->aTag[i].zUuid ? p->aTag[i].zUuid : "*") |
|
3070
|
/* We could arguably resolve the "*" as null or p's uuid. */; |
|
3071
|
KVP_STR(1, value, p->aTag[i].zValue); |
|
3072
|
blob_append_char(b, '}'); |
|
3073
|
} |
|
3074
|
blob_append_char(b, ']'); |
|
3075
|
} |
|
3076
|
CARD_STR2(U, p->zUser); |
|
3077
|
if( p->zWiki || CFTYPE_WIKI==p->type || CFTYPE_FORUM==p->type |
|
3078
|
|| CFTYPE_EVENT==p->type ){ |
|
3079
|
CARD_LETTER(W); |
|
3080
|
STR_OR_NULL((p->zWiki && *p->zWiki) ? p->zWiki : NULL); |
|
3081
|
} |
|
3082
|
blob_append_literal(b, "}"); |
|
3083
|
#undef CARD_FMT |
|
3084
|
#undef CARD_LETTER |
|
3085
|
#undef CARD_STR |
|
3086
|
#undef CARD_STR2 |
|
3087
|
#undef ISA |
|
3088
|
#undef KVP_STR |
|
3089
|
#undef STR_OR_NULL |
|
3090
|
} |
|
3091
|
|
|
3092
|
/* |
|
3093
|
** Convenience wrapper around artifact_to_json() which expects rid to |
|
3094
|
** be the blob.rid of any artifact type. If it can load a Manifest |
|
3095
|
** with that rid, it returns rid, else it returns 0. |
|
3096
|
*/ |
|
3097
|
int artifact_to_json_by_rid(int rid, Blob *pOut){ |
|
3098
|
Manifest * const p = manifest_get(rid, CFTYPE_ANY, 0); |
|
3099
|
if( p ){ |
|
3100
|
artifact_to_json(p, pOut); |
|
3101
|
manifest_destroy(p); |
|
3102
|
}else{ |
|
3103
|
rid = 0; |
|
3104
|
} |
|
3105
|
return rid; |
|
3106
|
} |
|
3107
|
|
|
3108
|
/* |
|
3109
|
** Convenience wrapper around artifact_to_json() which accepts any |
|
3110
|
** artifact name which is legal for symbolic_name_to_rid(). On success |
|
3111
|
** it returns the rid of the artifact. Returns 0 if no such artifact |
|
3112
|
** exists and a negative value if the name is ambiguous. |
|
3113
|
** |
|
3114
|
** pOut is not cleared before rendering, so the caller needs to do |
|
3115
|
** that if it's important for their use case. |
|
3116
|
*/ |
|
3117
|
int artifact_to_json_by_name(const char *zName, Blob *pOut){ |
|
3118
|
const int rid = symbolic_name_to_rid(zName, 0); |
|
3119
|
return rid>0 |
|
3120
|
? artifact_to_json_by_rid(rid, pOut) |
|
3121
|
: rid; |
|
3122
|
} |
|
3123
|
|
|
3124
|
/* |
|
3125
|
** SQLite UDF for artifact_to_json(). Its single argument should be |
|
3126
|
** either an INTEGER (blob.rid value) or a TEXT symbolic artifact |
|
3127
|
** name, as per symbolic_name_to_rid(). If an artifact is found then |
|
3128
|
** the result of the UDF is that JSON as a string, else it evaluates |
|
3129
|
** to NULL. |
|
3130
|
*/ |
|
3131
|
void artifact_to_json_sql_func( |
|
3132
|
sqlite3_context *context, |
|
3133
|
int argc, |
|
3134
|
sqlite3_value **argv |
|
3135
|
){ |
|
3136
|
int rid = 0; |
|
3137
|
Blob b = empty_blob; |
|
3138
|
|
|
3139
|
if(1 != argc){ |
|
3140
|
goto error_usage; |
|
3141
|
} |
|
3142
|
switch( sqlite3_value_type(argv[0]) ){ |
|
3143
|
case SQLITE_INTEGER: |
|
3144
|
rid = artifact_to_json_by_rid(sqlite3_value_int(argv[0]), &b); |
|
3145
|
break; |
|
3146
|
case SQLITE_TEXT:{ |
|
3147
|
const char * z = (const char *)sqlite3_value_text(argv[0]); |
|
3148
|
if( z ){ |
|
3149
|
rid = artifact_to_json_by_name(z, &b); |
|
3150
|
} |
|
3151
|
break; |
|
3152
|
} |
|
3153
|
default: |
|
3154
|
goto error_usage; |
|
3155
|
} |
|
3156
|
if( rid>0 ){ |
|
3157
|
sqlite3_result_text(context, blob_str(&b), blob_size(&b), |
|
3158
|
SQLITE_TRANSIENT); |
|
3159
|
blob_reset(&b); |
|
3160
|
}else{ |
|
3161
|
/* We should arguably error out if rid<0 (ambiguous name) */ |
|
3162
|
sqlite3_result_null(context); |
|
3163
|
} |
|
3164
|
return; |
|
3165
|
error_usage: |
|
3166
|
sqlite3_result_error(context, "Expecting one argument: blob.rid or " |
|
3167
|
"artifact symbolic name", -1); |
|
3168
|
} |
|
3169
|
|
|
3170
|
|
|
3171
|
|
|
3172
|
/* |
|
3173
|
** COMMAND: test-artifact-to-json |
|
3174
|
** |
|
3175
|
** Usage: %fossil test-artifact-to-json ?-pretty|-p? symbolic-name [...names] |
|
3176
|
** |
|
3177
|
** Tests the artifact_to_json() and artifact_to_json_by_name() APIs. |
|
3178
|
*/ |
|
3179
|
void test_manifest_to_json(void){ |
|
3180
|
int i; |
|
3181
|
Blob b = empty_blob; |
|
3182
|
Stmt q; |
|
3183
|
const int bPretty = find_option("pretty","p",0)!=0; |
|
3184
|
int nErr = 0; |
|
3185
|
|
|
3186
|
db_find_and_open_repository(0,0); |
|
3187
|
db_prepare(&q, "select json_pretty(:json)"); |
|
3188
|
for( i=2; i<g.argc; ++i ){ |
|
3189
|
char const *zName = g.argv[i]; |
|
3190
|
const int rc = artifact_to_json_by_name(zName, &b); |
|
3191
|
if( rc<=0 ){ |
|
3192
|
++nErr; |
|
3193
|
fossil_warning("Error reading artifact %Q", zName); |
|
3194
|
continue; |
|
3195
|
}else if( bPretty ){ |
|
3196
|
db_bind_blob(&q, ":json", &b); |
|
3197
|
b.nUsed = 0; |
|
3198
|
db_step(&q); |
|
3199
|
db_column_blob(&q, 0, &b); |
|
3200
|
db_reset(&q); |
|
3201
|
} |
|
3202
|
fossil_print("%b\n", &b); |
|
3203
|
blob_reset(&b); |
|
3204
|
} |
|
3205
|
db_finalize(&q); |
|
3206
|
if( nErr ){ |
|
3207
|
fossil_warning("Error count: %d", nErr); |
|
3208
|
} |
|
3209
|
} |
|
3210
|
|