|
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 to implement the "info" command. The |
|
19
|
** "info" command gives command-line access to information about |
|
20
|
** the current tree, or a particular artifact or check-in. |
|
21
|
*/ |
|
22
|
#include "config.h" |
|
23
|
#include "info.h" |
|
24
|
#include <assert.h> |
|
25
|
|
|
26
|
/* |
|
27
|
** Return a string (in memory obtained from malloc) holding a |
|
28
|
** comma-separated list of tags that apply to check-in with |
|
29
|
** record-id rid. If the "propagatingOnly" flag is true, then only |
|
30
|
** show branch tags (tags that propagate to children). |
|
31
|
** |
|
32
|
** Return NULL if there are no such tags. |
|
33
|
*/ |
|
34
|
char *info_tags_of_checkin(int rid, int propagatingOnly){ |
|
35
|
char *zTags; |
|
36
|
zTags = db_text(0, "SELECT group_concat(substr(tagname, 5), ', ')" |
|
37
|
" FROM tagxref, tag" |
|
38
|
" WHERE tagxref.rid=%d AND tagxref.tagtype>%d" |
|
39
|
" AND tag.tagid=tagxref.tagid" |
|
40
|
" AND tag.tagname GLOB 'sym-*'", |
|
41
|
rid, propagatingOnly!=0); |
|
42
|
return zTags; |
|
43
|
} |
|
44
|
|
|
45
|
|
|
46
|
/* |
|
47
|
** Print common information about a particular record. |
|
48
|
** |
|
49
|
** * The artifact hash |
|
50
|
** * The record ID |
|
51
|
** * mtime and ctime |
|
52
|
** * who signed it |
|
53
|
** |
|
54
|
*/ |
|
55
|
void show_common_info( |
|
56
|
int rid, /* The rid for the check-in to display info for */ |
|
57
|
const char *zRecDesc, /* Brief record description; e.g. "check-out:" */ |
|
58
|
int showComment, /* True to show the check-in comment */ |
|
59
|
int showFamily /* True to show parents and children */ |
|
60
|
){ |
|
61
|
Stmt q; |
|
62
|
char *zComment = 0; |
|
63
|
char *zTags; |
|
64
|
char *zDate; |
|
65
|
char *zUuid; |
|
66
|
zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); |
|
67
|
if( zUuid ){ |
|
68
|
zDate = db_text(0, |
|
69
|
"SELECT datetime(mtime) || ' UTC' FROM event WHERE objid=%d", |
|
70
|
rid |
|
71
|
); |
|
72
|
/* 01234567890123 */ |
|
73
|
fossil_print("%-13s %.40s %s\n", zRecDesc, zUuid, zDate ? zDate : ""); |
|
74
|
free(zDate); |
|
75
|
if( showComment ){ |
|
76
|
zComment = db_text(0, |
|
77
|
"SELECT coalesce(ecomment,comment) || " |
|
78
|
" ' (user: ' || coalesce(euser,user,'?') || ')' " |
|
79
|
" FROM event WHERE objid=%d", |
|
80
|
rid |
|
81
|
); |
|
82
|
} |
|
83
|
free(zUuid); |
|
84
|
} |
|
85
|
if( showFamily ){ |
|
86
|
db_prepare(&q, "SELECT uuid, pid, isprim FROM plink JOIN blob ON pid=rid " |
|
87
|
" WHERE cid=%d" |
|
88
|
" ORDER BY isprim DESC, mtime DESC /*sort*/", rid); |
|
89
|
while( db_step(&q)==SQLITE_ROW ){ |
|
90
|
const char *zUuid = db_column_text(&q, 0); |
|
91
|
const char *zType = db_column_int(&q, 2) ? "parent:" : "merged-from:"; |
|
92
|
zDate = db_text("", |
|
93
|
"SELECT datetime(mtime) || ' UTC' FROM event WHERE objid=%d", |
|
94
|
db_column_int(&q, 1) |
|
95
|
); |
|
96
|
fossil_print("%-13s %.40s %s\n", zType, zUuid, zDate); |
|
97
|
free(zDate); |
|
98
|
} |
|
99
|
db_finalize(&q); |
|
100
|
db_prepare(&q, "SELECT uuid, cid, isprim FROM plink JOIN blob ON cid=rid " |
|
101
|
" WHERE pid=%d" |
|
102
|
" ORDER BY isprim DESC, mtime DESC /*sort*/", rid); |
|
103
|
while( db_step(&q)==SQLITE_ROW ){ |
|
104
|
const char *zUuid = db_column_text(&q, 0); |
|
105
|
const char *zType = db_column_int(&q, 2) ? "child:" : "merged-into:"; |
|
106
|
zDate = db_text("", |
|
107
|
"SELECT datetime(mtime) || ' UTC' FROM event WHERE objid=%d", |
|
108
|
db_column_int(&q, 1) |
|
109
|
); |
|
110
|
fossil_print("%-13s %.40s %s\n", zType, zUuid, zDate); |
|
111
|
free(zDate); |
|
112
|
} |
|
113
|
db_finalize(&q); |
|
114
|
} |
|
115
|
zTags = info_tags_of_checkin(rid, 0); |
|
116
|
if( zTags && zTags[0] ){ |
|
117
|
fossil_print("tags: %s\n", zTags); |
|
118
|
} |
|
119
|
free(zTags); |
|
120
|
if( zComment ){ |
|
121
|
fossil_print("comment: "); |
|
122
|
comment_print(zComment, 0, 14, -1, get_comment_format()); |
|
123
|
free(zComment); |
|
124
|
} |
|
125
|
} |
|
126
|
|
|
127
|
/* |
|
128
|
** Print information about the URLs used to access a repository and |
|
129
|
** checkouts in a repository. |
|
130
|
*/ |
|
131
|
static void extraRepoInfo(void){ |
|
132
|
Stmt s; |
|
133
|
db_prepare(&s, "SELECT substr(name,7), date(mtime,'unixepoch')" |
|
134
|
" FROM config" |
|
135
|
" WHERE name GLOB 'ckout:*' ORDER BY mtime DESC"); |
|
136
|
while( db_step(&s)==SQLITE_ROW ){ |
|
137
|
const char *zName; |
|
138
|
const char *zCkout = db_column_text(&s, 0); |
|
139
|
if( !vfile_top_of_checkout(zCkout) ) continue; |
|
140
|
if( g.localOpen ){ |
|
141
|
if( fossil_strcmp(zCkout, g.zLocalRoot)==0 ) continue; |
|
142
|
zName = "alt-root:"; |
|
143
|
}else{ |
|
144
|
zName = "check-out:"; |
|
145
|
} |
|
146
|
fossil_print("%-11s %-54s %s\n", zName, zCkout, |
|
147
|
db_column_text(&s, 1)); |
|
148
|
} |
|
149
|
db_finalize(&s); |
|
150
|
db_prepare(&s, "SELECT substr(name,9), date(mtime,'unixepoch')" |
|
151
|
" FROM config" |
|
152
|
" WHERE name GLOB 'baseurl:*' ORDER BY mtime DESC"); |
|
153
|
while( db_step(&s)==SQLITE_ROW ){ |
|
154
|
fossil_print("access-url: %-54s %s\n", db_column_text(&s, 0), |
|
155
|
db_column_text(&s, 1)); |
|
156
|
} |
|
157
|
db_finalize(&s); |
|
158
|
} |
|
159
|
|
|
160
|
/* |
|
161
|
** Show the parent project, if any |
|
162
|
*/ |
|
163
|
static void showParentProject(void){ |
|
164
|
const char *zParentCode; |
|
165
|
zParentCode = db_get("parent-project-code",0); |
|
166
|
if( zParentCode ){ |
|
167
|
fossil_print("derived-from: %s %s\n", zParentCode, |
|
168
|
db_get("parent-project-name","")); |
|
169
|
} |
|
170
|
} |
|
171
|
|
|
172
|
/* |
|
173
|
** COMMAND: info |
|
174
|
** |
|
175
|
** Usage: %fossil info ?VERSION | REPOSITORY_FILENAME? ?OPTIONS? |
|
176
|
** |
|
177
|
** With no arguments, provide information about the current tree. |
|
178
|
** If an argument is specified, provide information about the object |
|
179
|
** in the repository of the current tree that the argument refers |
|
180
|
** to. Or if the argument is the name of a repository, show |
|
181
|
** information about that repository. |
|
182
|
** |
|
183
|
** If the argument is a repository name, then the --verbose option shows |
|
184
|
** all known check-out locations for that repository and all URLs used |
|
185
|
** to access the repository. The --verbose is (currently) a no-op if |
|
186
|
** the argument is the name of an object within the repository. |
|
187
|
** |
|
188
|
** Use the "finfo" command to get information about a specific |
|
189
|
** file in a check-out. |
|
190
|
** |
|
191
|
** Options: |
|
192
|
** -R|--repository REPO Extract info from repository REPO |
|
193
|
** -v|--verbose Show extra information about repositories |
|
194
|
** |
|
195
|
** See also: [[annotate]], [[artifact]], [[finfo]], [[timeline]] |
|
196
|
*/ |
|
197
|
void info_cmd(void){ |
|
198
|
i64 fsize; |
|
199
|
int verboseFlag = find_option("verbose","v",0)!=0; |
|
200
|
if( !verboseFlag ){ |
|
201
|
verboseFlag = find_option("detail","l",0)!=0; /* deprecated */ |
|
202
|
} |
|
203
|
|
|
204
|
if( g.argc==3 |
|
205
|
&& file_isfile(g.argv[2], ExtFILE) |
|
206
|
&& (fsize = file_size(g.argv[2], ExtFILE))>0 |
|
207
|
&& (fsize&0x1ff)==0 |
|
208
|
){ |
|
209
|
db_open_config(0, 0); |
|
210
|
db_open_repository(g.argv[2]); |
|
211
|
db_record_repository_filename(g.argv[2]); |
|
212
|
fossil_print("project-name: %s\n", db_get("project-name", "<unnamed>")); |
|
213
|
fossil_print("project-code: %s\n", db_get("project-code", "<none>")); |
|
214
|
showParentProject(); |
|
215
|
extraRepoInfo(); |
|
216
|
return; |
|
217
|
} |
|
218
|
db_find_and_open_repository(OPEN_OK_NOT_FOUND,0); |
|
219
|
verify_all_options(); |
|
220
|
if( g.argc==2 ){ |
|
221
|
int vid; |
|
222
|
if( g.repositoryOpen ){ |
|
223
|
db_record_repository_filename(0); |
|
224
|
fossil_print("project-name: %s\n", db_get("project-name", "<unnamed>")); |
|
225
|
}else{ |
|
226
|
db_open_config(0,1); |
|
227
|
} |
|
228
|
if( g.localOpen ){ |
|
229
|
fossil_print("repository: %s\n", db_repository_filename()); |
|
230
|
fossil_print("local-root: %s\n", g.zLocalRoot); |
|
231
|
} |
|
232
|
if( verboseFlag && g.repositoryOpen ){ |
|
233
|
extraRepoInfo(); |
|
234
|
} |
|
235
|
if( g.zConfigDbName ){ |
|
236
|
fossil_print("config-db: %s\n", g.zConfigDbName); |
|
237
|
} |
|
238
|
if( g.repositoryOpen ){ |
|
239
|
fossil_print("project-code: %s\n", db_get("project-code", "")); |
|
240
|
showParentProject(); |
|
241
|
vid = g.localOpen ? db_lget_int("checkout", 0) : 0; |
|
242
|
if( vid ){ |
|
243
|
show_common_info(vid, "checkout:", 1, 1); |
|
244
|
} |
|
245
|
fossil_print("check-ins: %d\n", |
|
246
|
db_int(-1, "SELECT count(*) FROM event WHERE type='ci' /*scan*/")); |
|
247
|
} |
|
248
|
if( verboseFlag || !g.repositoryOpen ){ |
|
249
|
Blob vx; |
|
250
|
char *z; |
|
251
|
fossil_version_blob(&vx, 0); |
|
252
|
z = strstr(blob_str(&vx), "version"); |
|
253
|
if( z ){ |
|
254
|
z += 8; |
|
255
|
}else{ |
|
256
|
z = blob_str(&vx); |
|
257
|
} |
|
258
|
fossil_print("fossil: %z\n", file_fullexename(g.nameOfExe)); |
|
259
|
fossil_print("version: %s", z); |
|
260
|
blob_reset(&vx); |
|
261
|
} |
|
262
|
}else if( g.repositoryOpen ){ |
|
263
|
int rid; |
|
264
|
rid = name_to_rid(g.argv[2]); |
|
265
|
if( rid==0 ){ |
|
266
|
fossil_fatal("no such object: %s", g.argv[2]); |
|
267
|
} |
|
268
|
show_common_info(rid, "hash:", 1, 1); |
|
269
|
}else{ |
|
270
|
fossil_fatal("Could not find or open a Fossil repository"); |
|
271
|
} |
|
272
|
} |
|
273
|
|
|
274
|
/* |
|
275
|
** Show the context graph (immediate parents and children) for |
|
276
|
** check-in rid and rid2 |
|
277
|
*/ |
|
278
|
void render_checkin_context(int rid, int rid2, int parentsOnly, int mFlags){ |
|
279
|
Blob sql; |
|
280
|
Stmt q; |
|
281
|
int rx[2]; |
|
282
|
int i, n; |
|
283
|
rx[0] = rid; |
|
284
|
rx[1] = rid2; |
|
285
|
n = rid2 ? 2 : 1; |
|
286
|
blob_zero(&sql); |
|
287
|
blob_append(&sql, timeline_query_for_www(), -1); |
|
288
|
|
|
289
|
db_multi_exec( |
|
290
|
"CREATE TEMP TABLE IF NOT EXISTS ok(rid INTEGER PRIMARY KEY);" |
|
291
|
"DELETE FROM ok;" |
|
292
|
); |
|
293
|
for(i=0; i<n; i++){ |
|
294
|
db_multi_exec( |
|
295
|
"INSERT OR IGNORE INTO ok VALUES(%d);" |
|
296
|
"INSERT OR IGNORE INTO ok SELECT pid FROM plink WHERE cid=%d;", |
|
297
|
rx[i], rx[i] |
|
298
|
); |
|
299
|
} |
|
300
|
if( !parentsOnly ){ |
|
301
|
for(i=0; i<n; i++){ |
|
302
|
db_multi_exec( |
|
303
|
"INSERT OR IGNORE INTO ok SELECT cid FROM plink WHERE pid=%d;", rx[i] |
|
304
|
); |
|
305
|
if( db_table_exists("repository","cherrypick") ){ |
|
306
|
db_multi_exec( |
|
307
|
"INSERT OR IGNORE INTO ok " |
|
308
|
" SELECT parentid FROM cherrypick WHERE childid=%d;" |
|
309
|
"INSERT OR IGNORE INTO ok " |
|
310
|
" SELECT childid FROM cherrypick WHERE parentid=%d;", |
|
311
|
rx[i], rx[i] |
|
312
|
); |
|
313
|
} |
|
314
|
} |
|
315
|
} |
|
316
|
blob_append_sql(&sql, " AND event.objid IN ok ORDER BY mtime DESC"); |
|
317
|
db_prepare(&q, "%s", blob_sql_text(&sql)); |
|
318
|
www_print_timeline(&q, |
|
319
|
mFlags |
|
320
|
|TIMELINE_GRAPH |
|
321
|
|TIMELINE_FILLGAPS |
|
322
|
|TIMELINE_NOSCROLL |
|
323
|
|TIMELINE_XMERGE |
|
324
|
|TIMELINE_CHPICK, |
|
325
|
0, 0, 0, rid, rid2, 0); |
|
326
|
db_finalize(&q); |
|
327
|
blob_reset(&sql); |
|
328
|
} |
|
329
|
|
|
330
|
|
|
331
|
/* |
|
332
|
** Append the difference between artifacts to the output |
|
333
|
*/ |
|
334
|
static void append_diff( |
|
335
|
const char *zFrom, /* Diff from this artifact */ |
|
336
|
const char *zTo, /* ... to this artifact */ |
|
337
|
DiffConfig *pCfg /* The diff configuration */ |
|
338
|
){ |
|
339
|
int fromid; |
|
340
|
int toid; |
|
341
|
Blob from, to; |
|
342
|
if( zFrom ){ |
|
343
|
fromid = uuid_to_rid(zFrom, 0); |
|
344
|
content_get(fromid, &from); |
|
345
|
pCfg->zLeftHash = zFrom; |
|
346
|
}else{ |
|
347
|
blob_zero(&from); |
|
348
|
pCfg->zLeftHash = 0; |
|
349
|
} |
|
350
|
if( zTo ){ |
|
351
|
toid = uuid_to_rid(zTo, 0); |
|
352
|
content_get(toid, &to); |
|
353
|
}else{ |
|
354
|
blob_zero(&to); |
|
355
|
} |
|
356
|
if( pCfg->diffFlags & DIFF_SIDEBYSIDE ){ |
|
357
|
pCfg->diffFlags |= DIFF_HTML | DIFF_NOTTOOBIG; |
|
358
|
}else{ |
|
359
|
pCfg->diffFlags |= DIFF_LINENO | DIFF_HTML | DIFF_NOTTOOBIG; |
|
360
|
} |
|
361
|
text_diff(&from, &to, cgi_output_blob(), pCfg); |
|
362
|
pCfg->zLeftHash = 0; |
|
363
|
blob_reset(&from); |
|
364
|
blob_reset(&to); |
|
365
|
} |
|
366
|
|
|
367
|
/* |
|
368
|
** Write a line of web-page output that shows changes that have occurred |
|
369
|
** to a file between two check-ins. |
|
370
|
*/ |
|
371
|
static void append_file_change_line( |
|
372
|
const char *zCkin, /* The check-in on which the change occurs */ |
|
373
|
const char *zName, /* Name of the file that has changed */ |
|
374
|
const char *zOld, /* blob.uuid before change. NULL for added files */ |
|
375
|
const char *zNew, /* blob.uuid after change. NULL for deletes */ |
|
376
|
const char *zOldName, /* Prior name. NULL if no name change. */ |
|
377
|
DiffConfig *pCfg, /* Flags for text_diff() or NULL to omit all */ |
|
378
|
int mperm /* executable or symlink permission for zNew */ |
|
379
|
){ |
|
380
|
@ <div class='file-change-line'><span> |
|
381
|
/* Maintenance reminder: the extra level of SPAN is for |
|
382
|
** arranging new elements via JS. */ |
|
383
|
if( !g.perm.Hyperlink ){ |
|
384
|
if( zNew==0 ){ |
|
385
|
@ Deleted %h(zName). |
|
386
|
}else if( zOld==0 ){ |
|
387
|
@ Added %h(zName). |
|
388
|
}else if( zOldName!=0 && fossil_strcmp(zName,zOldName)!=0 ){ |
|
389
|
@ Name change from %h(zOldName) to %h(zName). |
|
390
|
}else if( fossil_strcmp(zNew, zOld)==0 ){ |
|
391
|
if( mperm==PERM_EXE ){ |
|
392
|
@ %h(zName) became executable. |
|
393
|
}else if( mperm==PERM_LNK ){ |
|
394
|
@ %h(zName) became a symlink. |
|
395
|
}else{ |
|
396
|
@ %h(zName) became a regular file. |
|
397
|
} |
|
398
|
}else{ |
|
399
|
@ Changes to %h(zName). |
|
400
|
} |
|
401
|
@ </span></div> |
|
402
|
if( pCfg ){ |
|
403
|
append_diff(zOld, zNew, pCfg); |
|
404
|
} |
|
405
|
}else{ |
|
406
|
const char *zCkin2 = |
|
407
|
mprintf(validate16(zCkin, -1) ? "%!S" : "%T"/*works-like:"%s"*/, zCkin); |
|
408
|
if( zOld && zNew ){ |
|
409
|
if( fossil_strcmp(zOld, zNew)!=0 ){ |
|
410
|
if( zOldName!=0 && fossil_strcmp(zName,zOldName)!=0 ){ |
|
411
|
@ Renamed and modified |
|
412
|
@ %z(href("%R/finfo?name=%T&m=%!S&ci=%s",zOldName,zOld,zCkin2))\ |
|
413
|
@ %h(zOldName)</a> |
|
414
|
@ %z(href("%R/artifact/%!S",zOld))[%S(zOld)]</a> |
|
415
|
@ to %z(href("%R/finfo?name=%T&m=%!S&ci=%s",zName,zNew,zCkin2))\ |
|
416
|
@ %h(zName)</a> |
|
417
|
@ %z(href("%R/artifact/%!S",zNew))[%S(zNew)]</a>. |
|
418
|
}else{ |
|
419
|
@ Modified %z(href("%R/finfo?name=%T&m=%!S&ci=%s",zName,zNew,zCkin2))\ |
|
420
|
@ %h(zName)</a> |
|
421
|
@ from %z(href("%R/artifact/%!S",zOld))[%S(zOld)]</a> |
|
422
|
@ to %z(href("%R/artifact/%!S",zNew))[%S(zNew)]</a>. |
|
423
|
} |
|
424
|
}else if( zOldName!=0 && fossil_strcmp(zName,zOldName)!=0 ){ |
|
425
|
@ Name change |
|
426
|
@ from %z(href("%R/finfo?name=%T&m=%!S&ci=%s",zOldName,zOld,zCkin2))\ |
|
427
|
@ %h(zOldName)</a> |
|
428
|
@ to %z(href("%R/finfo?name=%T&m=%!S&ci=%s",zName,zNew,zCkin2))\ |
|
429
|
@ %h(zName)</a>. |
|
430
|
}else{ |
|
431
|
@ %z(href("%R/finfo?name=%T&m=%!S&ci=%s",zName,zNew,zCkin2))\ |
|
432
|
@ %h(zName)</a> became |
|
433
|
if( mperm==PERM_EXE ){ |
|
434
|
@ executable with contents |
|
435
|
}else if( mperm==PERM_LNK ){ |
|
436
|
@ a symlink with target |
|
437
|
}else{ |
|
438
|
@ a regular file with contents |
|
439
|
} |
|
440
|
@ %z(href("%R/artifact/%!S",zNew))[%S(zNew)]</a>. |
|
441
|
} |
|
442
|
}else if( zOld ){ |
|
443
|
@ Deleted %z(href("%R/finfo?name=%T&m=%!S&ci=%s",zName,zOld,zCkin2))\ |
|
444
|
@ %h(zName)</a> version %z(href("%R/artifact/%!S",zOld))[%S(zOld)]</a>. |
|
445
|
}else{ |
|
446
|
@ Added %z(href("%R/finfo?name=%T&m=%!S&ci=%s",zName,zNew,zCkin2))\ |
|
447
|
@ %h(zName)</a> version %z(href("%R/artifact/%!S",zNew))[%S(zNew)]</a>. |
|
448
|
} |
|
449
|
if( zOld && zNew && fossil_strcmp(zOld,zNew)!=0 ){ |
|
450
|
if( pCfg ){ |
|
451
|
@ </span></div> |
|
452
|
append_diff(zOld, zNew, pCfg); |
|
453
|
}else{ |
|
454
|
@ %z(href("%R/fdiff?v1=%!S&v2=%!S",zOld,zNew))[diff]</a> |
|
455
|
@ </span></div> |
|
456
|
} |
|
457
|
}else{ |
|
458
|
@ </span></div> |
|
459
|
} |
|
460
|
} |
|
461
|
} |
|
462
|
|
|
463
|
/* |
|
464
|
** Generate javascript to enhance HTML diffs. |
|
465
|
*/ |
|
466
|
void append_diff_javascript(int diffType){ |
|
467
|
if( diffType==0 ) return; |
|
468
|
builtin_fossil_js_bundle_or("diff", NULL); |
|
469
|
} |
|
470
|
|
|
471
|
/* |
|
472
|
** Construct an appropriate diffFlag for text_diff() based on query |
|
473
|
** parameters and the to boolean arguments. |
|
474
|
*/ |
|
475
|
DiffConfig *construct_diff_flags(int diffType, DiffConfig *pCfg){ |
|
476
|
u64 diffFlags = 0; /* Zero means do not show any diff */ |
|
477
|
if( diffType>0 ){ |
|
478
|
int x; |
|
479
|
if( diffType==2 ) diffFlags = DIFF_SIDEBYSIDE; |
|
480
|
if( P_NoBot("w") ) diffFlags |= DIFF_IGNORE_ALLWS; |
|
481
|
if( PD_NoBot("noopt",0)!=0 ) diffFlags |= DIFF_NOOPT; |
|
482
|
diffFlags |= DIFF_STRIP_EOLCR; |
|
483
|
diff_config_init(pCfg, diffFlags); |
|
484
|
|
|
485
|
/* "dc" query parameter determines lines of context */ |
|
486
|
x = atoi(PD("dc","7")); |
|
487
|
if( x>0 ) pCfg->nContext = x; |
|
488
|
|
|
489
|
/* The "noopt" parameter disables diff optimization */ |
|
490
|
return pCfg; |
|
491
|
}else{ |
|
492
|
diff_config_init(pCfg, 0); |
|
493
|
return 0; |
|
494
|
} |
|
495
|
} |
|
496
|
|
|
497
|
/* |
|
498
|
** WEBPAGE: ci_tags |
|
499
|
** URL: /ci_tags?name=ARTIFACTID |
|
500
|
** |
|
501
|
** Show all tags and properties for a given check-in. |
|
502
|
** |
|
503
|
** This information used to be part of the main /ci page, but it is of |
|
504
|
** marginal usefulness. Better to factor it out into a sub-screen. |
|
505
|
*/ |
|
506
|
void ci_tags_page(void){ |
|
507
|
const char *zHash; |
|
508
|
int rid; |
|
509
|
Stmt q; |
|
510
|
int cnt = 0; |
|
511
|
Blob sql; |
|
512
|
char const *zType; |
|
513
|
|
|
514
|
login_check_credentials(); |
|
515
|
if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
|
516
|
rid = name_to_rid_www("name"); |
|
517
|
if( rid==0 ){ |
|
518
|
style_header("Check-in Information Error"); |
|
519
|
@ No such object: %h(PD("name","")) |
|
520
|
style_finish_page(); |
|
521
|
return; |
|
522
|
} |
|
523
|
cgi_check_for_malice(); |
|
524
|
zHash = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); |
|
525
|
style_header("Tags and Properties"); |
|
526
|
zType = whatis_rid_type_label(rid); |
|
527
|
if(!zType) zType = "Artifact"; |
|
528
|
@ <h1>Tags and Properties for %s(zType) \ |
|
529
|
@ %z(href("%R/ci/%!S",zHash))%S(zHash)</a></h1> |
|
530
|
db_prepare(&q, |
|
531
|
"SELECT tag.tagid, tagname, " |
|
532
|
" (SELECT uuid FROM blob WHERE rid=tagxref.srcid AND rid!=%d)," |
|
533
|
" value, datetime(tagxref.mtime,toLocal()), tagtype," |
|
534
|
" (SELECT uuid FROM blob WHERE rid=tagxref.origid AND rid!=%d)" |
|
535
|
" FROM tagxref JOIN tag ON tagxref.tagid=tag.tagid" |
|
536
|
" WHERE tagxref.rid=%d" |
|
537
|
" ORDER BY tagname /*sort*/", rid, rid, rid |
|
538
|
); |
|
539
|
while( db_step(&q)==SQLITE_ROW ){ |
|
540
|
const char *zTagname = db_column_text(&q, 1); |
|
541
|
const char *zSrcUuid = db_column_text(&q, 2); |
|
542
|
const char *zValue = db_column_text(&q, 3); |
|
543
|
const char *zDate = db_column_text(&q, 4); |
|
544
|
int tagtype = db_column_int(&q, 5); |
|
545
|
const char *zOrigUuid = db_column_text(&q, 6); |
|
546
|
cnt++; |
|
547
|
if( cnt==1 ){ |
|
548
|
@ <ul> |
|
549
|
} |
|
550
|
@ <li> |
|
551
|
if( tagtype==0 ){ |
|
552
|
@ <span class="infoTagCancelled">%h(zTagname)</span> cancelled |
|
553
|
}else if( zValue ){ |
|
554
|
@ <span class="infoTag">%h(zTagname)=%h(zValue)</span> |
|
555
|
}else { |
|
556
|
@ <span class="infoTag">%h(zTagname)</span> |
|
557
|
} |
|
558
|
if( tagtype==2 ){ |
|
559
|
if( zOrigUuid && zOrigUuid[0] ){ |
|
560
|
@ inherited from |
|
561
|
hyperlink_to_version(zOrigUuid); |
|
562
|
}else{ |
|
563
|
@ propagates to descendants |
|
564
|
} |
|
565
|
} |
|
566
|
if( zSrcUuid && zSrcUuid[0] ){ |
|
567
|
if( tagtype==0 ){ |
|
568
|
@ by |
|
569
|
}else{ |
|
570
|
@ added by |
|
571
|
} |
|
572
|
hyperlink_to_version(zSrcUuid); |
|
573
|
@ on |
|
574
|
hyperlink_to_date(zDate,0); |
|
575
|
} |
|
576
|
@ </li> |
|
577
|
} |
|
578
|
db_finalize(&q); |
|
579
|
if( cnt ){ |
|
580
|
@ </ul> |
|
581
|
} |
|
582
|
@ <div class="section">Context</div> |
|
583
|
db_multi_exec( |
|
584
|
"CREATE TEMP TABLE IF NOT EXISTS ok(rid INTEGER PRIMARY KEY);" |
|
585
|
"DELETE FROM ok;" |
|
586
|
"INSERT INTO ok VALUES(%d);" |
|
587
|
"INSERT OR IGNORE INTO ok " |
|
588
|
" SELECT tagxref.srcid" |
|
589
|
" FROM tagxref JOIN tag ON tagxref.tagid=tag.tagid" |
|
590
|
" WHERE tagxref.rid=%d;" |
|
591
|
"INSERT OR IGNORE INTO ok " |
|
592
|
" SELECT tagxref.origid" |
|
593
|
" FROM tagxref JOIN tag ON tagxref.tagid=tag.tagid" |
|
594
|
" WHERE tagxref.rid=%d;", |
|
595
|
rid, rid, rid |
|
596
|
); |
|
597
|
#if 0 |
|
598
|
db_multi_exec( |
|
599
|
"SELECT tag.tagid, tagname, " |
|
600
|
" (SELECT uuid FROM blob WHERE rid=tagxref.srcid AND rid!=%d)," |
|
601
|
" value, datetime(tagxref.mtime,toLocal()), tagtype," |
|
602
|
" (SELECT uuid FROM blob WHERE rid=tagxref.origid AND rid!=%d)" |
|
603
|
" FROM tagxref JOIN tag ON tagxref.tagid=tag.tagid" |
|
604
|
" WHERE tagxref.rid=%d" |
|
605
|
" ORDER BY tagname /*sort*/", rid, rid, rid |
|
606
|
); |
|
607
|
#endif |
|
608
|
blob_zero(&sql); |
|
609
|
blob_append(&sql, timeline_query_for_www(), -1); |
|
610
|
blob_append_sql(&sql, " AND event.objid IN ok ORDER BY mtime DESC"); |
|
611
|
db_prepare(&q, "%s", blob_sql_text(&sql)); |
|
612
|
www_print_timeline(&q, TIMELINE_DISJOINT|TIMELINE_GRAPH|TIMELINE_NOSCROLL, |
|
613
|
0, 0, 0, rid, 0, 0); |
|
614
|
db_finalize(&q); |
|
615
|
style_finish_page(); |
|
616
|
} |
|
617
|
|
|
618
|
/* |
|
619
|
** Render a web-page diff of the changes in the working check-out |
|
620
|
*/ |
|
621
|
static void ckout_normal_diff(int vid){ |
|
622
|
int diffType; /* 0: no diff, 1: unified, 2: side-by-side */ |
|
623
|
DiffConfig DCfg,*pCfg; /* Diff details */ |
|
624
|
const char *zW; /* The "w" query parameter */ |
|
625
|
int nChng; /* Number of changes */ |
|
626
|
Stmt q; |
|
627
|
|
|
628
|
diffType = preferred_diff_type(); |
|
629
|
pCfg = construct_diff_flags(diffType, &DCfg); |
|
630
|
nChng = db_int(0, "SELECT count(*) FROM vfile" |
|
631
|
" WHERE vid=%d AND (deleted OR chnged OR rid==0)", vid); |
|
632
|
if( nChng==0 ){ |
|
633
|
@ <p>No uncommitted changes</p> |
|
634
|
return; |
|
635
|
} |
|
636
|
db_prepare(&q, |
|
637
|
/* 0 1 2 3 4 5 6 */ |
|
638
|
"SELECT pathname, deleted, chnged , rid==0, rid, islink, uuid" |
|
639
|
" FROM vfile LEFT JOIN blob USING(rid)" |
|
640
|
" WHERE vid=%d" |
|
641
|
" AND (deleted OR chnged OR rid==0)" |
|
642
|
" ORDER BY pathname /*scan*/", |
|
643
|
vid |
|
644
|
); |
|
645
|
if( DCfg.diffFlags & DIFF_SIDEBYSIDE ){ |
|
646
|
DCfg.diffFlags |= DIFF_HTML | DIFF_NOTTOOBIG; |
|
647
|
}else{ |
|
648
|
DCfg.diffFlags |= DIFF_LINENO | DIFF_HTML | DIFF_NOTTOOBIG; |
|
649
|
} |
|
650
|
@ <div class="section" id="changes_section">Changes</div> |
|
651
|
DCfg.diffFlags |= DIFF_NUMSTAT; /* Show stats in the 'Changes' section */ |
|
652
|
@ <div class="sectionmenu info-changes-menu"> |
|
653
|
zW = (DCfg.diffFlags&DIFF_IGNORE_ALLWS)?"&w":""; |
|
654
|
if( diffType!=1 ){ |
|
655
|
@ %z(chref("button","%R?diff=1%s",zW))Unified Diff</a> |
|
656
|
} |
|
657
|
if( diffType!=2 ){ |
|
658
|
@ %z(chref("button","%R?diff=2%s",zW))Side-by-Side Diff</a> |
|
659
|
} |
|
660
|
if( diffType!=0 ){ |
|
661
|
if( *zW ){ |
|
662
|
@ %z(chref("button","%R?diff=%d",diffType))\ |
|
663
|
@ Show Whitespace Changes</a> |
|
664
|
}else{ |
|
665
|
@ %z(chref("button","%R?diff=%d&w",diffType))Ignore Whitespace</a> |
|
666
|
} |
|
667
|
} |
|
668
|
@ </div> |
|
669
|
while( db_step(&q)==SQLITE_ROW ){ |
|
670
|
const char *zTreename = db_column_text(&q,0); |
|
671
|
int isDeleted = db_column_int(&q, 1); |
|
672
|
int isChnged = db_column_int(&q,2); |
|
673
|
int isNew = db_column_int(&q,3); |
|
674
|
int srcid = db_column_int(&q, 4); |
|
675
|
int isLink = db_column_int(&q, 5); |
|
676
|
const char *zUuid = db_column_text(&q, 6); |
|
677
|
int showDiff = 1; |
|
678
|
|
|
679
|
DCfg.diffFlags &= (~DIFF_FILE_MASK); |
|
680
|
@ <div class='file-change-line'><span> |
|
681
|
if( isDeleted ){ |
|
682
|
@ DELETED %h(zTreename) |
|
683
|
DCfg.diffFlags |= DIFF_FILE_DELETED; |
|
684
|
showDiff = 0; |
|
685
|
}else if( file_access(zTreename, F_OK) ){ |
|
686
|
@ MISSING %h(zTreename) |
|
687
|
showDiff = 0; |
|
688
|
}else if( isNew ){ |
|
689
|
@ ADDED %h(zTreename) |
|
690
|
DCfg.diffFlags |= DIFF_FILE_ADDED; |
|
691
|
srcid = 0; |
|
692
|
showDiff = 0; |
|
693
|
}else if( isChnged==3 ){ |
|
694
|
@ ADDED_BY_MERGE %h(zTreename) |
|
695
|
DCfg.diffFlags |= DIFF_FILE_ADDED; |
|
696
|
srcid = 0; |
|
697
|
showDiff = 0; |
|
698
|
}else if( isChnged==5 ){ |
|
699
|
@ ADDED_BY_INTEGRATE %h(zTreename) |
|
700
|
DCfg.diffFlags |= DIFF_FILE_ADDED; |
|
701
|
srcid = 0; |
|
702
|
showDiff = 0; |
|
703
|
}else{ |
|
704
|
@ CHANGED %h(zTreename) |
|
705
|
} |
|
706
|
@ </span></div> |
|
707
|
if( showDiff && pCfg ){ |
|
708
|
Blob old, new; |
|
709
|
if( !isLink != !file_islink(zTreename) ){ |
|
710
|
@ %s(DIFF_CANNOT_COMPUTE_SYMLINK) |
|
711
|
continue; |
|
712
|
} |
|
713
|
if( srcid>0 ){ |
|
714
|
content_get(srcid, &old); |
|
715
|
pCfg->zLeftHash = zUuid; |
|
716
|
}else{ |
|
717
|
blob_zero(&old); |
|
718
|
pCfg->zLeftHash = 0; |
|
719
|
} |
|
720
|
blob_read_from_file(&new, zTreename, ExtFILE); |
|
721
|
text_diff(&old, &new, cgi_output_blob(), pCfg); |
|
722
|
blob_reset(&old); |
|
723
|
blob_reset(&new); |
|
724
|
} |
|
725
|
} |
|
726
|
db_finalize(&q); |
|
727
|
@ <script nonce='%h(style_nonce())'>;/* info.c:%d(__LINE__) */ |
|
728
|
@ document.getElementById('changes_section').textContent = 'Changes ' + |
|
729
|
@ '(%d(g.diffCnt[0]) file' + (%d(g.diffCnt[0])===1 ? '' : 's') + ': ' + |
|
730
|
@ '+%d(g.diffCnt[1]) ' + |
|
731
|
@ '−%d(g.diffCnt[2]))' |
|
732
|
@ </script> |
|
733
|
append_diff_javascript(diffType); |
|
734
|
} |
|
735
|
|
|
736
|
/* |
|
737
|
** Render a web-page diff of the changes in the working check-out to |
|
738
|
** an external reference. |
|
739
|
*/ |
|
740
|
static void ckout_external_base_diff(int vid, const char *zExBase){ |
|
741
|
int diffType; /* 0: no diff, 1: unified, 2: side-by-side */ |
|
742
|
DiffConfig DCfg,*pCfg; /* Diff details */ |
|
743
|
const char *zW; /* The "w" query parameter */ |
|
744
|
Stmt q; |
|
745
|
|
|
746
|
diffType = preferred_diff_type(); |
|
747
|
pCfg = construct_diff_flags(diffType, &DCfg); |
|
748
|
db_prepare(&q, |
|
749
|
"SELECT pathname FROM vfile WHERE vid=%d ORDER BY pathname", vid |
|
750
|
); |
|
751
|
if( DCfg.diffFlags & DIFF_SIDEBYSIDE ){ |
|
752
|
DCfg.diffFlags |= DIFF_HTML | DIFF_NOTTOOBIG; |
|
753
|
}else{ |
|
754
|
DCfg.diffFlags |= DIFF_LINENO | DIFF_HTML | DIFF_NOTTOOBIG; |
|
755
|
} |
|
756
|
@ <div class="section" id="changes_section">Changes</div> |
|
757
|
DCfg.diffFlags |= DIFF_NUMSTAT; /* Show stats in the 'Changes' section */ |
|
758
|
@ <div class="sectionmenu info-changes-menu"> |
|
759
|
zW = (DCfg.diffFlags&DIFF_IGNORE_ALLWS)?"&w":""; |
|
760
|
if( diffType!=1 ){ |
|
761
|
@ %z(chref("button","%R?diff=1&exbase=%h%s",zExBase,zW))\ |
|
762
|
@ Unified Diff</a> |
|
763
|
} |
|
764
|
if( diffType!=2 ){ |
|
765
|
@ %z(chref("button","%R?diff=2&exbase=%h%s",zExBase,zW))\ |
|
766
|
@ Side-by-Side Diff</a> |
|
767
|
} |
|
768
|
if( diffType!=0 ){ |
|
769
|
if( *zW ){ |
|
770
|
@ %z(chref("button","%R?diff=%d&exbase=%h",diffType,zExBase))\ |
|
771
|
@ Show Whitespace Changes</a> |
|
772
|
}else{ |
|
773
|
@ %z(chref("button","%R?diff=%d&exbase=%h&w",diffType,zExBase))\ |
|
774
|
@ Ignore Whitespace</a> |
|
775
|
} |
|
776
|
} |
|
777
|
@ </div> |
|
778
|
while( db_step(&q)==SQLITE_ROW ){ |
|
779
|
const char *zFile; /* Name of file in the repository */ |
|
780
|
char *zLhs; /* Full name of left-hand side file */ |
|
781
|
char *zRhs; /* Full name of right-hand side file */ |
|
782
|
Blob rhs; /* Full text of RHS */ |
|
783
|
Blob lhs; /* Full text of LHS */ |
|
784
|
|
|
785
|
zFile = db_column_text(&q,0); |
|
786
|
zLhs = mprintf("%s/%s", zExBase, zFile); |
|
787
|
zRhs = mprintf("%s%s", g.zLocalRoot, zFile); |
|
788
|
if( file_size(zLhs, ExtFILE)<0 ){ |
|
789
|
@ <div class='file-change-line'><span> |
|
790
|
@ Missing from external baseline: %h(zFile) |
|
791
|
@ </span></div> |
|
792
|
}else{ |
|
793
|
blob_read_from_file(&lhs, zLhs, ExtFILE); |
|
794
|
blob_read_from_file(&rhs, zRhs, ExtFILE); |
|
795
|
if( blob_size(&lhs)!=blob_size(&rhs) |
|
796
|
|| memcmp(blob_buffer(&lhs), blob_buffer(&rhs), blob_size(&lhs))!=0 |
|
797
|
){ |
|
798
|
@ <div class='file-change-line'><span> |
|
799
|
@ Changes to %h(zFile) |
|
800
|
@ </span></div> |
|
801
|
if( pCfg ){ |
|
802
|
char *zFullFN; |
|
803
|
char *zHexFN; |
|
804
|
zFullFN = file_canonical_name_dup(zLhs); |
|
805
|
zHexFN = mprintf("x%H", zFullFN); |
|
806
|
fossil_free(zFullFN); |
|
807
|
pCfg->zLeftHash = zHexFN; |
|
808
|
text_diff(&lhs, &rhs, cgi_output_blob(), pCfg); |
|
809
|
pCfg->zLeftHash = 0; |
|
810
|
fossil_free(zHexFN); |
|
811
|
} |
|
812
|
} |
|
813
|
blob_reset(&lhs); |
|
814
|
blob_reset(&rhs); |
|
815
|
} |
|
816
|
fossil_free(zLhs); |
|
817
|
fossil_free(zRhs); |
|
818
|
} |
|
819
|
db_finalize(&q); |
|
820
|
@ <script nonce='%h(style_nonce())'>;/* info.c:%d(__LINE__) */ |
|
821
|
@ document.getElementById('changes_section').textContent = 'Changes ' + |
|
822
|
@ '(%d(g.diffCnt[0]) file' + (%d(g.diffCnt[0])===1 ? '' : 's') + ': ' + |
|
823
|
@ '+%d(g.diffCnt[1]) ' + |
|
824
|
@ '−%d(g.diffCnt[2]))' |
|
825
|
@ </script> |
|
826
|
append_diff_javascript(diffType); |
|
827
|
} |
|
828
|
|
|
829
|
/* |
|
830
|
** WEBPAGE: ckout |
|
831
|
** |
|
832
|
** Show information about the current checkout. This page only functions |
|
833
|
** if the web server is run on a loopback interface (in other words, was |
|
834
|
** started using "fossil ui" or similar) from within an open check-out. |
|
835
|
** |
|
836
|
** If the "exbase=PATH" query parameter is provided, then the diff shown |
|
837
|
** uses the files in PATH as the baseline. This is the same as using |
|
838
|
** the "--from PATH" argument to the "fossil diff" command-line. In fact, |
|
839
|
** when using "fossil ui --from PATH", the --from argument becomes the value |
|
840
|
** of the exbase query parameter for the start page. Note that if PATH |
|
841
|
** is a pure hexadecimal string, it is decoded first before being used as |
|
842
|
** the pathname. Real pathnames should contain at least one directory |
|
843
|
** separator character. |
|
844
|
** |
|
845
|
** Other query parameters related to diffs are also accepted. |
|
846
|
*/ |
|
847
|
void ckout_page(void){ |
|
848
|
int vid; |
|
849
|
const char *zHome; /* Home directory */ |
|
850
|
int nHome; |
|
851
|
const char *zExBase; |
|
852
|
char *zHostname; |
|
853
|
char *zCwd; |
|
854
|
|
|
855
|
if( !cgi_is_loopback(g.zIpAddr) || !db_open_local(0) ){ |
|
856
|
cgi_redirectf("%R/home"); |
|
857
|
return; |
|
858
|
} |
|
859
|
file_chdir(g.zLocalRoot, 0); |
|
860
|
vid = db_lget_int("checkout", 0); |
|
861
|
db_unprotect(PROTECT_ALL); |
|
862
|
vfile_check_signature(vid, CKSIG_ENOTFILE); |
|
863
|
db_protect_pop(); |
|
864
|
style_set_current_feature("vinfo"); |
|
865
|
zHostname = fossil_hostname(); |
|
866
|
zCwd = file_getcwd(0,0); |
|
867
|
zHome = fossil_getenv("HOME"); |
|
868
|
if( zHome ){ |
|
869
|
nHome = (int)strlen(zHome); |
|
870
|
if( strncmp(zCwd, zHome, nHome)==0 && zCwd[nHome]=='/' ){ |
|
871
|
zCwd = mprintf("~%s", zCwd+nHome); |
|
872
|
} |
|
873
|
}else{ |
|
874
|
nHome = 0; |
|
875
|
} |
|
876
|
if( zHostname ){ |
|
877
|
style_header("Checkout Status: %h on %h", zCwd, zHostname); |
|
878
|
}else{ |
|
879
|
style_header("Checkout Status: %h", zCwd); |
|
880
|
} |
|
881
|
render_checkin_context(vid, 0, 0, 0); |
|
882
|
@ <hr> |
|
883
|
zExBase = P("exbase"); |
|
884
|
if( zExBase && zExBase[0] ){ |
|
885
|
char *zPath = decode16_dup(zExBase); |
|
886
|
char *zCBase = file_canonical_name_dup(zPath?zPath:zExBase); |
|
887
|
if( nHome && strncmp(zCBase, zHome, nHome)==0 && zCBase[nHome]=='/' ){ |
|
888
|
@ <p>Using external baseline: ~%h(zCBase+nHome)</p> |
|
889
|
}else{ |
|
890
|
@ <p>Using external baseline: %h(zCBase)</p> |
|
891
|
} |
|
892
|
ckout_external_base_diff(vid, zCBase); |
|
893
|
fossil_free(zCBase); |
|
894
|
fossil_free(zPath); |
|
895
|
}else{ |
|
896
|
ckout_normal_diff(vid); |
|
897
|
} |
|
898
|
style_finish_page(); |
|
899
|
} |
|
900
|
|
|
901
|
/* |
|
902
|
** WEBPAGE: vinfo |
|
903
|
** WEBPAGE: ci |
|
904
|
** URL: /ci/ARTIFACTID |
|
905
|
** OR: /ci?name=ARTIFACTID |
|
906
|
** |
|
907
|
** Display information about a particular check-in. The exact |
|
908
|
** same information is shown on the /info page if the name query |
|
909
|
** parameter to /info describes a check-in. |
|
910
|
** |
|
911
|
** The ARTIFACTID can be a unique prefix for the HASH of the check-in, |
|
912
|
** or a tag or branch name that identifies the check-in. |
|
913
|
*/ |
|
914
|
void ci_page(void){ |
|
915
|
Stmt q1, q2, q3; |
|
916
|
int rid; |
|
917
|
int isLeaf; |
|
918
|
int diffType; /* 0: no diff, 1: unified, 2: side-by-side */ |
|
919
|
const char *zName; /* Name of the check-in to be displayed */ |
|
920
|
const char *zUuid; /* Hash of zName, found via blob.uuid */ |
|
921
|
const char *zParent; /* Hash of the parent check-in (if any) */ |
|
922
|
const char *zRe; /* regex parameter */ |
|
923
|
ReCompiled *pRe = 0; /* regex */ |
|
924
|
const char *zW; /* URL param for ignoring whitespace */ |
|
925
|
const char *zPage = "vinfo"; /* Page that shows diffs */ |
|
926
|
const char *zBrName; /* Branch name */ |
|
927
|
DiffConfig DCfg,*pCfg; /* Type of diff */ |
|
928
|
|
|
929
|
login_check_credentials(); |
|
930
|
if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
|
931
|
style_set_current_feature("vinfo"); |
|
932
|
zName = P("name"); |
|
933
|
rid = name_to_rid_www("name"); |
|
934
|
if( rid==0 ){ |
|
935
|
style_header("Check-in Information Error"); |
|
936
|
@ No such object: %h(zName) |
|
937
|
style_finish_page(); |
|
938
|
return; |
|
939
|
} |
|
940
|
zRe = P("regex"); |
|
941
|
if( zRe ) fossil_re_compile(&pRe, zRe, 0); |
|
942
|
zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); |
|
943
|
zParent = db_text(0, |
|
944
|
"SELECT uuid FROM plink, blob" |
|
945
|
" WHERE plink.cid=%d AND blob.rid=plink.pid AND plink.isprim", |
|
946
|
rid |
|
947
|
); |
|
948
|
isLeaf = !db_exists("SELECT 1 FROM plink WHERE pid=%d", rid); |
|
949
|
db_prepare(&q1, |
|
950
|
"SELECT uuid, datetime(mtime,toLocal(),'subsec'), user, comment," |
|
951
|
" datetime(omtime,toLocal(),'subsec'), mtime" |
|
952
|
" FROM blob, event" |
|
953
|
" WHERE blob.rid=%d" |
|
954
|
" AND event.objid=%d", |
|
955
|
rid, rid |
|
956
|
); |
|
957
|
zBrName = branch_of_rid(rid); |
|
958
|
|
|
959
|
diffType = preferred_diff_type(); |
|
960
|
cgi_check_for_malice(); |
|
961
|
if( db_step(&q1)==SQLITE_ROW ){ |
|
962
|
const char *zUuid = db_column_text(&q1, 0); |
|
963
|
int nUuid = db_column_bytes(&q1, 0); |
|
964
|
char *zEUser, *zEComment; |
|
965
|
const char *zUser; |
|
966
|
const char *zOrigUser; |
|
967
|
const char *zComment; |
|
968
|
const char *zDate; |
|
969
|
const char *zOrigDate; |
|
970
|
int okWiki = 0; |
|
971
|
Blob wiki_read_links = BLOB_INITIALIZER; |
|
972
|
Blob wiki_add_links = BLOB_INITIALIZER; |
|
973
|
|
|
974
|
Th_StoreUnsafe("current_checkin", zName); |
|
975
|
style_header("Check-in [%S]", zUuid); |
|
976
|
login_anonymous_available(); |
|
977
|
zEUser = db_text(0, |
|
978
|
"SELECT value FROM tagxref" |
|
979
|
" WHERE tagid=%d AND rid=%d AND tagtype>0", |
|
980
|
TAG_USER, rid); |
|
981
|
zEComment = db_text(0, |
|
982
|
"SELECT value FROM tagxref WHERE tagid=%d AND rid=%d", |
|
983
|
TAG_COMMENT, rid); |
|
984
|
zOrigUser = db_column_text(&q1, 2); |
|
985
|
zUser = zEUser ? zEUser : zOrigUser; |
|
986
|
zComment = db_column_text(&q1, 3); |
|
987
|
zDate = db_column_text(&q1,1); |
|
988
|
zOrigDate = db_column_text(&q1, 4); |
|
989
|
if( zOrigDate==0 ) zOrigDate = zDate; |
|
990
|
@ <div class="section accordion">Overview</div> |
|
991
|
@ <div class="accordion_panel"> |
|
992
|
@ <table class="label-value"> |
|
993
|
@ <tr><th>Comment:</th><td class="infoComment">\ |
|
994
|
@ %!W(zEComment?zEComment:zComment)</td></tr> |
|
995
|
|
|
996
|
/* The Download: line */ |
|
997
|
if( g.perm.Zip ){ |
|
998
|
@ <tr><th>Downloads:</th><td> |
|
999
|
if( robot_would_be_restricted("download") ){ |
|
1000
|
@ See separate %z(href("%R/rchvdwnld/%!S",zUuid))download page</a> |
|
1001
|
}else{ |
|
1002
|
char *zBase = archive_base_name(rid); |
|
1003
|
@ %z(href("%R/tarball/%s.tar.gz",zBase))Tarball</a> |
|
1004
|
@ | %z(href("%R/zip/%s.zip",zBase))ZIP archive</a> |
|
1005
|
if( g.zLogin!=0 ){ |
|
1006
|
@ | %z(href("%R/sqlar/%s.sqlar",zBase))\ |
|
1007
|
@ SQL archive</a></td></tr> |
|
1008
|
} |
|
1009
|
fossil_free(zBase); |
|
1010
|
} |
|
1011
|
} |
|
1012
|
|
|
1013
|
@ <tr><th>Timelines:</th><td> |
|
1014
|
@ %z(href("%R/timeline?f=%!S&unhide",zUuid))family</a> |
|
1015
|
if( zParent ){ |
|
1016
|
@ | %z(href("%R/timeline?p=%!S&unhide",zUuid))ancestors</a> |
|
1017
|
} |
|
1018
|
if( !isLeaf ){ |
|
1019
|
@ | %z(href("%R/timeline?d=%!S&unhide",zUuid))descendants</a> |
|
1020
|
} |
|
1021
|
if( zParent && !isLeaf ){ |
|
1022
|
@ | %z(href("%R/timeline?dp=%!S&unhide",zUuid))both</a> |
|
1023
|
} |
|
1024
|
db_prepare(&q2,"SELECT substr(tag.tagname,5) FROM tagxref, tag " |
|
1025
|
" WHERE rid=%d AND tagtype>0 " |
|
1026
|
" AND tag.tagid=tagxref.tagid " |
|
1027
|
" AND +tag.tagname GLOB 'sym-*'", rid); |
|
1028
|
while( db_step(&q2)==SQLITE_ROW ){ |
|
1029
|
const char *zTagName = db_column_text(&q2, 0); |
|
1030
|
if( fossil_strcmp(zTagName,zBrName)==0 ){ |
|
1031
|
cgi_printf(" | "); |
|
1032
|
style_copy_button(1, "name-br", 0, 0, "%z%h</a>", |
|
1033
|
href("%R/timeline?r=%T&unhide",zTagName), zTagName); |
|
1034
|
cgi_printf("\n"); |
|
1035
|
if( wiki_tagid2("branch",zTagName)!=0 ){ |
|
1036
|
blob_appendf(&wiki_read_links, " | %z%h</a>", |
|
1037
|
href("%R/%s?name=branch/%h", |
|
1038
|
(g.perm.Write && g.perm.WrWiki) |
|
1039
|
? "wikiedit" : "wiki", |
|
1040
|
zTagName), zTagName); |
|
1041
|
}else if( g.perm.Write && g.perm.WrWiki ){ |
|
1042
|
blob_appendf(&wiki_add_links, " | %z%h</a>", |
|
1043
|
href("%R/wikiedit?name=branch/%h",zTagName), zTagName); |
|
1044
|
} |
|
1045
|
}else{ |
|
1046
|
@ | %z(href("%R/timeline?t=%T&unhide",zTagName))%h(zTagName)</a> |
|
1047
|
if( wiki_tagid2("tag",zTagName)!=0 ){ |
|
1048
|
blob_appendf(&wiki_read_links, " | %z%h</a>", |
|
1049
|
href("%R/wiki?name=tag/%h",zTagName), zTagName); |
|
1050
|
}else if( g.perm.Write && g.perm.WrWiki ){ |
|
1051
|
blob_appendf(&wiki_add_links, " | %z%h</a>", |
|
1052
|
href("%R/wikiedit?name=tag/%h",zTagName), zTagName); |
|
1053
|
} |
|
1054
|
} |
|
1055
|
} |
|
1056
|
db_finalize(&q2); |
|
1057
|
@ </td></tr> |
|
1058
|
|
|
1059
|
@ <tr><th>Files:</th> |
|
1060
|
@ <td> |
|
1061
|
@ %z(href("%R/tree?ci=%!S",zUuid))files</a> |
|
1062
|
@ | %z(href("%R/fileage?name=%!S",zUuid))file ages</a> |
|
1063
|
@ | %z(href("%R/tree?nofiles&type=tree&ci=%!S",zUuid))folders</a> |
|
1064
|
@ </td> |
|
1065
|
@ </tr> |
|
1066
|
|
|
1067
|
@ <tr><th>%s(hname_alg(nUuid)):</th><td> |
|
1068
|
style_copy_button(1, "hash-ci", 0, 2, "%.32s<wbr>%s", zUuid, zUuid+32); |
|
1069
|
if( g.perm.Setup ){ |
|
1070
|
@ (Record ID: %d(rid)) |
|
1071
|
} |
|
1072
|
@ </td></tr> |
|
1073
|
@ <tr><th>User & Date:</th><td> |
|
1074
|
hyperlink_to_user(zUser,zDate," on "); |
|
1075
|
hyperlink_to_date(zDate, "</td></tr>"); |
|
1076
|
if( zEComment ){ |
|
1077
|
@ <tr><th>Original Comment:</th> |
|
1078
|
@ <td class="infoComment">%!W(zComment)</td></tr> |
|
1079
|
} |
|
1080
|
if( fossil_strcmp(zDate, zOrigDate)!=0 |
|
1081
|
|| fossil_strcmp(zOrigUser, zUser)!=0 |
|
1082
|
){ |
|
1083
|
@ <tr><th>Original User & Date:</th><td> |
|
1084
|
hyperlink_to_user(zOrigUser,zOrigDate," on "); |
|
1085
|
hyperlink_to_date(zOrigDate, "</td></tr>"); |
|
1086
|
} |
|
1087
|
if( g.perm.Admin ){ |
|
1088
|
db_prepare(&q2, |
|
1089
|
"SELECT rcvfrom.ipaddr, user.login, datetime(rcvfrom.mtime)," |
|
1090
|
" blob.rcvid" |
|
1091
|
" FROM blob JOIN rcvfrom USING(rcvid) LEFT JOIN user USING(uid)" |
|
1092
|
" WHERE blob.rid=%d", |
|
1093
|
rid |
|
1094
|
); |
|
1095
|
if( db_step(&q2)==SQLITE_ROW ){ |
|
1096
|
const char *zIpAddr = db_column_text(&q2, 0); |
|
1097
|
const char *zUser = db_column_text(&q2, 1); |
|
1098
|
const char *zDate = db_column_text(&q2, 2); |
|
1099
|
int rcvid = db_column_int(&q2,3); |
|
1100
|
if( zUser==0 || zUser[0]==0 ) zUser = "unknown"; |
|
1101
|
@ <tr><th>Received From:</th> |
|
1102
|
@ <td>%h(zUser) @ %h(zIpAddr) on %s(zDate) \ |
|
1103
|
@ (<a href="%R/rcvfrom?rcvid=%d(rcvid)">Rcvid %d(rcvid)</a>)</td></tr> |
|
1104
|
} |
|
1105
|
db_finalize(&q2); |
|
1106
|
} |
|
1107
|
|
|
1108
|
/* Only show links to edit wiki pages if the users can read wiki |
|
1109
|
** and if the wiki pages already exist */ |
|
1110
|
if( g.perm.WrWiki |
|
1111
|
&& g.perm.RdWiki |
|
1112
|
&& g.perm.Write |
|
1113
|
&& ((okWiki = wiki_tagid2("checkin",zUuid))!=0 || |
|
1114
|
blob_size(&wiki_read_links)>0) |
|
1115
|
&& db_get_boolean("wiki-about",1) |
|
1116
|
){ |
|
1117
|
const char *zLinks = blob_str(&wiki_read_links); |
|
1118
|
@ <tr><th>Edit Wiki:</th><td>\ |
|
1119
|
if( okWiki ){ |
|
1120
|
@ %z(href("%R/wikiedit?name=checkin/%s",zUuid))this check-in</a>\ |
|
1121
|
}else if( zLinks[0] ){ |
|
1122
|
zLinks += 3; |
|
1123
|
} |
|
1124
|
@ %s(zLinks)</td></tr> |
|
1125
|
} |
|
1126
|
|
|
1127
|
/* Only show links to create new wiki pages if the users can write wiki |
|
1128
|
** and if the wiki pages do not already exist */ |
|
1129
|
if( g.perm.WrWiki |
|
1130
|
&& g.perm.RdWiki |
|
1131
|
&& g.perm.Write |
|
1132
|
&& (blob_size(&wiki_add_links)>0 || !okWiki) |
|
1133
|
&& db_get_boolean("wiki-about",1) |
|
1134
|
){ |
|
1135
|
const char *zLinks = blob_str(&wiki_add_links); |
|
1136
|
@ <tr><th>Add Wiki:</th><td>\ |
|
1137
|
if( !okWiki ){ |
|
1138
|
@ %z(href("%R/wikiedit?name=checkin/%s",zUuid))this check-in</a>\ |
|
1139
|
}else if( zLinks[0] ){ |
|
1140
|
zLinks += 3; |
|
1141
|
} |
|
1142
|
@ %s(zLinks)</td></tr> |
|
1143
|
} |
|
1144
|
|
|
1145
|
if( g.perm.Hyperlink ){ |
|
1146
|
const char *zMainBranch = db_main_branch(); |
|
1147
|
@ <tr><th>Other Links:</th> |
|
1148
|
@ <td> |
|
1149
|
if( fossil_strcmp(zBrName, zMainBranch)!=0 ){ |
|
1150
|
@ %z(href("%R/vdiff?branch=%!S", zUuid))branch diff</a> | |
|
1151
|
} |
|
1152
|
@ %z(href("%R/artifact/%!S",zUuid))manifest</a> |
|
1153
|
@ | %z(href("%R/ci_tags/%!S",zUuid))tags</a> |
|
1154
|
if( g.perm.Admin ){ |
|
1155
|
@ | %z(href("%R/mlink?ci=%!S",zUuid))mlink table</a> |
|
1156
|
} |
|
1157
|
if( g.anon.Write ){ |
|
1158
|
@ | %z(href("%R/ci_edit?r=%!S",zUuid))edit</a> |
|
1159
|
} |
|
1160
|
@ </td> |
|
1161
|
@ </tr> |
|
1162
|
} |
|
1163
|
@ </table> |
|
1164
|
blob_reset(&wiki_read_links); |
|
1165
|
blob_reset(&wiki_add_links); |
|
1166
|
}else{ |
|
1167
|
style_header("Check-in Information"); |
|
1168
|
login_anonymous_available(); |
|
1169
|
} |
|
1170
|
db_finalize(&q1); |
|
1171
|
@ </div> |
|
1172
|
builtin_request_js("accordion.js"); |
|
1173
|
if( !PB("nowiki") ){ |
|
1174
|
wiki_render_associated("checkin", zUuid, 0); |
|
1175
|
} |
|
1176
|
render_backlink_graph(zUuid, |
|
1177
|
"<div class=\"section accordion\">References</div>\n"); |
|
1178
|
@ <div class="section accordion">Context</div><div class="accordion_panel"> |
|
1179
|
render_checkin_context(rid, 0, 0, 0); |
|
1180
|
@ </div><div class="section accordion" id="changes_section">Changes</div> |
|
1181
|
@ <div class="accordion_panel"> |
|
1182
|
@ <div class="sectionmenu info-changes-menu"> |
|
1183
|
/* ^^^ .info-changes-menu is used by diff scroll sync */ |
|
1184
|
pCfg = construct_diff_flags(diffType, &DCfg); |
|
1185
|
DCfg.diffFlags |= DIFF_NUMSTAT; /* Show stats in the 'Changes' section */ |
|
1186
|
DCfg.pRe = pRe; |
|
1187
|
zW = (DCfg.diffFlags&DIFF_IGNORE_ALLWS)?"&w":""; |
|
1188
|
if( diffType!=1 ){ |
|
1189
|
@ %z(chref("button","%R/%s/%T?diff=1%s",zPage,zName,zW))\ |
|
1190
|
@ Unified Diff</a> |
|
1191
|
} |
|
1192
|
if( diffType!=2 ){ |
|
1193
|
@ %z(chref("button","%R/%s/%T?diff=2%s",zPage,zName,zW))\ |
|
1194
|
@ Side-by-Side Diff</a> |
|
1195
|
} |
|
1196
|
if( diffType!=0 ){ |
|
1197
|
if( *zW ){ |
|
1198
|
@ %z(chref("button","%R/%s/%T?diff=%d",zPage,zName,diffType)) |
|
1199
|
@ Show Whitespace Changes</a> |
|
1200
|
}else{ |
|
1201
|
@ %z(chref("button","%R/%s/%T?diff=%d&w",zPage,zName,diffType)) |
|
1202
|
@ Ignore Whitespace</a> |
|
1203
|
} |
|
1204
|
} |
|
1205
|
if( zParent ){ |
|
1206
|
@ %z(chref("button","%R/vpatch?from=%!S&to=%!S",zParent,zUuid)) |
|
1207
|
@ Patch</a> |
|
1208
|
} |
|
1209
|
if( g.perm.Admin ){ |
|
1210
|
@ %z(chref("button","%R/mlink?ci=%!S",zUuid))MLink Table</a> |
|
1211
|
} |
|
1212
|
@ </div> |
|
1213
|
if( pRe ){ |
|
1214
|
@ <p><b>Only differences that match regular expression "%h(zRe)" |
|
1215
|
@ are shown.</b></p> |
|
1216
|
} |
|
1217
|
db_prepare(&q3, |
|
1218
|
"SELECT name," |
|
1219
|
" mperm," |
|
1220
|
" (SELECT uuid FROM blob WHERE rid=mlink.pid)," |
|
1221
|
" (SELECT uuid FROM blob WHERE rid=mlink.fid)," |
|
1222
|
" (SELECT name FROM filename WHERE filename.fnid=mlink.pfnid)" |
|
1223
|
" FROM mlink JOIN filename ON filename.fnid=mlink.fnid" |
|
1224
|
" WHERE mlink.mid=%d AND NOT mlink.isaux" |
|
1225
|
" AND (mlink.fid>0" |
|
1226
|
" OR mlink.fnid NOT IN (SELECT pfnid FROM mlink WHERE mid=%d))" |
|
1227
|
" ORDER BY name /*sort*/", |
|
1228
|
rid, rid |
|
1229
|
); |
|
1230
|
while( db_step(&q3)==SQLITE_ROW ){ |
|
1231
|
const char *zName = db_column_text(&q3,0); |
|
1232
|
int mperm = db_column_int(&q3, 1); |
|
1233
|
const char *zOld = db_column_text(&q3,2); |
|
1234
|
const char *zNew = db_column_text(&q3,3); |
|
1235
|
const char *zOldName = db_column_text(&q3, 4); |
|
1236
|
append_file_change_line(zUuid, zName, zOld, zNew, zOldName, |
|
1237
|
pCfg,mperm); |
|
1238
|
} |
|
1239
|
db_finalize(&q3); |
|
1240
|
@ </div> |
|
1241
|
if( diffType!=0 ){ |
|
1242
|
@ <script nonce='%h(style_nonce())'>;/* info.c:%d(__LINE__) */ |
|
1243
|
@ document.getElementById('changes_section').textContent = 'Changes ' + |
|
1244
|
@ '(%d(g.diffCnt[0]) file' + (%d(g.diffCnt[0])===1 ? '' : 's') + ': ' + |
|
1245
|
@ '+%d(g.diffCnt[1]) ' + |
|
1246
|
@ '−%d(g.diffCnt[2]))' |
|
1247
|
@ </script> |
|
1248
|
} |
|
1249
|
append_diff_javascript(diffType); |
|
1250
|
style_finish_page(); |
|
1251
|
} |
|
1252
|
|
|
1253
|
/* |
|
1254
|
** WEBPAGE: winfo |
|
1255
|
** URL: /winfo?name=HASH |
|
1256
|
** |
|
1257
|
** Display information about a wiki page. |
|
1258
|
*/ |
|
1259
|
void winfo_page(void){ |
|
1260
|
int rid; |
|
1261
|
Manifest *pWiki; |
|
1262
|
char *zUuid; |
|
1263
|
char *zDate; |
|
1264
|
Blob wiki; |
|
1265
|
int modPending; |
|
1266
|
const char *zModAction; |
|
1267
|
int tagid; |
|
1268
|
int ridNext; |
|
1269
|
|
|
1270
|
login_check_credentials(); |
|
1271
|
if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; } |
|
1272
|
style_set_current_feature("winfo"); |
|
1273
|
rid = name_to_rid_www("name"); |
|
1274
|
if( rid==0 || (pWiki = manifest_get(rid, CFTYPE_WIKI, 0))==0 ){ |
|
1275
|
style_header("Wiki Page Information Error"); |
|
1276
|
@ No such object: %h(P("name")) |
|
1277
|
style_finish_page(); |
|
1278
|
return; |
|
1279
|
} |
|
1280
|
if( g.perm.ModWiki && (zModAction = P("modaction"))!=0 ){ |
|
1281
|
if( strcmp(zModAction,"delete")==0 ){ |
|
1282
|
moderation_disapprove(rid); |
|
1283
|
/* |
|
1284
|
** Next, check if the wiki page still exists; if not, we cannot |
|
1285
|
** redirect to it. |
|
1286
|
*/ |
|
1287
|
if( db_exists("SELECT 1 FROM tagxref JOIN tag USING(tagid)" |
|
1288
|
" WHERE rid=%d AND tagname LIKE 'wiki-%%'", rid) ){ |
|
1289
|
cgi_redirectf("%R/wiki?name=%T", pWiki->zWikiTitle); |
|
1290
|
/*NOTREACHED*/ |
|
1291
|
}else{ |
|
1292
|
cgi_redirectf("%R/modreq"); |
|
1293
|
/*NOTREACHED*/ |
|
1294
|
} |
|
1295
|
} |
|
1296
|
if( strcmp(zModAction,"approve")==0 ){ |
|
1297
|
moderation_approve('w', rid); |
|
1298
|
} |
|
1299
|
} |
|
1300
|
cgi_check_for_malice(); |
|
1301
|
style_header("Update of \"%h\"", pWiki->zWikiTitle); |
|
1302
|
zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); |
|
1303
|
zDate = db_text(0, "SELECT datetime(%.17g,toLocal())", pWiki->rDate); |
|
1304
|
style_submenu_element("Raw", "%R/artifact/%s", zUuid); |
|
1305
|
style_submenu_element("History", "%R/whistory?name=%t", pWiki->zWikiTitle); |
|
1306
|
style_submenu_element("Page", "%R/wiki?name=%t", pWiki->zWikiTitle); |
|
1307
|
login_anonymous_available(); |
|
1308
|
@ <div class="section">Overview</div> |
|
1309
|
@ <p><table class="label-value"> |
|
1310
|
@ <tr><th>Artifact ID:</th> |
|
1311
|
@ <td>%z(href("%R/artifact/%!S",zUuid))%s(zUuid)</a> |
|
1312
|
if( g.perm.Setup ){ |
|
1313
|
@ (%d(rid)) |
|
1314
|
} |
|
1315
|
modPending = moderation_pending_www(rid); |
|
1316
|
@ </td></tr> |
|
1317
|
@ <tr><th>Page Name:</th>\ |
|
1318
|
@ <td>%z(href("%R/whistory?name=%h",pWiki->zWikiTitle))\ |
|
1319
|
@ %h(pWiki->zWikiTitle)</a></td></tr> |
|
1320
|
@ <tr><th>Date:</th><td> |
|
1321
|
hyperlink_to_date(zDate, "</td></tr>"); |
|
1322
|
@ <tr><th>Original User:</th><td> |
|
1323
|
hyperlink_to_user(pWiki->zUser, zDate, "</td></tr>"); |
|
1324
|
if( pWiki->zMimetype ){ |
|
1325
|
@ <tr><th>Mimetype:</th><td>%h(pWiki->zMimetype)</td></tr> |
|
1326
|
} |
|
1327
|
if( pWiki->nParent>0 ){ |
|
1328
|
int i; |
|
1329
|
@ <tr><th>Parent%s(pWiki->nParent==1?"":"s"):</th><td> |
|
1330
|
for(i=0; i<pWiki->nParent; i++){ |
|
1331
|
char *zParent = pWiki->azParent[i]; |
|
1332
|
@ %z(href("%R/info/%!S",zParent))%s(zParent)</a> |
|
1333
|
@ %z(href("%R/wdiff?id=%!S&pid=%!S",zUuid,zParent))(diff)</a> |
|
1334
|
} |
|
1335
|
@ </td></tr> |
|
1336
|
} |
|
1337
|
tagid = wiki_tagid(pWiki->zWikiTitle); |
|
1338
|
if( tagid>0 && (ridNext = wiki_next(tagid, pWiki->rDate))>0 ){ |
|
1339
|
char *zId = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", ridNext); |
|
1340
|
@ <tr><th>Next</th> |
|
1341
|
@ <td>%z(href("%R/info/%!S",zId))%s(zId)</a></td> |
|
1342
|
} |
|
1343
|
@ </table> |
|
1344
|
|
|
1345
|
if( g.perm.ModWiki && modPending ){ |
|
1346
|
@ <div class="section">Moderation</div> |
|
1347
|
@ <blockquote> |
|
1348
|
@ <form method="POST" action="%R/winfo/%s(zUuid)"> |
|
1349
|
@ <label><input type="radio" name="modaction" value="delete"> |
|
1350
|
@ Delete this change</label><br> |
|
1351
|
@ <label><input type="radio" name="modaction" value="approve"> |
|
1352
|
@ Approve this change</label><br> |
|
1353
|
@ <input type="submit" value="Submit"> |
|
1354
|
@ </form> |
|
1355
|
@ </blockquote> |
|
1356
|
} |
|
1357
|
|
|
1358
|
|
|
1359
|
@ <div class="section">Content</div> |
|
1360
|
blob_init(&wiki, pWiki->zWiki, -1); |
|
1361
|
safe_html_context(DOCSRC_WIKI); |
|
1362
|
wiki_render_by_mimetype(&wiki, pWiki->zMimetype); |
|
1363
|
blob_reset(&wiki); |
|
1364
|
manifest_destroy(pWiki); |
|
1365
|
document_emit_js(); |
|
1366
|
style_finish_page(); |
|
1367
|
} |
|
1368
|
|
|
1369
|
/* |
|
1370
|
** Find an check-in based on query parameter zParam and parse its |
|
1371
|
** manifest. Return the number of errors. |
|
1372
|
*/ |
|
1373
|
static Manifest *vdiff_parse_manifest(const char *zParam, int *pRid){ |
|
1374
|
int rid; |
|
1375
|
|
|
1376
|
*pRid = rid = name_to_rid_www(zParam); |
|
1377
|
if( rid==0 ){ |
|
1378
|
const char *z = P(zParam); |
|
1379
|
if( z==0 || z[0]==0 ){ |
|
1380
|
webpage_error("Missing \"%s\" query parameter.", zParam); |
|
1381
|
}else{ |
|
1382
|
webpage_error("No such artifact: \"%s\"", z); |
|
1383
|
} |
|
1384
|
return 0; |
|
1385
|
} |
|
1386
|
if( !is_a_version(rid) ){ |
|
1387
|
webpage_error("Artifact %s is not a check-in.", P(zParam)); |
|
1388
|
return 0; |
|
1389
|
} |
|
1390
|
return manifest_get(rid, CFTYPE_MANIFEST, 0); |
|
1391
|
} |
|
1392
|
|
|
1393
|
/* |
|
1394
|
** WEBPAGE: vdiff |
|
1395
|
** URL: /vdiff?from=TAG&to=TAG |
|
1396
|
** |
|
1397
|
** Show the difference between two check-ins identified by the from= and |
|
1398
|
** to= query parameters. |
|
1399
|
** |
|
1400
|
** Query parameters: |
|
1401
|
** |
|
1402
|
** from=TAG Left side of the comparison |
|
1403
|
** to=TAG Right side of the comparison |
|
1404
|
** branch=TAG Show all changes on a particular branch |
|
1405
|
** diff=INTEGER 0: none, 1: unified, 2: side-by-side |
|
1406
|
** glob=STRING only diff files matching this glob |
|
1407
|
** dc=N show N lines of context around each diff |
|
1408
|
** w=BOOLEAN ignore whitespace when computing diffs |
|
1409
|
** nohdr omit the description at the top of the page |
|
1410
|
** nc omit branch coloration from the header graph |
|
1411
|
** inv "Invert". Exchange the roles of from= and to= |
|
1412
|
** |
|
1413
|
** Show all differences between two check-ins. |
|
1414
|
*/ |
|
1415
|
void vdiff_page(void){ |
|
1416
|
int ridFrom, ridTo; |
|
1417
|
int diffType = 0; /* 0: none, 1: unified, 2: side-by-side */ |
|
1418
|
Manifest *pFrom, *pTo; |
|
1419
|
ManifestFile *pFileFrom, *pFileTo; |
|
1420
|
const char *zBranch; |
|
1421
|
const char *zFrom; |
|
1422
|
const char *zTo; |
|
1423
|
const char *zRe; |
|
1424
|
const char *zGlob; |
|
1425
|
Glob * pGlob = 0; |
|
1426
|
char *zMergeOrigin = 0; |
|
1427
|
ReCompiled *pRe = 0; |
|
1428
|
DiffConfig DCfg, *pCfg = 0; |
|
1429
|
int graphFlags = 0; |
|
1430
|
Blob qp; /* non-glob= query parameters for generated links */ |
|
1431
|
Blob qpGlob; /* glob= query parameter for generated links */ |
|
1432
|
int bInvert = PB("inv"); |
|
1433
|
|
|
1434
|
login_check_credentials(); |
|
1435
|
if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
|
1436
|
if( robot_restrict("diff") ) return; |
|
1437
|
login_anonymous_available(); |
|
1438
|
fossil_nice_default(); |
|
1439
|
blob_init(&qp, 0, 0); |
|
1440
|
blob_init(&qpGlob, 0, 0); |
|
1441
|
diffType = preferred_diff_type(); |
|
1442
|
zRe = P("regex"); |
|
1443
|
if( zRe ) fossil_re_compile(&pRe, zRe, 0); |
|
1444
|
zBranch = P("branch"); |
|
1445
|
if( zBranch && zBranch[0]==0 ) zBranch = 0; |
|
1446
|
if( zBranch ){ |
|
1447
|
blob_appendf(&qp, "branch=%T", zBranch); |
|
1448
|
zMergeOrigin = mprintf("merge-in:%s", zBranch); |
|
1449
|
cgi_replace_parameter("from", zMergeOrigin); |
|
1450
|
cgi_replace_parameter("to", zBranch); |
|
1451
|
}else{ |
|
1452
|
if( bInvert ){ |
|
1453
|
blob_appendf(&qp, "to=%T&from=%T",PD("from",""),PD("to","")); |
|
1454
|
}else{ |
|
1455
|
blob_appendf(&qp, "from=%T&to=%T",PD("from",""),PD("to","")); |
|
1456
|
} |
|
1457
|
} |
|
1458
|
pTo = vdiff_parse_manifest("to", &ridTo); |
|
1459
|
if( pTo==0 ) return; |
|
1460
|
pFrom = vdiff_parse_manifest("from", &ridFrom); |
|
1461
|
if( pFrom==0 ) return; |
|
1462
|
zGlob = P("glob"); |
|
1463
|
/* |
|
1464
|
** Maintenace reminder: we explicitly do _not_ use P_NoBot() |
|
1465
|
** for "from" and "to" because those args can contain legitimate |
|
1466
|
** strings which may trigger the looks-like SQL checks, e.g. |
|
1467
|
** from=merge-in:OR-clause-improvement |
|
1468
|
** to=OR-clause-improvement |
|
1469
|
*/ |
|
1470
|
zFrom = P("from"); |
|
1471
|
zTo = P("to"); |
|
1472
|
if( bInvert ){ |
|
1473
|
Manifest *pTemp = pTo; |
|
1474
|
const char *zTemp = zTo; |
|
1475
|
pTo = pFrom; |
|
1476
|
pFrom = pTemp; |
|
1477
|
zTo = zFrom; |
|
1478
|
zFrom = zTemp; |
|
1479
|
} |
|
1480
|
if( zGlob ){ |
|
1481
|
if( !*zGlob ){ |
|
1482
|
zGlob = NULL; |
|
1483
|
}else{ |
|
1484
|
blob_appendf(&qpGlob, "&glob=%T", zGlob); |
|
1485
|
pGlob = glob_create(zGlob); |
|
1486
|
} |
|
1487
|
} |
|
1488
|
if( PB("nc") ){ |
|
1489
|
graphFlags |= TIMELINE_NOCOLOR; |
|
1490
|
blob_appendf(&qp, "&nc"); |
|
1491
|
} |
|
1492
|
pCfg = construct_diff_flags(diffType, &DCfg); |
|
1493
|
if( DCfg.diffFlags & DIFF_IGNORE_ALLWS ){ |
|
1494
|
blob_appendf(&qp, "&w"); |
|
1495
|
} |
|
1496
|
cgi_check_for_malice(); |
|
1497
|
style_set_current_feature("vdiff"); |
|
1498
|
if( zBranch==0 ){ |
|
1499
|
style_submenu_element("Path", "%R/timeline?me=%T&you=%T", zFrom, zTo); |
|
1500
|
} |
|
1501
|
if( diffType!=2 ){ |
|
1502
|
style_submenu_element("Side-by-Side Diff", "%R/vdiff?diff=2&%b%b", &qp, |
|
1503
|
&qpGlob); |
|
1504
|
} |
|
1505
|
if( diffType!=1 ) { |
|
1506
|
style_submenu_element("Unified Diff", "%R/vdiff?diff=1&%b%b", &qp, &qpGlob); |
|
1507
|
} |
|
1508
|
if( zBranch==0 ){ |
|
1509
|
style_submenu_element("Invert","%R/vdiff?diff=%d&inv&%b%b", diffType, |
|
1510
|
&qp, &qpGlob); |
|
1511
|
} |
|
1512
|
if( zGlob ){ |
|
1513
|
style_submenu_element("Clear glob", "%R/vdiff?diff=%d&%b", diffType, &qp); |
|
1514
|
}else{ |
|
1515
|
style_submenu_element("Patch", "%R/vpatch?from=%T&to=%T%s", zFrom, zTo, |
|
1516
|
(DCfg.diffFlags & DIFF_IGNORE_ALLWS)?"&w":""); |
|
1517
|
} |
|
1518
|
if( diffType!=0 ){ |
|
1519
|
style_submenu_checkbox("w", "Ignore Whitespace", 0, 0); |
|
1520
|
} |
|
1521
|
if( zBranch ){ |
|
1522
|
style_header("Changes On Branch %h", zBranch); |
|
1523
|
}else{ |
|
1524
|
style_header("Check-in Differences"); |
|
1525
|
} |
|
1526
|
if( P("nohdr")==0 ){ |
|
1527
|
if( zBranch ){ |
|
1528
|
char *zRealBranch = branch_of_rid(ridTo); |
|
1529
|
char *zToUuid = rid_to_uuid(ridTo); |
|
1530
|
char *zFromUuid = rid_to_uuid(ridFrom); |
|
1531
|
@ <h2>Changes In Branch \ |
|
1532
|
@ %z(href("%R/timeline?r=%T",zRealBranch))%h(zRealBranch)</a> |
|
1533
|
if( ridTo != symbolic_name_to_rid(zRealBranch,"ci") ){ |
|
1534
|
@ Through %z(href("%R/info/%!S",zToUuid))[%S(zToUuid)]</a> |
|
1535
|
} |
|
1536
|
@ Excluding Merge-Ins</h2> |
|
1537
|
@ <p>This is equivalent to a diff from |
|
1538
|
@ <span class='timelineSelected'>\ |
|
1539
|
@ %z(href("%R/info/%!S",zFromUuid))%S(zFromUuid)</a></span> |
|
1540
|
@ to <span class='timelineSelected timelineSecondary'>\ |
|
1541
|
@ %z(href("%R/info/%!S",zToUuid))%S(zToUuid)</a></span></p> |
|
1542
|
}else{ |
|
1543
|
@ <h2>Difference From <span class='timelineSelected'>\ |
|
1544
|
@ %z(href("%R/info/%h",zFrom))%h(zFrom)</a></span> |
|
1545
|
@ To <span class='timelineSelected timelineSecondary'>\ |
|
1546
|
@ %z(href("%R/info/%h",zTo))%h(zTo)</a></span></h2> |
|
1547
|
} |
|
1548
|
render_checkin_context(ridFrom, ridTo, 0, graphFlags); |
|
1549
|
if( pRe ){ |
|
1550
|
@ <p><b>Only differences that match regular expression "%h(zRe)" |
|
1551
|
@ are shown.</b></p> |
|
1552
|
} |
|
1553
|
if( zGlob ){ |
|
1554
|
@ <p><b>Only files matching the glob "%h(zGlob)" are shown.</b></p> |
|
1555
|
} |
|
1556
|
@<hr><p> |
|
1557
|
} |
|
1558
|
blob_reset(&qp); |
|
1559
|
blob_reset(&qpGlob); |
|
1560
|
|
|
1561
|
manifest_file_rewind(pFrom); |
|
1562
|
pFileFrom = manifest_file_next(pFrom, 0); |
|
1563
|
manifest_file_rewind(pTo); |
|
1564
|
pFileTo = manifest_file_next(pTo, 0); |
|
1565
|
DCfg.pRe = pRe; |
|
1566
|
while( pFileFrom || pFileTo ){ |
|
1567
|
int cmp; |
|
1568
|
if( pFileFrom==0 ){ |
|
1569
|
cmp = +1; |
|
1570
|
}else if( pFileTo==0 ){ |
|
1571
|
cmp = -1; |
|
1572
|
}else{ |
|
1573
|
cmp = fossil_strcmp(pFileFrom->zName, pFileTo->zName); |
|
1574
|
} |
|
1575
|
if( cmp<0 ){ |
|
1576
|
if( !pGlob || glob_match(pGlob, pFileFrom->zName) ){ |
|
1577
|
append_file_change_line(zFrom, pFileFrom->zName, |
|
1578
|
pFileFrom->zUuid, 0, 0, pCfg, 0); |
|
1579
|
} |
|
1580
|
pFileFrom = manifest_file_next(pFrom, 0); |
|
1581
|
}else if( cmp>0 ){ |
|
1582
|
if( !pGlob || glob_match(pGlob, pFileTo->zName) ){ |
|
1583
|
append_file_change_line(zTo, pFileTo->zName, |
|
1584
|
0, pFileTo->zUuid, 0, pCfg, |
|
1585
|
manifest_file_mperm(pFileTo)); |
|
1586
|
} |
|
1587
|
pFileTo = manifest_file_next(pTo, 0); |
|
1588
|
}else if( fossil_strcmp(pFileFrom->zUuid, pFileTo->zUuid)==0 ){ |
|
1589
|
pFileFrom = manifest_file_next(pFrom, 0); |
|
1590
|
pFileTo = manifest_file_next(pTo, 0); |
|
1591
|
}else{ |
|
1592
|
if(!pGlob || (glob_match(pGlob, pFileFrom->zName) |
|
1593
|
|| glob_match(pGlob, pFileTo->zName)) ){ |
|
1594
|
append_file_change_line(zFrom, pFileFrom->zName, |
|
1595
|
pFileFrom->zUuid, |
|
1596
|
pFileTo->zUuid, 0, pCfg, |
|
1597
|
manifest_file_mperm(pFileTo)); |
|
1598
|
} |
|
1599
|
pFileFrom = manifest_file_next(pFrom, 0); |
|
1600
|
pFileTo = manifest_file_next(pTo, 0); |
|
1601
|
} |
|
1602
|
} |
|
1603
|
glob_free(pGlob); |
|
1604
|
manifest_destroy(pFrom); |
|
1605
|
manifest_destroy(pTo); |
|
1606
|
append_diff_javascript(diffType); |
|
1607
|
style_finish_page(); |
|
1608
|
} |
|
1609
|
|
|
1610
|
#if INTERFACE |
|
1611
|
/* |
|
1612
|
** Possible return values from object_description() |
|
1613
|
*/ |
|
1614
|
#define OBJTYPE_CHECKIN 0x0001 |
|
1615
|
#define OBJTYPE_CONTENT 0x0002 |
|
1616
|
#define OBJTYPE_WIKI 0x0004 |
|
1617
|
#define OBJTYPE_TICKET 0x0008 |
|
1618
|
#define OBJTYPE_ATTACHMENT 0x0010 |
|
1619
|
#define OBJTYPE_EVENT 0x0020 |
|
1620
|
#define OBJTYPE_TAG 0x0040 |
|
1621
|
#define OBJTYPE_SYMLINK 0x0080 |
|
1622
|
#define OBJTYPE_EXE 0x0100 |
|
1623
|
#define OBJTYPE_FORUM 0x0200 |
|
1624
|
|
|
1625
|
/* |
|
1626
|
** Possible flags for the second parameter to |
|
1627
|
** object_description() |
|
1628
|
*/ |
|
1629
|
#define OBJDESC_DETAIL 0x0001 /* Show more detail */ |
|
1630
|
#define OBJDESC_BASE 0x0002 /* Set <base> using this object */ |
|
1631
|
#endif |
|
1632
|
|
|
1633
|
/* |
|
1634
|
** Write a description of an object to the www reply. |
|
1635
|
*/ |
|
1636
|
int object_description( |
|
1637
|
int rid, /* The artifact ID for the object to describe */ |
|
1638
|
u32 objdescFlags, /* Flags to control display */ |
|
1639
|
const char *zFileName, /* For file objects, use this name. Can be NULL */ |
|
1640
|
Blob *pDownloadName /* Fill with a good download name. Can be NULL */ |
|
1641
|
){ |
|
1642
|
Stmt q; |
|
1643
|
int cnt = 0; |
|
1644
|
int nWiki = 0; |
|
1645
|
int objType = 0; |
|
1646
|
char *zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); |
|
1647
|
int showDetail = (objdescFlags & OBJDESC_DETAIL)!=0; |
|
1648
|
char *prevName = 0; |
|
1649
|
int bNeedBase = (objdescFlags & OBJDESC_BASE)!=0; |
|
1650
|
|
|
1651
|
db_prepare(&q, |
|
1652
|
"SELECT filename.name, datetime(event.mtime,toLocal())," |
|
1653
|
" coalesce(event.ecomment,event.comment)," |
|
1654
|
" coalesce(event.euser,event.user)," |
|
1655
|
" b.uuid, mlink.mperm," |
|
1656
|
" coalesce((SELECT value FROM tagxref" |
|
1657
|
" WHERE tagid=%d AND tagtype>0 AND rid=mlink.mid),'trunk')," |
|
1658
|
" a.size" |
|
1659
|
" FROM mlink, filename, event, blob a, blob b" |
|
1660
|
" WHERE filename.fnid=mlink.fnid" |
|
1661
|
" AND event.objid=mlink.mid" |
|
1662
|
" AND a.rid=mlink.fid" |
|
1663
|
" AND b.rid=mlink.mid" |
|
1664
|
" AND mlink.fid=%d" |
|
1665
|
" ORDER BY filename.name, event.mtime /*sort*/", |
|
1666
|
TAG_BRANCH, rid |
|
1667
|
); |
|
1668
|
@ <ul> |
|
1669
|
while( db_step(&q)==SQLITE_ROW ){ |
|
1670
|
const char *zName = db_column_text(&q, 0); |
|
1671
|
const char *zDate = db_column_text(&q, 1); |
|
1672
|
const char *zCom = db_column_text(&q, 2); |
|
1673
|
const char *zUser = db_column_text(&q, 3); |
|
1674
|
const char *zVers = db_column_text(&q, 4); |
|
1675
|
int mPerm = db_column_int(&q, 5); |
|
1676
|
const char *zBr = db_column_text(&q, 6); |
|
1677
|
int szFile = db_column_int(&q,7); |
|
1678
|
int sameFilename = prevName!=0 && fossil_strcmp(zName,prevName)==0; |
|
1679
|
if( zFileName && fossil_strcmp(zName,zFileName)!=0 ) continue; |
|
1680
|
if( sameFilename && !showDetail ){ |
|
1681
|
if( cnt==1 ){ |
|
1682
|
@ %z(href("%R/whatis/%!S",zUuid))[more...]</a> |
|
1683
|
} |
|
1684
|
cnt++; |
|
1685
|
continue; |
|
1686
|
} |
|
1687
|
if( !sameFilename ){ |
|
1688
|
if( prevName && showDetail ) { |
|
1689
|
@ </ul> |
|
1690
|
} |
|
1691
|
if( mPerm==PERM_LNK ){ |
|
1692
|
@ <li>Symbolic link |
|
1693
|
objType |= OBJTYPE_SYMLINK; |
|
1694
|
}else if( mPerm==PERM_EXE ){ |
|
1695
|
@ <li>Executable file |
|
1696
|
objType |= OBJTYPE_EXE; |
|
1697
|
}else{ |
|
1698
|
@ <li>File |
|
1699
|
if( bNeedBase ){ |
|
1700
|
bNeedBase = 0; |
|
1701
|
style_set_current_page("doc/%S/%s",zVers,zName); |
|
1702
|
} |
|
1703
|
} |
|
1704
|
objType |= OBJTYPE_CONTENT; |
|
1705
|
@ %z(href("%R/finfo?name=%T&ci=%!S&m=%!S",zName,zVers,zUuid))\ |
|
1706
|
@ %h(zName)</a> |
|
1707
|
tag_private_status(rid); |
|
1708
|
if( showDetail ){ |
|
1709
|
@ <ul> |
|
1710
|
} |
|
1711
|
prevName = fossil_strdup(zName); |
|
1712
|
} |
|
1713
|
if( showDetail ){ |
|
1714
|
@ <li> |
|
1715
|
hyperlink_to_date(zDate,""); |
|
1716
|
@ — part of check-in |
|
1717
|
hyperlink_to_version(zVers); |
|
1718
|
}else{ |
|
1719
|
@ — part of check-in |
|
1720
|
hyperlink_to_version(zVers); |
|
1721
|
@ at |
|
1722
|
hyperlink_to_date(zDate,""); |
|
1723
|
} |
|
1724
|
if( zBr && zBr[0] ){ |
|
1725
|
@ on branch %z(href("%R/timeline?r=%T",zBr))%h(zBr)</a> |
|
1726
|
} |
|
1727
|
@ — %!W(zCom) (user: |
|
1728
|
hyperlink_to_user(zUser,zDate,","); |
|
1729
|
@ size: %d(szFile)) |
|
1730
|
if( g.perm.Hyperlink ){ |
|
1731
|
@ %z(href("%R/annotate?filename=%T&checkin=%!S",zName,zVers)) |
|
1732
|
@ [annotate]</a> |
|
1733
|
@ %z(href("%R/blame?filename=%T&checkin=%!S",zName,zVers)) |
|
1734
|
@ [blame]</a> |
|
1735
|
@ %z(href("%R/timeline?uf=%!S",zUuid))[check-ins using]</a> |
|
1736
|
if( fileedit_is_editable(zName) ){ |
|
1737
|
@ %z(href("%R/fileedit?filename=%T&checkin=%!S",zName,zVers))[edit]</a> |
|
1738
|
} |
|
1739
|
} |
|
1740
|
cnt++; |
|
1741
|
if( pDownloadName && blob_size(pDownloadName)==0 ){ |
|
1742
|
blob_append(pDownloadName, zName, -1); |
|
1743
|
} |
|
1744
|
} |
|
1745
|
if( prevName && showDetail ){ |
|
1746
|
@ </ul> |
|
1747
|
} |
|
1748
|
@ </ul> |
|
1749
|
free(prevName); |
|
1750
|
db_finalize(&q); |
|
1751
|
db_prepare(&q, |
|
1752
|
"SELECT substr(tagname, 6, 10000), datetime(event.mtime, toLocal())," |
|
1753
|
" coalesce(event.euser, event.user)" |
|
1754
|
" FROM tagxref, tag, event" |
|
1755
|
" WHERE tagxref.rid=%d" |
|
1756
|
" AND tag.tagid=tagxref.tagid" |
|
1757
|
" AND tag.tagname LIKE 'wiki-%%'" |
|
1758
|
" AND event.objid=tagxref.rid", |
|
1759
|
rid |
|
1760
|
); |
|
1761
|
while( db_step(&q)==SQLITE_ROW ){ |
|
1762
|
const char *zPagename = db_column_text(&q, 0); |
|
1763
|
const char *zDate = db_column_text(&q, 1); |
|
1764
|
const char *zUser = db_column_text(&q, 2); |
|
1765
|
if( cnt>0 ){ |
|
1766
|
@ Also wiki page |
|
1767
|
}else{ |
|
1768
|
@ Wiki page |
|
1769
|
} |
|
1770
|
objType |= OBJTYPE_WIKI; |
|
1771
|
@ [%z(href("%R/wiki?name=%t",zPagename))%h(zPagename)</a>] by |
|
1772
|
hyperlink_to_user(zUser,zDate," on"); |
|
1773
|
hyperlink_to_date(zDate,"."); |
|
1774
|
nWiki++; |
|
1775
|
cnt++; |
|
1776
|
if( pDownloadName && blob_size(pDownloadName)==0 ){ |
|
1777
|
blob_appendf(pDownloadName, "%s.txt", zPagename); |
|
1778
|
} |
|
1779
|
} |
|
1780
|
db_finalize(&q); |
|
1781
|
if( nWiki==0 ){ |
|
1782
|
db_prepare(&q, |
|
1783
|
"SELECT datetime(mtime, toLocal()), user, comment, type, uuid, tagid" |
|
1784
|
" FROM event, blob" |
|
1785
|
" WHERE event.objid=%d" |
|
1786
|
" AND blob.rid=%d", |
|
1787
|
rid, rid |
|
1788
|
); |
|
1789
|
while( db_step(&q)==SQLITE_ROW ){ |
|
1790
|
const char *zDate = db_column_text(&q, 0); |
|
1791
|
const char *zUser = db_column_text(&q, 1); |
|
1792
|
const char *zCom = db_column_text(&q, 2); |
|
1793
|
const char *zType = db_column_text(&q, 3); |
|
1794
|
const char *zUuid = db_column_text(&q, 4); |
|
1795
|
int eventTagId = db_column_int(&q, 5); |
|
1796
|
if( cnt>0 ){ |
|
1797
|
@ Also |
|
1798
|
} |
|
1799
|
if( zType[0]=='w' ){ |
|
1800
|
@ Wiki edit |
|
1801
|
objType |= OBJTYPE_WIKI; |
|
1802
|
}else if( zType[0]=='t' ){ |
|
1803
|
@ Ticket change |
|
1804
|
objType |= OBJTYPE_TICKET; |
|
1805
|
}else if( zType[0]=='c' ){ |
|
1806
|
@ Manifest of check-in |
|
1807
|
objType |= OBJTYPE_CHECKIN; |
|
1808
|
}else if( zType[0]=='e' ){ |
|
1809
|
if( eventTagId != 0) { |
|
1810
|
@ Instance of technote |
|
1811
|
objType |= OBJTYPE_EVENT; |
|
1812
|
hyperlink_to_event_tagid(db_column_int(&q, 5)); |
|
1813
|
}else{ |
|
1814
|
@ Attachment to technote |
|
1815
|
} |
|
1816
|
}else if( zType[0]=='f' ){ |
|
1817
|
objType |= OBJTYPE_FORUM; |
|
1818
|
@ Forum post |
|
1819
|
}else{ |
|
1820
|
@ Tag referencing |
|
1821
|
} |
|
1822
|
if( zType[0]!='e' || eventTagId == 0){ |
|
1823
|
hyperlink_to_version(zUuid); |
|
1824
|
} |
|
1825
|
@ - %!W(zCom) by |
|
1826
|
hyperlink_to_user(zUser,zDate," on"); |
|
1827
|
hyperlink_to_date(zDate, "."); |
|
1828
|
if( pDownloadName && blob_size(pDownloadName)==0 ){ |
|
1829
|
blob_appendf(pDownloadName, "%S.txt", zUuid); |
|
1830
|
} |
|
1831
|
tag_private_status(rid); |
|
1832
|
cnt++; |
|
1833
|
} |
|
1834
|
db_finalize(&q); |
|
1835
|
} |
|
1836
|
db_prepare(&q, |
|
1837
|
"SELECT target, filename, datetime(mtime, toLocal()), user, src" |
|
1838
|
" FROM attachment" |
|
1839
|
" WHERE src=(SELECT uuid FROM blob WHERE rid=%d)" |
|
1840
|
" ORDER BY mtime DESC /*sort*/", |
|
1841
|
rid |
|
1842
|
); |
|
1843
|
while( db_step(&q)==SQLITE_ROW ){ |
|
1844
|
const char *zTarget = db_column_text(&q, 0); |
|
1845
|
const char *zFilename = db_column_text(&q, 1); |
|
1846
|
const char *zDate = db_column_text(&q, 2); |
|
1847
|
const char *zUser = db_column_text(&q, 3); |
|
1848
|
/* const char *zSrc = db_column_text(&q, 4); */ |
|
1849
|
if( cnt>0 ){ |
|
1850
|
@ Also attachment "%h(zFilename)" to |
|
1851
|
}else{ |
|
1852
|
@ Attachment "%h(zFilename)" to |
|
1853
|
} |
|
1854
|
objType |= OBJTYPE_ATTACHMENT; |
|
1855
|
if( fossil_is_artifact_hash(zTarget) ){ |
|
1856
|
if ( db_exists("SELECT 1 FROM tag WHERE tagname='tkt-%q'", |
|
1857
|
zTarget) |
|
1858
|
){ |
|
1859
|
if( g.perm.Hyperlink && g.anon.RdTkt ){ |
|
1860
|
@ ticket [%z(href("%R/tktview?name=%!S",zTarget))%S(zTarget)</a>] |
|
1861
|
}else{ |
|
1862
|
@ ticket [%S(zTarget)] |
|
1863
|
} |
|
1864
|
}else if( db_exists("SELECT 1 FROM tag WHERE tagname='event-%q'", |
|
1865
|
zTarget) |
|
1866
|
){ |
|
1867
|
if( g.perm.Hyperlink && g.anon.RdWiki ){ |
|
1868
|
@ tech note [%z(href("%R/technote/%h",zTarget))%S(zTarget)</a>] |
|
1869
|
}else{ |
|
1870
|
@ tech note [%S(zTarget)] |
|
1871
|
} |
|
1872
|
}else{ |
|
1873
|
if( g.perm.Hyperlink && g.anon.RdWiki ){ |
|
1874
|
@ wiki page [%z(href("%R/wiki?name=%t",zTarget))%h(zTarget)</a>] |
|
1875
|
}else{ |
|
1876
|
@ wiki page [%h(zTarget)] |
|
1877
|
} |
|
1878
|
} |
|
1879
|
}else{ |
|
1880
|
if( g.perm.Hyperlink && g.anon.RdWiki ){ |
|
1881
|
@ wiki page [%z(href("%R/wiki?name=%t",zTarget))%h(zTarget)</a>] |
|
1882
|
}else{ |
|
1883
|
@ wiki page [%h(zTarget)] |
|
1884
|
} |
|
1885
|
} |
|
1886
|
@ added by |
|
1887
|
hyperlink_to_user(zUser,zDate," on"); |
|
1888
|
hyperlink_to_date(zDate,"."); |
|
1889
|
cnt++; |
|
1890
|
if( pDownloadName && blob_size(pDownloadName)==0 ){ |
|
1891
|
blob_append(pDownloadName, zFilename, -1); |
|
1892
|
} |
|
1893
|
tag_private_status(rid); |
|
1894
|
} |
|
1895
|
db_finalize(&q); |
|
1896
|
if( db_exists("SELECT 1 FROM tagxref WHERE rid=%d AND tagid=%d", |
|
1897
|
rid, TAG_CLUSTER) ){ |
|
1898
|
@ Cluster %z(href("%R/info/%S",zUuid))%S(zUuid)</a>. |
|
1899
|
cnt++; |
|
1900
|
} |
|
1901
|
if( cnt==0 ){ |
|
1902
|
@ Unrecognized artifact |
|
1903
|
if( pDownloadName && blob_size(pDownloadName)==0 ){ |
|
1904
|
blob_appendf(pDownloadName, "%S.txt", zUuid); |
|
1905
|
} |
|
1906
|
tag_private_status(rid); |
|
1907
|
} |
|
1908
|
return objType; |
|
1909
|
} |
|
1910
|
|
|
1911
|
/* |
|
1912
|
** SETTING: preferred-diff-type width=16 default=0 |
|
1913
|
** |
|
1914
|
** The preferred-diff-type setting determines the preferred diff format |
|
1915
|
** for web pages if the format is not otherwise specified, for example |
|
1916
|
** by a query parameter or cookie. Allowed values: |
|
1917
|
** |
|
1918
|
** 1 Unified diff |
|
1919
|
** 2 Side-by-side diff |
|
1920
|
** |
|
1921
|
** If this setting is omitted or has a value of 0 or less, then it |
|
1922
|
** is ignored. |
|
1923
|
*/ |
|
1924
|
/* |
|
1925
|
** Return the preferred diff type. |
|
1926
|
** |
|
1927
|
** 0 = No diff at all. |
|
1928
|
** 1 = unified diff |
|
1929
|
** 2 = side-by-side diff |
|
1930
|
** |
|
1931
|
** To determine the preferred diff type, the following values are |
|
1932
|
** consulted in the order shown. The first available source wins. |
|
1933
|
** |
|
1934
|
** * The "diff" query parameter |
|
1935
|
** * The "diff" field of the user display cookie |
|
1936
|
** * The "preferred-diff-type" setting |
|
1937
|
** * 1 for mobile and 2 for desktop, based on the UserAgent |
|
1938
|
*/ |
|
1939
|
int preferred_diff_type(void){ |
|
1940
|
int dflt; |
|
1941
|
int res; |
|
1942
|
int isBot; |
|
1943
|
static char zDflt[2] |
|
1944
|
/*static b/c cookie_link_parameter() does not copy it!*/; |
|
1945
|
if( robot_would_be_restricted("diff") ){ |
|
1946
|
dflt = 0; |
|
1947
|
isBot = 1; |
|
1948
|
}else{ |
|
1949
|
dflt = db_get_int("preferred-diff-type",-99); |
|
1950
|
if( dflt<=0 ) dflt = user_agent_is_likely_mobile() ? 1 : 2; |
|
1951
|
isBot = 0; |
|
1952
|
} |
|
1953
|
zDflt[0] = dflt + '0'; |
|
1954
|
zDflt[1] = 0; |
|
1955
|
cookie_link_parameter("diff","diff", zDflt); |
|
1956
|
res = atoi(PD_NoBot("diff",zDflt)); |
|
1957
|
if( isBot && res>0 && robot_restrict("diff") ){ |
|
1958
|
cgi_reply(); |
|
1959
|
fossil_exit(0); |
|
1960
|
} |
|
1961
|
return res; |
|
1962
|
} |
|
1963
|
|
|
1964
|
|
|
1965
|
/* |
|
1966
|
** WEBPAGE: fdiff |
|
1967
|
** URL: fdiff?v1=HASH&v2=HASH |
|
1968
|
** |
|
1969
|
** Two arguments, v1 and v2, identify the artifacts to be diffed. |
|
1970
|
** Show diff side by side unless sbs is 0. Generate plain text if |
|
1971
|
** "patch" is present, otherwise generate "pretty" HTML. |
|
1972
|
** |
|
1973
|
** Alternative URL: fdiff?from=filename1&to=filename2&ci=checkin |
|
1974
|
** |
|
1975
|
** If the "from" and "to" query parameters are both present, then they are |
|
1976
|
** the names of two files within the check-in "ci" that are diffed. If the |
|
1977
|
** "ci" parameter is omitted, then the most recent check-in ("tip") is |
|
1978
|
** used. |
|
1979
|
** |
|
1980
|
** Additional parameters: |
|
1981
|
** |
|
1982
|
** dc=N Show N lines of context around each diff |
|
1983
|
** patch Use the patch diff format |
|
1984
|
** regex=REGEX Only show differences that match REGEX |
|
1985
|
** sbs=BOOLEAN Turn side-by-side diffs on and off (default: on) |
|
1986
|
** verbose=BOOLEAN Show more detail when describing artifacts |
|
1987
|
** w=BOOLEAN Ignore whitespace |
|
1988
|
*/ |
|
1989
|
void diff_page(void){ |
|
1990
|
int v1, v2; |
|
1991
|
int isPatch = P("patch")!=0; |
|
1992
|
int diffType; /* 0: none, 1: unified, 2: side-by-side */ |
|
1993
|
char *zV1; |
|
1994
|
char *zV2; |
|
1995
|
const char *zRe; |
|
1996
|
ReCompiled *pRe = 0; |
|
1997
|
u32 objdescFlags = 0; |
|
1998
|
int verbose = PB("verbose"); |
|
1999
|
DiffConfig DCfg; |
|
2000
|
|
|
2001
|
login_check_credentials(); |
|
2002
|
if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
|
2003
|
if( robot_restrict("diff") ) return; |
|
2004
|
diff_config_init(&DCfg, 0); |
|
2005
|
diffType = preferred_diff_type(); |
|
2006
|
if( P("from") && P("to") ){ |
|
2007
|
v1 = artifact_from_ci_and_filename("from"); |
|
2008
|
v2 = artifact_from_ci_and_filename("to"); |
|
2009
|
if( v1==0 || v2==0 ) fossil_redirect_home(); |
|
2010
|
}else{ |
|
2011
|
Stmt q; |
|
2012
|
v1 = name_to_rid_www("v1"); |
|
2013
|
v2 = name_to_rid_www("v2"); |
|
2014
|
if( v1==0 || v2==0 ) fossil_redirect_home(); |
|
2015
|
|
|
2016
|
/* If the two file versions being compared both have the same |
|
2017
|
** filename, then offer an "Annotate" link that constructs an |
|
2018
|
** annotation between those version. */ |
|
2019
|
db_prepare(&q, |
|
2020
|
"SELECT (SELECT substr(uuid,1,20) FROM blob WHERE rid=a.mid)," |
|
2021
|
" (SELECT substr(uuid,1,20) FROM blob WHERE rid=b.mid)," |
|
2022
|
" (SELECT name FROM filename WHERE filename.fnid=a.fnid)" |
|
2023
|
" FROM mlink a, event ea, mlink b, event eb" |
|
2024
|
" WHERE a.fid=%d" |
|
2025
|
" AND b.fid=%d" |
|
2026
|
" AND a.fnid=b.fnid" |
|
2027
|
" AND a.fid!=a.pid" |
|
2028
|
" AND b.fid!=b.pid" |
|
2029
|
" AND ea.objid=a.mid" |
|
2030
|
" AND eb.objid=b.mid" |
|
2031
|
" ORDER BY ea.mtime ASC, eb.mtime ASC", |
|
2032
|
v1, v2 |
|
2033
|
); |
|
2034
|
if( db_step(&q)==SQLITE_ROW ){ |
|
2035
|
const char *zCkin = db_column_text(&q, 0); |
|
2036
|
const char *zOrig = db_column_text(&q, 1); |
|
2037
|
const char *zFN = db_column_text(&q, 2); |
|
2038
|
style_submenu_element("Annotate", |
|
2039
|
"%R/annotate?origin=%s&checkin=%s&filename=%T", |
|
2040
|
zOrig, zCkin, zFN); |
|
2041
|
} |
|
2042
|
db_finalize(&q); |
|
2043
|
} |
|
2044
|
zRe = P("regex"); |
|
2045
|
cgi_check_for_malice(); |
|
2046
|
if( zRe ) fossil_re_compile(&pRe, zRe, 0); |
|
2047
|
if( verbose ) objdescFlags |= OBJDESC_DETAIL; |
|
2048
|
if( isPatch ){ |
|
2049
|
Blob c1, c2, *pOut; |
|
2050
|
DiffConfig DCfg; |
|
2051
|
pOut = cgi_output_blob(); |
|
2052
|
cgi_set_content_type("text/plain"); |
|
2053
|
DCfg.diffFlags = DIFF_VERBOSE; |
|
2054
|
content_get(v1, &c1); |
|
2055
|
content_get(v2, &c2); |
|
2056
|
DCfg.pRe = pRe; |
|
2057
|
text_diff(&c1, &c2, pOut, &DCfg); |
|
2058
|
blob_reset(&c1); |
|
2059
|
blob_reset(&c2); |
|
2060
|
return; |
|
2061
|
} |
|
2062
|
|
|
2063
|
zV1 = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", v1); |
|
2064
|
zV2 = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", v2); |
|
2065
|
construct_diff_flags(diffType, &DCfg); |
|
2066
|
DCfg.diffFlags |= DIFF_HTML; |
|
2067
|
|
|
2068
|
style_set_current_feature("fdiff"); |
|
2069
|
style_header("Diff"); |
|
2070
|
style_submenu_checkbox("w", "Ignore Whitespace", 0, 0); |
|
2071
|
if( diffType==2 ){ |
|
2072
|
style_submenu_element("Unified Diff", "%R/fdiff?v1=%T&v2=%T&diff=1", |
|
2073
|
P("v1"), P("v2")); |
|
2074
|
}else{ |
|
2075
|
style_submenu_element("Side-by-side Diff", "%R/fdiff?v1=%T&v2=%T&diff=2", |
|
2076
|
P("v1"), P("v2")); |
|
2077
|
} |
|
2078
|
style_submenu_checkbox("verbose", "Verbose", 0, 0); |
|
2079
|
style_submenu_element("Patch", "%R/fdiff?v1=%T&v2=%T&patch", |
|
2080
|
P("v1"), P("v2")); |
|
2081
|
|
|
2082
|
if( P("smhdr")!=0 ){ |
|
2083
|
@ <h2>Differences From Artifact |
|
2084
|
@ %z(href("%R/artifact/%!S",zV1))[%S(zV1)]</a> To |
|
2085
|
@ %z(href("%R/artifact/%!S",zV2))[%S(zV2)]</a>.</h2> |
|
2086
|
}else{ |
|
2087
|
@ <h2>Differences From |
|
2088
|
@ Artifact %z(href("%R/artifact/%!S",zV1))[%S(zV1)]</a>:</h2> |
|
2089
|
object_description(v1, objdescFlags,0, 0); |
|
2090
|
@ <h2>To Artifact %z(href("%R/artifact/%!S",zV2))[%S(zV2)]</a>:</h2> |
|
2091
|
object_description(v2, objdescFlags,0, 0); |
|
2092
|
} |
|
2093
|
if( pRe ){ |
|
2094
|
@ <b>Only differences that match regular expression "%h(zRe)" |
|
2095
|
@ are shown.</b> |
|
2096
|
DCfg.pRe = pRe; |
|
2097
|
} |
|
2098
|
@ <hr> |
|
2099
|
append_diff(zV1, zV2, &DCfg); |
|
2100
|
append_diff_javascript(diffType); |
|
2101
|
style_finish_page(); |
|
2102
|
} |
|
2103
|
|
|
2104
|
/* |
|
2105
|
** WEBPAGE: raw |
|
2106
|
** URL: /raw/ARTIFACTID |
|
2107
|
** URL: /raw?ci=BRANCH&filename=NAME |
|
2108
|
** |
|
2109
|
** Additional query parameters: |
|
2110
|
** |
|
2111
|
** m=MIMETYPE The mimetype is MIMETYPE |
|
2112
|
** at=FILENAME Content-disposition; attachment; filename=FILENAME; |
|
2113
|
** |
|
2114
|
** Return the uninterpreted content of an artifact. Used primarily |
|
2115
|
** to view artifacts that are images. |
|
2116
|
*/ |
|
2117
|
void rawartifact_page(void){ |
|
2118
|
int rid = 0; |
|
2119
|
char *zUuid; |
|
2120
|
|
|
2121
|
(void)P("at")/*for cgi_check_for_malice()*/; |
|
2122
|
(void)P("m"); |
|
2123
|
if( P("ci") ){ |
|
2124
|
rid = artifact_from_ci_and_filename(0); |
|
2125
|
} |
|
2126
|
if( rid==0 ){ |
|
2127
|
rid = name_to_rid_www("name"); |
|
2128
|
} |
|
2129
|
login_check_credentials(); |
|
2130
|
if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
|
2131
|
cgi_check_for_malice(); |
|
2132
|
if( rid==0 ) fossil_redirect_home(); |
|
2133
|
zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); |
|
2134
|
etag_check(ETAG_HASH, zUuid); |
|
2135
|
if( fossil_strcmp(P("name"), zUuid)==0 && login_is_nobody() ){ |
|
2136
|
g.isConst = 1; |
|
2137
|
} |
|
2138
|
free(zUuid); |
|
2139
|
deliver_artifact(rid, P("m")); |
|
2140
|
} |
|
2141
|
|
|
2142
|
|
|
2143
|
/* |
|
2144
|
** WEBPAGE: secureraw |
|
2145
|
** URL: /secureraw/HASH?m=TYPE |
|
2146
|
** |
|
2147
|
** Return the uninterpreted content of an artifact. This is similar |
|
2148
|
** to /raw except in this case the only way to specify the artifact |
|
2149
|
** is by the full-length SHA1 or SHA3 hash. Abbreviations are not |
|
2150
|
** accepted. |
|
2151
|
*/ |
|
2152
|
void secure_rawartifact_page(void){ |
|
2153
|
int rid = 0; |
|
2154
|
const char *zName = PD("name", ""); |
|
2155
|
|
|
2156
|
(void)P("at")/*for cgi_check_for_malice()*/; |
|
2157
|
(void)P("m"); |
|
2158
|
cgi_check_for_malice(); |
|
2159
|
login_check_credentials(); |
|
2160
|
if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
|
2161
|
rid = db_int(0, "SELECT rid FROM blob WHERE uuid=%Q", zName); |
|
2162
|
if( rid==0 ){ |
|
2163
|
cgi_set_status(404, "Not Found"); |
|
2164
|
@ Unknown artifact: "%h(zName)" |
|
2165
|
return; |
|
2166
|
} |
|
2167
|
g.isConst = 1; |
|
2168
|
deliver_artifact(rid, P("m")); |
|
2169
|
} |
|
2170
|
|
|
2171
|
|
|
2172
|
/* |
|
2173
|
** WEBPAGE: jchunk hidden |
|
2174
|
** URL: /jchunk/HASH?from=N&to=M |
|
2175
|
** |
|
2176
|
** Return lines of text from a file as a JSON array - one entry in the |
|
2177
|
** array for each line of text. |
|
2178
|
** |
|
2179
|
** The HASH is normally a sha1 or sha3 hash that identifies an artifact |
|
2180
|
** in the BLOB table of the database. However, if HASH starts with an "x" |
|
2181
|
** and is followed by valid hexadecimal, and if we are running in a |
|
2182
|
** "fossil ui" situation (locally and with privilege), then decode the hex |
|
2183
|
** into a filename and read the file content from that name. |
|
2184
|
** |
|
2185
|
** **Warning:** This is an internal-use-only interface that is subject to |
|
2186
|
** change at any moment. External application should not use this interface |
|
2187
|
** since the application will break when this interface changes, and this |
|
2188
|
** interface will undoubtedly change. |
|
2189
|
** |
|
2190
|
** This page is intended to be used in an XHR from javascript on a |
|
2191
|
** diff page, to return unseen context to fill in additional context |
|
2192
|
** when the user clicks on the appropriate button. The response is |
|
2193
|
** always in JSON form and errors are reported as documented for |
|
2194
|
** ajax_route_error(). |
|
2195
|
*/ |
|
2196
|
void jchunk_page(void){ |
|
2197
|
int rid = 0; |
|
2198
|
const char *zName = PD("name", ""); |
|
2199
|
int nName = (int)(strlen(zName)&0x7fffffff); |
|
2200
|
int iFrom = atoi(PD("from","0")); |
|
2201
|
int iTo = atoi(PD("to","0")); |
|
2202
|
int ln; |
|
2203
|
int go = 1; |
|
2204
|
const char *zSep; |
|
2205
|
Blob content; |
|
2206
|
Blob line; |
|
2207
|
Blob *pOut; |
|
2208
|
|
|
2209
|
if(0){ |
|
2210
|
ajax_route_error(400, "Just testing client-side error handling."); |
|
2211
|
return; |
|
2212
|
} |
|
2213
|
|
|
2214
|
login_check_credentials(); |
|
2215
|
cgi_check_for_malice(); |
|
2216
|
if( !g.perm.Read ){ |
|
2217
|
ajax_route_error(403, "Access requires Read permissions."); |
|
2218
|
return; |
|
2219
|
} |
|
2220
|
if( iFrom<1 || iTo<iFrom ){ |
|
2221
|
ajax_route_error(500, "Invalid line range from=%d, to=%d.", |
|
2222
|
iFrom, iTo); |
|
2223
|
return; |
|
2224
|
} |
|
2225
|
if( zName[0]=='x' |
|
2226
|
&& ((nName-1)&1)==0 |
|
2227
|
&& validate16(&zName[1],nName-1) |
|
2228
|
&& g.perm.Admin |
|
2229
|
&& cgi_is_loopback(g.zIpAddr) |
|
2230
|
&& db_open_local(0) |
|
2231
|
){ |
|
2232
|
/* Treat the HASH as a hex-encoded filename */ |
|
2233
|
int n = (nName-1)/2; |
|
2234
|
char *zFN = fossil_malloc(n+1); |
|
2235
|
decode16((const u8*)&zName[1], (u8*)zFN, nName-1); |
|
2236
|
zFN[n] = 0; |
|
2237
|
if( file_size(zFN, ExtFILE)<0 ){ |
|
2238
|
blob_zero(&content); |
|
2239
|
}else{ |
|
2240
|
blob_read_from_file(&content, zFN, ExtFILE); |
|
2241
|
} |
|
2242
|
fossil_free(zFN); |
|
2243
|
}else{ |
|
2244
|
/* Treat the HASH as an artifact hash matching BLOB.UUID */ |
|
2245
|
#if 1 |
|
2246
|
/* Re-enable this block once this code is integrated somewhere into |
|
2247
|
the UI. */ |
|
2248
|
rid = db_int(0, "SELECT rid FROM blob WHERE uuid=%Q", zName); |
|
2249
|
if( rid==0 ){ |
|
2250
|
ajax_route_error(404, "Unknown artifact: %h", zName); |
|
2251
|
return; |
|
2252
|
} |
|
2253
|
#else |
|
2254
|
/* This impl is only to simplify "manual" testing via the JS |
|
2255
|
console. */ |
|
2256
|
rid = symbolic_name_to_rid(zName, "*"); |
|
2257
|
if( rid==0 ){ |
|
2258
|
ajax_route_error(404, "Unknown artifact: %h", zName); |
|
2259
|
return; |
|
2260
|
}else if( rid<0 ){ |
|
2261
|
ajax_route_error(418, "Ambiguous artifact name: %h", zName); |
|
2262
|
return; |
|
2263
|
} |
|
2264
|
#endif |
|
2265
|
content_get(rid, &content); |
|
2266
|
} |
|
2267
|
g.isConst = 1; |
|
2268
|
cgi_set_content_type("application/json"); |
|
2269
|
ln = 0; |
|
2270
|
while( go && ln<iFrom ){ |
|
2271
|
go = blob_line(&content, &line); |
|
2272
|
ln++; |
|
2273
|
} |
|
2274
|
pOut = cgi_output_blob(); |
|
2275
|
blob_append(pOut, "[\n", 2); |
|
2276
|
zSep = 0; |
|
2277
|
while( go && ln<=iTo ){ |
|
2278
|
if( zSep ) blob_append(pOut, zSep, 2); |
|
2279
|
blob_trim(&line); |
|
2280
|
blob_append_json_literal(pOut, blob_buffer(&line), blob_size(&line)); |
|
2281
|
zSep = ",\n"; |
|
2282
|
go = blob_line(&content, &line); |
|
2283
|
ln++; |
|
2284
|
} |
|
2285
|
blob_appendf(pOut,"]\n"); |
|
2286
|
blob_reset(&content); |
|
2287
|
} |
|
2288
|
|
|
2289
|
/* |
|
2290
|
** Generate a verbatim artifact as the result of an HTTP request. |
|
2291
|
** If zMime is not NULL, use it as the mimetype. If zMime is |
|
2292
|
** NULL, guess at the mimetype based on the filename |
|
2293
|
** associated with the artifact. |
|
2294
|
*/ |
|
2295
|
void deliver_artifact(int rid, const char *zMime){ |
|
2296
|
Blob content; |
|
2297
|
const char *zAttachName = P("at"); |
|
2298
|
if( zMime==0 ){ |
|
2299
|
char *zFN = (char*)zAttachName; |
|
2300
|
if( zFN==0 ){ |
|
2301
|
zFN = db_text(0, "SELECT filename.name FROM mlink, filename" |
|
2302
|
" WHERE mlink.fid=%d" |
|
2303
|
" AND filename.fnid=mlink.fnid", rid); |
|
2304
|
} |
|
2305
|
if( zFN==0 ){ |
|
2306
|
/* Look also at the attachment table */ |
|
2307
|
zFN = db_text(0, "SELECT attachment.filename FROM attachment, blob" |
|
2308
|
" WHERE blob.rid=%d" |
|
2309
|
" AND attachment.src=blob.uuid", rid); |
|
2310
|
} |
|
2311
|
if( zFN ){ |
|
2312
|
zMime = mimetype_from_name(zFN); |
|
2313
|
} |
|
2314
|
if( zMime==0 ){ |
|
2315
|
zMime = "application/x-fossil-artifact"; |
|
2316
|
} |
|
2317
|
} |
|
2318
|
content_get(rid, &content); |
|
2319
|
fossil_free(style_csp(1)); |
|
2320
|
cgi_set_content_type(zMime); |
|
2321
|
if( zAttachName ){ |
|
2322
|
cgi_content_disposition_filename(zAttachName); |
|
2323
|
} |
|
2324
|
cgi_set_content(&content); |
|
2325
|
} |
|
2326
|
|
|
2327
|
/* |
|
2328
|
** Render a hex dump of a file. |
|
2329
|
*/ |
|
2330
|
static void hexdump(Blob *pBlob){ |
|
2331
|
const unsigned char *x; |
|
2332
|
int n, i, j, k; |
|
2333
|
char zLine[100]; |
|
2334
|
static const char zHex[] = "0123456789abcdef"; |
|
2335
|
|
|
2336
|
x = (const unsigned char*)blob_buffer(pBlob); |
|
2337
|
n = blob_size(pBlob); |
|
2338
|
for(i=0; i<n; i+=16){ |
|
2339
|
j = 0; |
|
2340
|
zLine[0] = zHex[(i>>24)&0xf]; |
|
2341
|
zLine[1] = zHex[(i>>16)&0xf]; |
|
2342
|
zLine[2] = zHex[(i>>8)&0xf]; |
|
2343
|
zLine[3] = zHex[i&0xf]; |
|
2344
|
zLine[4] = ':'; |
|
2345
|
sqlite3_snprintf(sizeof(zLine), zLine, "%04x: ", i); |
|
2346
|
for(j=0; j<16; j++){ |
|
2347
|
k = 5+j*3; |
|
2348
|
zLine[k] = ' '; |
|
2349
|
if( i+j<n ){ |
|
2350
|
unsigned char c = x[i+j]; |
|
2351
|
zLine[k+1] = zHex[c>>4]; |
|
2352
|
zLine[k+2] = zHex[c&0xf]; |
|
2353
|
}else{ |
|
2354
|
zLine[k+1] = ' '; |
|
2355
|
zLine[k+2] = ' '; |
|
2356
|
} |
|
2357
|
} |
|
2358
|
zLine[53] = ' '; |
|
2359
|
zLine[54] = ' '; |
|
2360
|
cgi_append_content(zLine, 55); |
|
2361
|
for(j=k=0; j<16; j++){ |
|
2362
|
if( i+j<n ){ |
|
2363
|
unsigned char c = x[i+j]; |
|
2364
|
if( c>'>' && c<=0x7e ){ |
|
2365
|
zLine[k++] = c; |
|
2366
|
}else if( c=='>' ){ |
|
2367
|
zLine[k++] = '&'; |
|
2368
|
zLine[k++] = 'g'; |
|
2369
|
zLine[k++] = 't'; |
|
2370
|
zLine[k++] = ';'; |
|
2371
|
}else if( c=='<' ){ |
|
2372
|
zLine[k++] = '&'; |
|
2373
|
zLine[k++] = 'l'; |
|
2374
|
zLine[k++] = 't'; |
|
2375
|
zLine[k++] = ';'; |
|
2376
|
}else if( c=='&' ){ |
|
2377
|
zLine[k++] = '&'; |
|
2378
|
zLine[k++] = 'a'; |
|
2379
|
zLine[k++] = 'm'; |
|
2380
|
zLine[k++] = 'p'; |
|
2381
|
zLine[k++] = ';'; |
|
2382
|
}else if( c>=' ' ){ |
|
2383
|
zLine[k++] = c; |
|
2384
|
}else{ |
|
2385
|
zLine[k++] = '.'; |
|
2386
|
} |
|
2387
|
}else{ |
|
2388
|
break; |
|
2389
|
} |
|
2390
|
} |
|
2391
|
zLine[k++] = '\n'; |
|
2392
|
cgi_append_content(zLine, k); |
|
2393
|
} |
|
2394
|
} |
|
2395
|
|
|
2396
|
/* |
|
2397
|
** WEBPAGE: hexdump |
|
2398
|
** URL: /hexdump?name=ARTIFACTID |
|
2399
|
** |
|
2400
|
** Show the complete content of a file identified by ARTIFACTID |
|
2401
|
** as preformatted text. |
|
2402
|
** |
|
2403
|
** Other parameters: |
|
2404
|
** |
|
2405
|
** verbose Show more detail when describing the object |
|
2406
|
*/ |
|
2407
|
void hexdump_page(void){ |
|
2408
|
int rid; |
|
2409
|
Blob content; |
|
2410
|
Blob downloadName; |
|
2411
|
char *zUuid; |
|
2412
|
u32 objdescFlags = 0; |
|
2413
|
|
|
2414
|
rid = name_to_rid_www("name"); |
|
2415
|
login_check_credentials(); |
|
2416
|
if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
|
2417
|
if( rid==0 ) fossil_redirect_home(); |
|
2418
|
cgi_check_for_malice(); |
|
2419
|
if( g.perm.Admin ){ |
|
2420
|
const char *zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid); |
|
2421
|
if( db_exists("SELECT 1 FROM shun WHERE uuid=%Q", zUuid) ){ |
|
2422
|
style_submenu_element("Unshun", "%R/shun?accept=%s&sub=1#delshun", zUuid); |
|
2423
|
}else{ |
|
2424
|
style_submenu_element("Shun", "%R/shun?shun=%s#addshun", zUuid); |
|
2425
|
} |
|
2426
|
} |
|
2427
|
style_header("Hex Artifact Content"); |
|
2428
|
zUuid = db_text("?","SELECT uuid FROM blob WHERE rid=%d", rid); |
|
2429
|
etag_check(ETAG_HASH, zUuid); |
|
2430
|
@ <h2>Artifact |
|
2431
|
style_copy_button(1, "hash-ar", 0, 2, "%s", zUuid); |
|
2432
|
if( g.perm.Setup ){ |
|
2433
|
@ (%d(rid)):</h2> |
|
2434
|
}else{ |
|
2435
|
@ :</h2> |
|
2436
|
} |
|
2437
|
blob_zero(&downloadName); |
|
2438
|
if( P("verbose")!=0 ) objdescFlags |= OBJDESC_DETAIL; |
|
2439
|
object_description(rid, objdescFlags, 0, &downloadName); |
|
2440
|
style_submenu_element("Download", "%R/raw/%s?at=%T", |
|
2441
|
zUuid, file_tail(blob_str(&downloadName))); |
|
2442
|
@ <hr> |
|
2443
|
content_get(rid, &content); |
|
2444
|
if( blob_size(&content)>100000 ){ |
|
2445
|
/* Prevent robots from running hexdump on megabyte-sized source files |
|
2446
|
** and there by eating up lots of CPU time and bandwidth. There is |
|
2447
|
** no good reason for a robot to need a hexdump. */ |
|
2448
|
@ <p>A hex dump of this file is not available because it is too large. |
|
2449
|
@ Please download the raw binary file and generate a hex dump yourself.</p> |
|
2450
|
}else{ |
|
2451
|
@ <blockquote><pre> |
|
2452
|
hexdump(&content); |
|
2453
|
@ </pre></blockquote> |
|
2454
|
} |
|
2455
|
style_finish_page(); |
|
2456
|
} |
|
2457
|
|
|
2458
|
/* |
|
2459
|
** Look for "ci" and "filename" query parameters. If found, try to |
|
2460
|
** use them to extract the record ID of an artifact for the file. |
|
2461
|
** |
|
2462
|
** Also look for "fn" and "name" as an aliases for "filename". If any |
|
2463
|
** "filename" or "fn" or "name" are present but "ci" is missing, then |
|
2464
|
** use "tip" as the default value for "ci". |
|
2465
|
** |
|
2466
|
** If zNameParam is not NULL, then use that parameter as the filename |
|
2467
|
** rather than "fn" or "filename" or "name". the zNameParam is used |
|
2468
|
** for the from= and to= query parameters of /fdiff. |
|
2469
|
*/ |
|
2470
|
int artifact_from_ci_and_filename(const char *zNameParam){ |
|
2471
|
const char *zFilename; |
|
2472
|
const char *zCI; |
|
2473
|
int cirid; |
|
2474
|
Manifest *pManifest; |
|
2475
|
ManifestFile *pFile; |
|
2476
|
int rid = 0; |
|
2477
|
|
|
2478
|
if( zNameParam ){ |
|
2479
|
zFilename = P(zNameParam); |
|
2480
|
}else{ |
|
2481
|
zFilename = P("filename"); |
|
2482
|
if( zFilename==0 ){ |
|
2483
|
zFilename = P("fn"); |
|
2484
|
} |
|
2485
|
if( zFilename==0 ){ |
|
2486
|
zFilename = P("name"); |
|
2487
|
} |
|
2488
|
} |
|
2489
|
if( zFilename==0 ) return 0; |
|
2490
|
|
|
2491
|
zCI = PD("ci", "tip"); |
|
2492
|
cirid = name_to_typed_rid(zCI, "ci"); |
|
2493
|
if( cirid<=0 ) return 0; |
|
2494
|
pManifest = manifest_get(cirid, CFTYPE_MANIFEST, 0); |
|
2495
|
if( pManifest==0 ) return 0; |
|
2496
|
manifest_file_rewind(pManifest); |
|
2497
|
while( (pFile = manifest_file_next(pManifest,0))!=0 ){ |
|
2498
|
if( fossil_strcmp(zFilename, pFile->zName)==0 ){ |
|
2499
|
rid = db_int(0, "SELECT rid FROM blob WHERE uuid=%Q", pFile->zUuid); |
|
2500
|
break; |
|
2501
|
} |
|
2502
|
} |
|
2503
|
manifest_destroy(pManifest); |
|
2504
|
return rid; |
|
2505
|
} |
|
2506
|
|
|
2507
|
/* |
|
2508
|
** The "z" argument is a string that contains the text of a source |
|
2509
|
** code file and nZ is its length in bytes. This routine appends that |
|
2510
|
** text to the HTTP reply with line numbering. |
|
2511
|
** |
|
2512
|
** zName is the content's file name, if any (it may be NULL). If that |
|
2513
|
** name contains a '.' then the part after the final '.' is used as |
|
2514
|
** the X part of a "language-X" CSS class on the generated CODE block. |
|
2515
|
** |
|
2516
|
** zLn is the ?ln= parameter for the HTTP query. If there is an argument, |
|
2517
|
** then highlight that line number and scroll to it once the page loads. |
|
2518
|
** If there are two line numbers, highlight the range of lines. |
|
2519
|
** Multiple ranges can be highlighed by adding additional line numbers |
|
2520
|
** separated by a non-digit character (also not one of [-,.]). |
|
2521
|
** |
|
2522
|
** If includeJS is true then the JS code associated with line |
|
2523
|
** numbering is also emitted, else it is not. If this routine is |
|
2524
|
** called multiple times in a single app run, the JS is emitted only |
|
2525
|
** once. Note that when using this routine to emit Ajax responses, the |
|
2526
|
** JS should be not be included, as it will not get imported properly |
|
2527
|
** into the response's rendering. |
|
2528
|
*/ |
|
2529
|
void output_text_with_line_numbers( |
|
2530
|
const char *z, |
|
2531
|
int nZ, |
|
2532
|
const char *zName, |
|
2533
|
const char *zLn, |
|
2534
|
int includeJS |
|
2535
|
){ |
|
2536
|
int iStart, iEnd; /* Start and end of region to highlight */ |
|
2537
|
int n = 0; /* Current line number */ |
|
2538
|
int i = 0; /* Loop index */ |
|
2539
|
int iTop = 0; /* Scroll so that this line is on top of screen. */ |
|
2540
|
int nLine = 0; /* content line count */ |
|
2541
|
int nSpans = 0; /* number of distinct zLn spans */ |
|
2542
|
const char *zExt = file_extension(zName); |
|
2543
|
static int emittedJS = 0; /* emitted shared JS yet? */ |
|
2544
|
Stmt q; |
|
2545
|
|
|
2546
|
iStart = iEnd = atoi(zLn); |
|
2547
|
db_multi_exec( |
|
2548
|
"CREATE TEMP TABLE lnos(iStart INTEGER PRIMARY KEY, iEnd INTEGER)"); |
|
2549
|
if( iStart>0 ){ |
|
2550
|
do{ |
|
2551
|
while( fossil_isdigit(zLn[i]) ) i++; |
|
2552
|
if( zLn[i]==',' || zLn[i]=='-' || zLn[i]=='.' ){ |
|
2553
|
i++; |
|
2554
|
while( zLn[i]=='.' ){ i++; } |
|
2555
|
iEnd = atoi(&zLn[i]); |
|
2556
|
while( fossil_isdigit(zLn[i]) ) i++; |
|
2557
|
} |
|
2558
|
while( fossil_isdigit(zLn[i]) ) i++; |
|
2559
|
if( iEnd<iStart ) iEnd = iStart; |
|
2560
|
db_multi_exec( |
|
2561
|
"INSERT OR REPLACE INTO lnos VALUES(%d,%d)", iStart, iEnd |
|
2562
|
); |
|
2563
|
++nSpans; |
|
2564
|
iStart = iEnd = atoi(&zLn[i++]); |
|
2565
|
}while( zLn[i] && iStart && iEnd ); |
|
2566
|
} |
|
2567
|
/*cgi_printf("<!-- ln span count=%d -->", nSpans);*/ |
|
2568
|
cgi_append_content("<table class='numbered-lines'><tbody>" |
|
2569
|
"<tr><td class='line-numbers'><pre>", -1); |
|
2570
|
iStart = iEnd = 0; |
|
2571
|
count_lines(z, nZ, &nLine); |
|
2572
|
for( n=1 ; n<=nLine; ++n ){ |
|
2573
|
const char * zAttr = ""; |
|
2574
|
const char * zId = ""; |
|
2575
|
if(nSpans>0 && iEnd==0){/*Grab the next range of zLn marking*/ |
|
2576
|
db_prepare(&q, "SELECT iStart, iEnd FROM lnos " |
|
2577
|
"WHERE iStart >= %d ORDER BY iStart", n); |
|
2578
|
if( db_step(&q)==SQLITE_ROW ){ |
|
2579
|
iStart = db_column_int(&q, 0); |
|
2580
|
iEnd = db_column_int(&q, 1); |
|
2581
|
if(!iTop){ |
|
2582
|
iTop = iStart - 15 + (iEnd-iStart)/4; |
|
2583
|
if( iTop>iStart - 2 ) iTop = iStart-2; |
|
2584
|
} |
|
2585
|
}else{ |
|
2586
|
/* Note that overlapping multi-spans, e.g. 10-15+12-20, |
|
2587
|
can cause us to miss a row. */ |
|
2588
|
iStart = iEnd = 0; |
|
2589
|
} |
|
2590
|
db_finalize(&q); |
|
2591
|
--nSpans; |
|
2592
|
/*cgi_printf("<!-- iStart=%d, iEnd=%d -->", iStart, iEnd);*/ |
|
2593
|
} |
|
2594
|
if(n==iTop) { |
|
2595
|
zId = " id='scrollToMe'"; |
|
2596
|
} |
|
2597
|
if(n==iStart){/*Figure out which CSS class(es) this line needs...*/ |
|
2598
|
if(n==iEnd){ |
|
2599
|
zAttr = " class='selected-line start end'"; |
|
2600
|
iEnd = 0; |
|
2601
|
}else{ |
|
2602
|
zAttr = " class='selected-line start'"; |
|
2603
|
} |
|
2604
|
iStart = 0; |
|
2605
|
}else if(n==iEnd){ |
|
2606
|
zAttr = " class='selected-line end'"; |
|
2607
|
iEnd = 0; |
|
2608
|
}else if( n>iStart && n<iEnd ){ |
|
2609
|
zAttr = " class='selected-line'"; |
|
2610
|
} |
|
2611
|
cgi_printf("<span%s%s>%6d</span>\n", zId, zAttr, n) |
|
2612
|
/* ^^^ explicit \n is necessary for text-mode browsers. */; |
|
2613
|
} |
|
2614
|
cgi_append_content("</pre></td><td class='file-content'><pre>",-1); |
|
2615
|
if(zExt && *zExt){ |
|
2616
|
cgi_printf("<code class='language-%h'>",zExt); |
|
2617
|
}else{ |
|
2618
|
cgi_append_content("<code>", -1); |
|
2619
|
} |
|
2620
|
cgi_printf("%z", htmlize(z, nZ)); |
|
2621
|
CX("</code></pre></td></tr></tbody></table>\n"); |
|
2622
|
if(includeJS && !emittedJS){ |
|
2623
|
emittedJS = 1; |
|
2624
|
if( db_int(0, "SELECT EXISTS(SELECT 1 FROM lnos)") ){ |
|
2625
|
builtin_request_js("scroll.js"); |
|
2626
|
} |
|
2627
|
builtin_fossil_js_bundle_or("numbered-lines", NULL); |
|
2628
|
} |
|
2629
|
} |
|
2630
|
|
|
2631
|
/* |
|
2632
|
** COMMAND: test-line-numbers |
|
2633
|
** |
|
2634
|
** Usage: %fossil test-line-numbers FILE ?LN-SPEC? |
|
2635
|
** |
|
2636
|
*/ |
|
2637
|
void cmd_test_line_numbers(void){ |
|
2638
|
Blob content = empty_blob; |
|
2639
|
const char * zLn = ""; |
|
2640
|
const char * zFilename = 0; |
|
2641
|
|
|
2642
|
if(g.argc < 3){ |
|
2643
|
usage("FILE"); |
|
2644
|
}else if(g.argc>3){ |
|
2645
|
zLn = g.argv[3]; |
|
2646
|
} |
|
2647
|
db_find_and_open_repository(0,0); |
|
2648
|
zFilename = g.argv[2]; |
|
2649
|
fossil_print("%s %s\n", zFilename, zLn); |
|
2650
|
|
|
2651
|
blob_read_from_file(&content, zFilename, ExtFILE); |
|
2652
|
output_text_with_line_numbers(blob_str(&content), blob_size(&content), |
|
2653
|
zFilename, zLn, 0); |
|
2654
|
blob_reset(&content); |
|
2655
|
fossil_print("%b\n", cgi_output_blob()); |
|
2656
|
} |
|
2657
|
|
|
2658
|
/* |
|
2659
|
** WEBPAGE: artifact |
|
2660
|
** WEBPAGE: file |
|
2661
|
** WEBPAGE: whatis |
|
2662
|
** WEBPAGE: docfile |
|
2663
|
** |
|
2664
|
** Typical usage: |
|
2665
|
** |
|
2666
|
** /artifact/HASH |
|
2667
|
** /whatis/HASH |
|
2668
|
** /file/NAME |
|
2669
|
** /docfile/NAME |
|
2670
|
** |
|
2671
|
** Additional query parameters: |
|
2672
|
** |
|
2673
|
** ln - show line numbers |
|
2674
|
** ln=N - highlight line number N |
|
2675
|
** ln=M-N - highlight lines M through N inclusive |
|
2676
|
** ln=M-N+Y-Z - highlight lines M through N and Y through Z (inclusive) |
|
2677
|
** verbose - show more detail in the description |
|
2678
|
** brief - show just the document, not the metadata. The |
|
2679
|
** /docfile page is an alias for /file?brief |
|
2680
|
** download - redirect to the download (artifact page only) |
|
2681
|
** name=NAME - filename or hash as a query parameter |
|
2682
|
** filename=NAME - alternative spelling for "name=" |
|
2683
|
** fn=NAME - alternative spelling for "name=" |
|
2684
|
** ci=VERSION - The specific check-in to use with "name=" to |
|
2685
|
** identify the file. |
|
2686
|
** txt - Force display of unformatted source text |
|
2687
|
** hash - Output only the hash of the artifact |
|
2688
|
** |
|
2689
|
** The /artifact page show the complete content of a file |
|
2690
|
** identified by HASH. The /whatis page shows only a description |
|
2691
|
** of how the artifact is used. The /file page shows the most recent |
|
2692
|
** version of the file or directory called NAME, or a list of the |
|
2693
|
** top-level directory if NAME is omitted. |
|
2694
|
** |
|
2695
|
** For /artifact and /whatis, the name= query parameter can refer to |
|
2696
|
** either the name of a file, or an artifact hash. If the ci= query |
|
2697
|
** parameter is also present, then name= must refer to a file name. |
|
2698
|
** If ci= is omitted, then the hash interpretation is preferred but |
|
2699
|
** if name= cannot be understood as a hash, a default "tip" value is |
|
2700
|
** used for ci=. |
|
2701
|
** |
|
2702
|
** For /file, name= can only be interpreted as a filename. As before, |
|
2703
|
** a default value of "tip" is used for ci= if ci= is omitted. |
|
2704
|
*/ |
|
2705
|
void artifact_page(void){ |
|
2706
|
int rid = 0; |
|
2707
|
Blob content; |
|
2708
|
const char *zMime; |
|
2709
|
Blob downloadName; |
|
2710
|
Blob uuid; |
|
2711
|
int renderAsWiki = 0; |
|
2712
|
int renderAsHtml = 0; |
|
2713
|
int renderAsSvg = 0; |
|
2714
|
int objType; |
|
2715
|
int asText; |
|
2716
|
const char *zUuid = 0; |
|
2717
|
u32 objdescFlags = OBJDESC_BASE; |
|
2718
|
int descOnly = fossil_strcmp(g.zPath,"whatis")==0; |
|
2719
|
int hashOnly = P("hash")!=0; |
|
2720
|
int docOnly = P("brief")!=0; |
|
2721
|
int isFile = fossil_strcmp(g.zPath,"file")==0; |
|
2722
|
const char *zLn = P("ln"); |
|
2723
|
const char *zName = P("name"); |
|
2724
|
const char *zCI = P("ci"); |
|
2725
|
HQuery url; |
|
2726
|
char *zCIUuid = 0; |
|
2727
|
int isSymbolicCI = 0; /* ci= exists and is a symbolic name, not a hash */ |
|
2728
|
int isBranchCI = 0; /* ci= refers to a branch name */ |
|
2729
|
char *zHeader = 0; |
|
2730
|
|
|
2731
|
login_check_credentials(); |
|
2732
|
if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
|
2733
|
cgi_check_for_malice(); |
|
2734
|
style_set_current_feature("artifact"); |
|
2735
|
if( fossil_strcmp(g.zPath, "docfile")==0 ){ |
|
2736
|
isFile = 1; |
|
2737
|
docOnly = 1; |
|
2738
|
} |
|
2739
|
|
|
2740
|
/* Capture and normalize the name= and ci= query parameters */ |
|
2741
|
if( zName==0 ){ |
|
2742
|
zName = P("filename"); |
|
2743
|
if( zName==0 ){ |
|
2744
|
zName = P("fn"); |
|
2745
|
} |
|
2746
|
} |
|
2747
|
if( zCI && strlen(zCI)==0 ){ zCI = 0; } |
|
2748
|
if( zCI |
|
2749
|
&& name_to_uuid2(zCI, "ci", &zCIUuid) |
|
2750
|
&& sqlite3_strnicmp(zCIUuid, zCI, strlen(zCI))!=0 |
|
2751
|
){ |
|
2752
|
isSymbolicCI = 1; |
|
2753
|
isBranchCI = branch_includes_uuid(zCI, zCIUuid); |
|
2754
|
} |
|
2755
|
|
|
2756
|
/* The name= query parameter (or at least one of its alternative |
|
2757
|
** spellings) is required. Except for /file, show a top-level |
|
2758
|
** directory listing if name= is omitted. |
|
2759
|
*/ |
|
2760
|
if( zName==0 ){ |
|
2761
|
if( isFile ){ |
|
2762
|
if( P("ci")==0 ) cgi_set_query_parameter("ci","tip"); |
|
2763
|
page_tree(); |
|
2764
|
return; |
|
2765
|
} |
|
2766
|
style_header("Missing name= query parameter"); |
|
2767
|
@ The name= query parameter is missing |
|
2768
|
style_finish_page(); |
|
2769
|
return; |
|
2770
|
} |
|
2771
|
|
|
2772
|
url_initialize(&url, g.zPath); |
|
2773
|
url_add_parameter(&url, "name", zName); |
|
2774
|
url_add_parameter(&url, "ci", zCI); /* no-op if zCI is NULL */ |
|
2775
|
|
|
2776
|
if( zCI==0 && !isFile ){ |
|
2777
|
/* If there is no ci= query parameter, then prefer to interpret |
|
2778
|
** name= as a hash for /artifact and /whatis. But not for /file. |
|
2779
|
** For /file, a name= without a ci= will prefer to use the default |
|
2780
|
** "tip" value for ci=. */ |
|
2781
|
rid = name_to_rid(zName); |
|
2782
|
} |
|
2783
|
if( rid==0 ){ |
|
2784
|
rid = artifact_from_ci_and_filename(0); |
|
2785
|
} |
|
2786
|
|
|
2787
|
if( rid==0 ){ /* Artifact not found */ |
|
2788
|
if( isFile ){ |
|
2789
|
Stmt q; |
|
2790
|
/* For /file, also check to see if name= refers to a directory, |
|
2791
|
** and if so, do a listing for that directory */ |
|
2792
|
int nName = (int)strlen(zName); |
|
2793
|
if( nName && zName[nName-1]=='/' ) nName--; |
|
2794
|
if( db_exists( |
|
2795
|
"SELECT 1 FROM filename" |
|
2796
|
" WHERE name GLOB '%.*q/*' AND substr(name,1,%d)=='%.*q/';", |
|
2797
|
nName, zName, nName+1, nName, zName |
|
2798
|
) ){ |
|
2799
|
if( P("ci")==0 ) cgi_set_query_parameter("ci","tip"); |
|
2800
|
page_tree(); |
|
2801
|
return; |
|
2802
|
} |
|
2803
|
/* No directory found, look for an historic version of the file |
|
2804
|
** that was subsequently deleted. */ |
|
2805
|
db_prepare(&q, |
|
2806
|
"SELECT fid, uuid FROM mlink, filename, event, blob" |
|
2807
|
" WHERE filename.name=%Q" |
|
2808
|
" AND mlink.fnid=filename.fnid AND mlink.fid>0" |
|
2809
|
" AND event.objid=mlink.mid" |
|
2810
|
" AND blob.rid=mlink.mid" |
|
2811
|
" ORDER BY event.mtime DESC", |
|
2812
|
zName |
|
2813
|
); |
|
2814
|
if( db_step(&q)==SQLITE_ROW ){ |
|
2815
|
rid = db_column_int(&q, 0); |
|
2816
|
zCI = fossil_strdup(db_column_text(&q, 1)); |
|
2817
|
zCIUuid = fossil_strdup(zCI); |
|
2818
|
url_add_parameter(&url, "ci", zCI); |
|
2819
|
} |
|
2820
|
db_finalize(&q); |
|
2821
|
if( rid==0 ){ |
|
2822
|
style_header("No such file"); |
|
2823
|
@ File '%h(zName)' does not exist in this repository. |
|
2824
|
} |
|
2825
|
}else{ |
|
2826
|
style_header("No such artifact"); |
|
2827
|
@ Artifact '%h(zName)' does not exist in this repository. |
|
2828
|
} |
|
2829
|
if( rid==0 ){ |
|
2830
|
style_finish_page(); |
|
2831
|
return; |
|
2832
|
} |
|
2833
|
} |
|
2834
|
|
|
2835
|
if( descOnly || P("verbose")!=0 ){ |
|
2836
|
url_add_parameter(&url, "verbose", "1"); |
|
2837
|
objdescFlags |= OBJDESC_DETAIL; |
|
2838
|
} |
|
2839
|
zUuid = db_text("?", "SELECT uuid FROM blob WHERE rid=%d", rid); |
|
2840
|
etag_check(ETAG_HASH, zUuid); |
|
2841
|
|
|
2842
|
if( descOnly && hashOnly ){ |
|
2843
|
blob_set(&uuid, zUuid); |
|
2844
|
cgi_set_content_type("text/plain"); |
|
2845
|
cgi_set_content(&uuid); |
|
2846
|
return; |
|
2847
|
} |
|
2848
|
|
|
2849
|
asText = P("txt")!=0; |
|
2850
|
if( isFile ){ |
|
2851
|
if( docOnly ){ |
|
2852
|
/* No header */ |
|
2853
|
}else if( zCI==0 || fossil_strcmp(zCI,"tip")==0 ){ |
|
2854
|
zCI = "tip"; |
|
2855
|
@ <h2>File %z(href("%R/finfo?name=%T&m&ci=tip",zName))%h(zName)</a> |
|
2856
|
@ from the %z(href("%R/info/tip"))latest check-in</a></h2> |
|
2857
|
}else{ |
|
2858
|
const char *zPath; |
|
2859
|
Blob path; |
|
2860
|
blob_zero(&path); |
|
2861
|
hyperlinked_path(zName, &path, zCI, "dir", "", LINKPATH_FINFO); |
|
2862
|
zPath = blob_str(&path); |
|
2863
|
@ <h2>File %s(zPath) artifact \ |
|
2864
|
style_copy_button(1,"hash-fid",0,0,"%z%S</a> ", |
|
2865
|
href("%R/info/%s",zUuid),zUuid); |
|
2866
|
if( isBranchCI ){ |
|
2867
|
@ on branch %z(href("%R/timeline?r=%T",zCI))%h(zCI)</a></h2> |
|
2868
|
}else if( isSymbolicCI ){ |
|
2869
|
@ part of check-in %z(href("%R/info/%!S",zCIUuid))%s(zCI)</a></h2> |
|
2870
|
}else{ |
|
2871
|
@ part of check-in %z(href("%R/info/%!S",zCIUuid))%S(zCIUuid)</a></h2> |
|
2872
|
} |
|
2873
|
blob_reset(&path); |
|
2874
|
} |
|
2875
|
zMime = mimetype_from_name(zName); |
|
2876
|
if( !docOnly ){ |
|
2877
|
style_submenu_element("Artifact", "%R/artifact/%S", zUuid); |
|
2878
|
style_submenu_element("Annotate", "%R/annotate?filename=%T&checkin=%T", |
|
2879
|
zName, zCI); |
|
2880
|
style_submenu_element("Blame", "%R/blame?filename=%T&checkin=%T", |
|
2881
|
zName, zCI); |
|
2882
|
style_submenu_element("Doc", "%R/doc/%T/%T", zCI, zName); |
|
2883
|
} |
|
2884
|
blob_init(&downloadName, zName, -1); |
|
2885
|
objType = OBJTYPE_CONTENT; |
|
2886
|
}else{ |
|
2887
|
@ <h2>Artifact |
|
2888
|
style_copy_button(1, "hash-ar", 0, 2, "%s", zUuid); |
|
2889
|
if( g.perm.Setup ){ |
|
2890
|
@ (%d(rid)):</h2> |
|
2891
|
}else{ |
|
2892
|
@ :</h2> |
|
2893
|
} |
|
2894
|
blob_zero(&downloadName); |
|
2895
|
if( asText ) objdescFlags &= ~OBJDESC_BASE; |
|
2896
|
objType = object_description(rid, objdescFlags, |
|
2897
|
(isFile?zName:0), &downloadName); |
|
2898
|
zMime = mimetype_from_name(blob_str(&downloadName)); |
|
2899
|
} |
|
2900
|
if( !descOnly && P("download")!=0 ){ |
|
2901
|
cgi_redirectf("%R/raw/%s?at=%T", |
|
2902
|
db_text("x", "SELECT uuid FROM blob WHERE rid=%d", rid), |
|
2903
|
file_tail(blob_str(&downloadName))); |
|
2904
|
/*NOTREACHED*/ |
|
2905
|
} |
|
2906
|
if( g.perm.Admin && !docOnly ){ |
|
2907
|
const char *zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid); |
|
2908
|
if( db_exists("SELECT 1 FROM shun WHERE uuid=%Q", zUuid) ){ |
|
2909
|
style_submenu_element("Unshun", "%R/shun?accept=%s&sub=1#accshun", zUuid); |
|
2910
|
}else{ |
|
2911
|
style_submenu_element("Shun", "%R/shun?shun=%s#addshun",zUuid); |
|
2912
|
} |
|
2913
|
} |
|
2914
|
|
|
2915
|
if( isFile ){ |
|
2916
|
if( isSymbolicCI ){ |
|
2917
|
zHeader = mprintf("%s at %s", file_tail(zName), zCI); |
|
2918
|
style_set_current_page("doc/%t/%T", zCI, zName); |
|
2919
|
}else if( zCIUuid && zCIUuid[0] ){ |
|
2920
|
zHeader = mprintf("%s at [%S]", file_tail(zName), zCIUuid); |
|
2921
|
style_set_current_page("doc/%S/%T", zCIUuid, zName); |
|
2922
|
}else{ |
|
2923
|
zHeader = fossil_strdup(file_tail(zName)); |
|
2924
|
style_set_current_page("doc/tip/%T", zName); |
|
2925
|
} |
|
2926
|
}else if( descOnly ){ |
|
2927
|
zHeader = mprintf("Artifact Description [%S]", zUuid); |
|
2928
|
}else{ |
|
2929
|
zHeader = mprintf("Artifact [%S]", zUuid); |
|
2930
|
} |
|
2931
|
style_header("%s", zHeader); |
|
2932
|
fossil_free(zCIUuid); |
|
2933
|
fossil_free(zHeader); |
|
2934
|
if( !isFile && g.perm.Admin ){ |
|
2935
|
Stmt q; |
|
2936
|
db_prepare(&q, |
|
2937
|
"SELECT coalesce(user.login,rcvfrom.uid)," |
|
2938
|
" datetime(rcvfrom.mtime,toLocal())," |
|
2939
|
" coalesce(rcvfrom.ipaddr,'unknown')," |
|
2940
|
" rcvfrom.rcvid" |
|
2941
|
" FROM blob, rcvfrom LEFT JOIN user ON user.uid=rcvfrom.uid" |
|
2942
|
" WHERE blob.rid=%d" |
|
2943
|
" AND rcvfrom.rcvid=blob.rcvid;", rid); |
|
2944
|
while( db_step(&q)==SQLITE_ROW ){ |
|
2945
|
const char *zUser = db_column_text(&q,0); |
|
2946
|
const char *zDate = db_column_text(&q,1); |
|
2947
|
const char *zIp = db_column_text(&q,2); |
|
2948
|
int rcvid = db_column_int(&q,3); |
|
2949
|
@ <p>Received on %s(zDate) from %h(zUser) at %h(zIp). |
|
2950
|
@ (<a href="%R/rcvfrom?rcvid=%d(rcvid)">rcvid %d(rcvid)</a>)</p> |
|
2951
|
} |
|
2952
|
db_finalize(&q); |
|
2953
|
} |
|
2954
|
if( !docOnly ){ |
|
2955
|
style_submenu_element("Download", "%R/raw/%s?at=%T", |
|
2956
|
zUuid, file_tail(blob_str(&downloadName))); |
|
2957
|
if( db_exists("SELECT 1 FROM mlink WHERE fid=%d", rid) ){ |
|
2958
|
style_submenu_element("Check-ins Using", "%R/timeline?uf=%s", zUuid); |
|
2959
|
} |
|
2960
|
} |
|
2961
|
if( zMime ){ |
|
2962
|
if( fossil_strcmp(zMime, "text/html")==0 ){ |
|
2963
|
if( asText ){ |
|
2964
|
style_submenu_element("Html", "%s", url_render(&url, "txt", 0, 0, 0)); |
|
2965
|
}else{ |
|
2966
|
renderAsHtml = 1; |
|
2967
|
if( !docOnly ){ |
|
2968
|
style_submenu_element("Text", "%s", url_render(&url, "txt","1",0,0)); |
|
2969
|
} |
|
2970
|
} |
|
2971
|
}else if( fossil_strcmp(zMime, "text/x-fossil-wiki")==0 |
|
2972
|
|| fossil_strcmp(zMime, "text/x-markdown")==0 |
|
2973
|
|| fossil_strcmp(zMime, "text/x-pikchr")==0 ){ |
|
2974
|
if( asText ){ |
|
2975
|
style_submenu_element(zMime[7]=='p' ? "Pikchr" : "Wiki", |
|
2976
|
"%s", url_render(&url, "txt", 0, 0, 0)); |
|
2977
|
}else{ |
|
2978
|
renderAsWiki = 1; |
|
2979
|
if( !docOnly ){ |
|
2980
|
style_submenu_element("Text", "%s", url_render(&url, "txt","1",0,0)); |
|
2981
|
} |
|
2982
|
} |
|
2983
|
}else if( fossil_strcmp(zMime, "image/svg+xml")==0 ){ |
|
2984
|
if( asText ){ |
|
2985
|
style_submenu_element("Svg", "%s", url_render(&url, "txt", 0, 0, 0)); |
|
2986
|
}else{ |
|
2987
|
renderAsSvg = 1; |
|
2988
|
if( !docOnly ){ |
|
2989
|
style_submenu_element("Text", "%s", url_render(&url, "txt","1",0,0)); |
|
2990
|
} |
|
2991
|
} |
|
2992
|
} |
|
2993
|
if( !docOnly && fileedit_is_editable(zName) ){ |
|
2994
|
style_submenu_element("Edit", |
|
2995
|
"%R/fileedit?filename=%T&checkin=%!S", |
|
2996
|
zName, zCI); |
|
2997
|
} |
|
2998
|
} |
|
2999
|
if( (objType & (OBJTYPE_WIKI|OBJTYPE_TICKET))!=0 ){ |
|
3000
|
style_submenu_element("Parsed", "%R/info/%s", zUuid); |
|
3001
|
} |
|
3002
|
if( descOnly ){ |
|
3003
|
style_submenu_element("Content", "%R/artifact/%s", zUuid); |
|
3004
|
}else{ |
|
3005
|
if( !docOnly || !isFile ){ |
|
3006
|
@ <hr> |
|
3007
|
} |
|
3008
|
content_get(rid, &content); |
|
3009
|
if( renderAsWiki ){ |
|
3010
|
safe_html_context(DOCSRC_FILE); |
|
3011
|
wiki_render_by_mimetype(&content, zMime); |
|
3012
|
document_emit_js(); |
|
3013
|
}else if( renderAsHtml ){ |
|
3014
|
@ <iframe src="%R/raw/%s(zUuid)" |
|
3015
|
@ width="100%%" frameborder="0" marginwidth="0" marginheight="0" |
|
3016
|
@ sandbox="allow-same-origin" id="ifm1"> |
|
3017
|
@ </iframe> |
|
3018
|
@ <script nonce="%h(style_nonce())">/* info.c:%d(__LINE__) */ |
|
3019
|
@ document.getElementById("ifm1").addEventListener("load", |
|
3020
|
@ function(){ |
|
3021
|
@ this.height=this.contentDocument.documentElement.scrollHeight + 75; |
|
3022
|
@ } |
|
3023
|
@ ); |
|
3024
|
@ </script> |
|
3025
|
}else if( renderAsSvg ){ |
|
3026
|
@ <object type="image/svg+xml" data="%R/raw/%s(zUuid)"></object> |
|
3027
|
}else{ |
|
3028
|
const char *zContentMime; |
|
3029
|
style_submenu_element("Hex", "%R/hexdump?name=%s", zUuid); |
|
3030
|
if( zLn==0 || atoi(zLn)==0 ){ |
|
3031
|
style_submenu_checkbox("ln", "Line Numbers", 0, 0); |
|
3032
|
} |
|
3033
|
blob_to_utf8_no_bom(&content, 0); |
|
3034
|
zContentMime = mimetype_from_content(&content); |
|
3035
|
if( zMime==0 ) zMime = zContentMime; |
|
3036
|
@ <blockquote class="file-content"> |
|
3037
|
if( zContentMime==0 ){ |
|
3038
|
const char *z, *zFileName, *zExt; |
|
3039
|
z = blob_str(&content); |
|
3040
|
zFileName = db_text(0, |
|
3041
|
"SELECT name FROM mlink, filename" |
|
3042
|
" WHERE filename.fnid=mlink.fnid" |
|
3043
|
" AND mlink.fid=%d", |
|
3044
|
rid); |
|
3045
|
zExt = zFileName ? file_extension(zFileName) : 0; |
|
3046
|
if( zLn ){ |
|
3047
|
output_text_with_line_numbers(z, blob_size(&content), |
|
3048
|
zFileName, zLn, 1); |
|
3049
|
}else if( zExt && zExt[0] ){ |
|
3050
|
@ <pre> |
|
3051
|
@ <code class="language-%s(zExt)">%h(z)</code> |
|
3052
|
@ </pre> |
|
3053
|
}else{ |
|
3054
|
@ <pre> |
|
3055
|
@ %h(z) |
|
3056
|
@ </pre> |
|
3057
|
} |
|
3058
|
}else if( strncmp(zMime, "image/", 6)==0 ){ |
|
3059
|
@ <p>(file is %d(blob_size(&content)) bytes of image data)</i></p> |
|
3060
|
@ <p><img src="%R/raw/%s(zUuid)?m=%s(zMime)"></p> |
|
3061
|
style_submenu_element("Image", "%R/raw/%s?m=%s", zUuid, zMime); |
|
3062
|
}else if( strncmp(zMime, "audio/", 6)==0 ){ |
|
3063
|
@ <p>(file is %d(blob_size(&content)) bytes of sound data)</i></p> |
|
3064
|
@ <audio controls src="%R/raw/%s(zUuid)?m=%s(zMime)"> |
|
3065
|
@ (Not supported by this browser) |
|
3066
|
@ </audio> |
|
3067
|
}else{ |
|
3068
|
@ <i>(file is %d(blob_size(&content)) bytes of binary data)</i> |
|
3069
|
} |
|
3070
|
@ </blockquote> |
|
3071
|
} |
|
3072
|
} |
|
3073
|
style_finish_page(); |
|
3074
|
} |
|
3075
|
|
|
3076
|
/* |
|
3077
|
** WEBPAGE: tinfo |
|
3078
|
** URL: /tinfo?name=ARTIFACTID |
|
3079
|
** |
|
3080
|
** Show the details of a ticket change control artifact. |
|
3081
|
*/ |
|
3082
|
void tinfo_page(void){ |
|
3083
|
int rid; |
|
3084
|
char *zDate; |
|
3085
|
const char *zUuid; |
|
3086
|
char zTktName[HNAME_MAX+1]; |
|
3087
|
Manifest *pTktChng; |
|
3088
|
int modPending; |
|
3089
|
const char *zModAction; |
|
3090
|
char *zTktTitle; |
|
3091
|
login_check_credentials(); |
|
3092
|
if( !g.perm.RdTkt ){ login_needed(g.anon.RdTkt); return; } |
|
3093
|
rid = name_to_rid_www("name"); |
|
3094
|
if( rid==0 ){ fossil_redirect_home(); } |
|
3095
|
cgi_check_for_malice(); |
|
3096
|
zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid); |
|
3097
|
if( g.perm.Admin ){ |
|
3098
|
if( db_exists("SELECT 1 FROM shun WHERE uuid=%Q", zUuid) ){ |
|
3099
|
style_submenu_element("Unshun", "%R/shun?accept=%s&sub=1#accshun", zUuid); |
|
3100
|
}else{ |
|
3101
|
style_submenu_element("Shun", "%R/shun?shun=%s#addshun", zUuid); |
|
3102
|
} |
|
3103
|
} |
|
3104
|
pTktChng = manifest_get(rid, CFTYPE_TICKET, 0); |
|
3105
|
if( pTktChng==0 ) fossil_redirect_home(); |
|
3106
|
zDate = db_text(0, "SELECT datetime(%.12f,toLocal())", pTktChng->rDate); |
|
3107
|
sqlite3_snprintf(sizeof(zTktName), zTktName, "%s", pTktChng->zTicketUuid); |
|
3108
|
if( g.perm.ModTkt && (zModAction = P("modaction"))!=0 ){ |
|
3109
|
if( strcmp(zModAction,"delete")==0 ){ |
|
3110
|
moderation_disapprove(rid); |
|
3111
|
/* |
|
3112
|
** Next, check if the ticket still exists; if not, we cannot |
|
3113
|
** redirect to it. |
|
3114
|
*/ |
|
3115
|
if( db_exists("SELECT 1 FROM ticket WHERE tkt_uuid GLOB '%q*'", |
|
3116
|
zTktName) ){ |
|
3117
|
cgi_redirectf("%R/tktview/%s", zTktName); |
|
3118
|
/*NOTREACHED*/ |
|
3119
|
}else{ |
|
3120
|
cgi_redirectf("%R/modreq"); |
|
3121
|
/*NOTREACHED*/ |
|
3122
|
} |
|
3123
|
} |
|
3124
|
if( strcmp(zModAction,"approve")==0 ){ |
|
3125
|
moderation_approve('t', rid); |
|
3126
|
} |
|
3127
|
} |
|
3128
|
zTktTitle = db_table_has_column("repository", "ticket", "title" ) |
|
3129
|
? db_text("(No title)", |
|
3130
|
"SELECT title FROM ticket WHERE tkt_uuid=%Q", zTktName) |
|
3131
|
: 0; |
|
3132
|
style_set_current_feature("tinfo"); |
|
3133
|
style_header("Ticket Change Details"); |
|
3134
|
style_submenu_element("Raw", "%R/artifact/%s", zUuid); |
|
3135
|
style_submenu_element("History", "%R/tkthistory/%s#%S", zTktName,zUuid); |
|
3136
|
style_submenu_element("Page", "%R/tktview/%t", zTktName); |
|
3137
|
style_submenu_element("Timeline", "%R/tkttimeline/%t", zTktName); |
|
3138
|
if( P("plaintext") ){ |
|
3139
|
style_submenu_element("Formatted", "%R/info/%s", zUuid); |
|
3140
|
}else{ |
|
3141
|
style_submenu_element("Plaintext", "%R/info/%s?plaintext", zUuid); |
|
3142
|
} |
|
3143
|
|
|
3144
|
@ <div class="section">Overview</div> |
|
3145
|
@ <p><table class="label-value"> |
|
3146
|
@ <tr><th>Artifact ID:</th> |
|
3147
|
@ <td>%z(href("%R/artifact/%!S",zUuid))%s(zUuid)</a> |
|
3148
|
if( g.perm.Setup ){ |
|
3149
|
@ (%d(rid)) |
|
3150
|
} |
|
3151
|
modPending = moderation_pending_www(rid); |
|
3152
|
@ <tr><th>Ticket:</th> |
|
3153
|
@ <td>%z(href("%R/tktview/%s",zTktName))%s(zTktName)</a> |
|
3154
|
if( zTktTitle ){ |
|
3155
|
@<br>%h(zTktTitle) |
|
3156
|
} |
|
3157
|
@</td></tr> |
|
3158
|
@ <tr><th>User & Date:</th><td> |
|
3159
|
hyperlink_to_user(pTktChng->zUser, zDate, " on "); |
|
3160
|
hyperlink_to_date(zDate, "</td></tr>"); |
|
3161
|
@ </table> |
|
3162
|
free(zDate); |
|
3163
|
free(zTktTitle); |
|
3164
|
|
|
3165
|
if( g.perm.ModTkt && modPending ){ |
|
3166
|
@ <div class="section">Moderation</div> |
|
3167
|
@ <blockquote> |
|
3168
|
@ <form method="POST" action="%R/tinfo/%s(zUuid)"> |
|
3169
|
@ <label><input type="radio" name="modaction" value="delete"> |
|
3170
|
@ Delete this change</label><br> |
|
3171
|
@ <label><input type="radio" name="modaction" value="approve"> |
|
3172
|
@ Approve this change</label><br> |
|
3173
|
@ <input type="submit" value="Submit"> |
|
3174
|
@ </form> |
|
3175
|
@ </blockquote> |
|
3176
|
} |
|
3177
|
|
|
3178
|
@ <div class="section">Changes</div> |
|
3179
|
@ <p> |
|
3180
|
ticket_output_change_artifact(pTktChng, 0, 1, 0); |
|
3181
|
manifest_destroy(pTktChng); |
|
3182
|
style_finish_page(); |
|
3183
|
} |
|
3184
|
|
|
3185
|
/* |
|
3186
|
** rid is a cluster. Paint a page that contains detailed information |
|
3187
|
** about that cluster. |
|
3188
|
*/ |
|
3189
|
static void cluster_info(int rid, const char *zName){ |
|
3190
|
Manifest *pCluster; |
|
3191
|
int i; |
|
3192
|
Blob where = BLOB_INITIALIZER; |
|
3193
|
Blob unks = BLOB_INITIALIZER; |
|
3194
|
Stmt q; |
|
3195
|
char *zSha1Bg; |
|
3196
|
char *zSha3Bg; |
|
3197
|
int badRid = 0; |
|
3198
|
int rcvid; |
|
3199
|
int hashClr = PB("hclr"); |
|
3200
|
const char *zDate; |
|
3201
|
|
|
3202
|
pCluster = manifest_get(rid, CFTYPE_CLUSTER, 0); |
|
3203
|
if( pCluster==0 ){ |
|
3204
|
artifact_page(); |
|
3205
|
return; |
|
3206
|
} |
|
3207
|
style_header("Cluster %S", zName); |
|
3208
|
rcvid = db_int(0, "SELECT rcvid FROM blob WHERE rid=%d", rid); |
|
3209
|
if( rcvid==0 ){ |
|
3210
|
zDate = 0; |
|
3211
|
}else{ |
|
3212
|
zDate = db_text(0, "SELECT datetime(mtime) FROM rcvfrom WHERE rcvid=%d", |
|
3213
|
rcvid); |
|
3214
|
} |
|
3215
|
@ <p>Artifact %z(href("%R/artifact/%h",zName))%S(zName)</a> is a cluster |
|
3216
|
@ with %d(pCluster->nCChild) entries |
|
3217
|
if( g.perm.Admin ){ |
|
3218
|
@ received <a href="%R/rcvfrom?rcvid=%d(rcvid)">%h(zDate)</a>: |
|
3219
|
}else{ |
|
3220
|
@ received %h(zDate): |
|
3221
|
} |
|
3222
|
blob_appendf(&where,"IN(0"); |
|
3223
|
for(i=0; i<pCluster->nCChild; i++){ |
|
3224
|
int rid = fast_uuid_to_rid(pCluster->azCChild[i]); |
|
3225
|
if( rid ){ |
|
3226
|
blob_appendf(&where,",%d", rid); |
|
3227
|
}else{ |
|
3228
|
if( blob_size(&unks)>0 ) blob_append_char(&unks, ','); |
|
3229
|
badRid++; |
|
3230
|
blob_append_sql(&unks,"(%d,%Q)",-badRid,pCluster->azCChild[i]); |
|
3231
|
} |
|
3232
|
} |
|
3233
|
blob_append_char(&where,')'); |
|
3234
|
describe_artifacts(blob_str(&where)); |
|
3235
|
blob_reset(&where); |
|
3236
|
if( badRid>0 ){ |
|
3237
|
db_multi_exec( |
|
3238
|
"WITH unks(rx,hx) AS (VALUES %s)\n" |
|
3239
|
"INSERT INTO description(rid,uuid,type,summary) " |
|
3240
|
" SELECT rx, hx, 'phantom', '' FROM unks;", |
|
3241
|
blob_sql_text(&unks) |
|
3242
|
); |
|
3243
|
} |
|
3244
|
blob_reset(&unks); |
|
3245
|
db_prepare(&q, |
|
3246
|
"SELECT rid, uuid, summary, isPrivate, type='phantom', rcvid, ref" |
|
3247
|
" FROM description ORDER BY uuid" |
|
3248
|
); |
|
3249
|
if( skin_detail_boolean("white-foreground") ){ |
|
3250
|
zSha1Bg = "#714417"; |
|
3251
|
zSha3Bg = "#177117"; |
|
3252
|
}else{ |
|
3253
|
zSha1Bg = "#ebffb0"; |
|
3254
|
zSha3Bg = "#b0ffb0"; |
|
3255
|
} |
|
3256
|
@ <table cellpadding="2" cellspacing="0" border="1"> |
|
3257
|
if( g.perm.Admin ){ |
|
3258
|
@ <tr><th>RID<th>Hash<th>Rcvid<th>Description<th>Ref<th>Remarks |
|
3259
|
}else{ |
|
3260
|
@ <tr><th>RID<th>Hash<th>Description<th>Ref<th>Remarks |
|
3261
|
} |
|
3262
|
while( db_step(&q)==SQLITE_ROW ){ |
|
3263
|
int rid = db_column_int(&q,0); |
|
3264
|
const char *zUuid = db_column_text(&q, 1); |
|
3265
|
const char *zDesc = db_column_text(&q, 2); |
|
3266
|
int isPriv = db_column_int(&q,3); |
|
3267
|
int isPhantom = db_column_int(&q,4); |
|
3268
|
const char *zRef = db_column_text(&q,6); |
|
3269
|
if( isPriv && !isPhantom && !g.perm.Private && !g.perm.Admin ){ |
|
3270
|
/* Don't show private artifacts to users without Private (x) permission */ |
|
3271
|
continue; |
|
3272
|
} |
|
3273
|
if( rid<=0 ){ |
|
3274
|
@ <tr><td> </td> |
|
3275
|
}else if( hashClr ){ |
|
3276
|
const char *zClr = db_column_bytes(&q,1)>40 ? zSha3Bg : zSha1Bg; |
|
3277
|
@ <tr style='background-color:%s(zClr);'><td align="right">%d(rid)</td> |
|
3278
|
}else{ |
|
3279
|
@ <tr><td align="right">%d(rid)</td> |
|
3280
|
} |
|
3281
|
if( rid<=0 ){ |
|
3282
|
@ <td> %S(zUuid) </td> |
|
3283
|
}else{ |
|
3284
|
@ <td> %z(href("%R/info/%!S",zUuid))%S(zUuid)</a> </td> |
|
3285
|
} |
|
3286
|
if( g.perm.Admin ){ |
|
3287
|
int rcvid = db_column_int(&q,5); |
|
3288
|
if( rcvid<=0 ){ |
|
3289
|
@ <td> |
|
3290
|
}else{ |
|
3291
|
@ <td><a href='%R/rcvfrom?rcvid=%d(rcvid)'>%d(rcvid)</a> |
|
3292
|
} |
|
3293
|
} |
|
3294
|
@ <td align="left">%h(zDesc)</td> |
|
3295
|
if( zRef && zRef[0] ){ |
|
3296
|
@ <td>%z(href("%R/info/%!S",zRef))%S(zRef)</a> |
|
3297
|
}else{ |
|
3298
|
@ <td> |
|
3299
|
} |
|
3300
|
if( isPriv || isPhantom ){ |
|
3301
|
if( isPriv==0 ){ |
|
3302
|
@ <td>phantom</td> |
|
3303
|
}else if( isPhantom==0 ){ |
|
3304
|
@ <td>private</td> |
|
3305
|
}else{ |
|
3306
|
@ <td>private,phantom</td> |
|
3307
|
} |
|
3308
|
}else{ |
|
3309
|
@ <td> |
|
3310
|
} |
|
3311
|
@ </tr> |
|
3312
|
} |
|
3313
|
@ </table> |
|
3314
|
db_finalize(&q); |
|
3315
|
style_finish_page(); |
|
3316
|
} |
|
3317
|
|
|
3318
|
|
|
3319
|
/* |
|
3320
|
** WEBPAGE: info |
|
3321
|
** URL: info/NAME |
|
3322
|
** |
|
3323
|
** The NAME argument is any valid artifact name: an artifact hash, |
|
3324
|
** a timestamp, a tag name, etc. |
|
3325
|
** |
|
3326
|
** Because NAME can match so many different things (commit artifacts, |
|
3327
|
** wiki pages, ticket comments, forum posts...) the format of the output |
|
3328
|
** page depends on the type of artifact that NAME matches. |
|
3329
|
*/ |
|
3330
|
void info_page(void){ |
|
3331
|
const char *zName; |
|
3332
|
Blob uuid; |
|
3333
|
int rid; |
|
3334
|
int rc; |
|
3335
|
int nLen; |
|
3336
|
|
|
3337
|
zName = P("name"); |
|
3338
|
if( zName==0 ) fossil_redirect_home(); |
|
3339
|
cgi_check_for_malice(); |
|
3340
|
nLen = strlen(zName); |
|
3341
|
blob_set(&uuid, zName); |
|
3342
|
if( name_collisions(zName) ){ |
|
3343
|
cgi_set_parameter("src","info"); |
|
3344
|
ambiguous_page(); |
|
3345
|
return; |
|
3346
|
} |
|
3347
|
rc = name_to_uuid(&uuid, -1, "*"); |
|
3348
|
if( rc==1 ){ |
|
3349
|
if( validate16(zName, nLen) ){ |
|
3350
|
if( db_exists("SELECT 1 FROM ticket WHERE tkt_uuid GLOB '%q*'", zName) ){ |
|
3351
|
cgi_set_parameter_nocopy("tl","1",0); |
|
3352
|
tktview_page(); |
|
3353
|
return; |
|
3354
|
} |
|
3355
|
if( db_exists("SELECT 1 FROM tag" |
|
3356
|
" WHERE tagname GLOB 'event-%q*'", zName) ){ |
|
3357
|
event_page(); |
|
3358
|
return; |
|
3359
|
} |
|
3360
|
} |
|
3361
|
style_header("No Such Object"); |
|
3362
|
@ <p>No such object: %h(zName)</p> |
|
3363
|
if( nLen<4 ){ |
|
3364
|
@ <p>Object name should be no less than 4 characters. Ten or more |
|
3365
|
@ characters are recommended.</p> |
|
3366
|
} |
|
3367
|
style_finish_page(); |
|
3368
|
return; |
|
3369
|
}else if( rc==2 ){ |
|
3370
|
cgi_set_parameter("src","info"); |
|
3371
|
ambiguous_page(); |
|
3372
|
return; |
|
3373
|
} |
|
3374
|
zName = blob_str(&uuid); |
|
3375
|
rid = db_int(0, "SELECT rid FROM blob WHERE uuid=%Q", zName); |
|
3376
|
if( rid==0 ){ |
|
3377
|
style_header("Broken Link"); |
|
3378
|
@ <p>No such object: %h(zName)</p> |
|
3379
|
style_finish_page(); |
|
3380
|
return; |
|
3381
|
} |
|
3382
|
if( db_exists("SELECT 1 FROM mlink WHERE mid=%d", rid) ){ |
|
3383
|
ci_page(); |
|
3384
|
}else |
|
3385
|
if( db_exists("SELECT 1 FROM tagxref JOIN tag USING(tagid)" |
|
3386
|
" WHERE rid=%d AND tagname LIKE 'wiki-%%'", rid) ){ |
|
3387
|
winfo_page(); |
|
3388
|
}else |
|
3389
|
if( db_exists("SELECT 1 FROM tagxref JOIN tag USING(tagid)" |
|
3390
|
" WHERE rid=%d AND tagname LIKE 'tkt-%%'", rid) ){ |
|
3391
|
tinfo_page(); |
|
3392
|
}else |
|
3393
|
if( db_table_exists("repository","forumpost") |
|
3394
|
&& db_exists("SELECT 1 FROM forumpost WHERE fpid=%d", rid) |
|
3395
|
){ |
|
3396
|
forumthread_page(); |
|
3397
|
}else |
|
3398
|
if( db_exists("SELECT 1 FROM plink WHERE cid=%d", rid) ){ |
|
3399
|
ci_page(); |
|
3400
|
}else |
|
3401
|
if( db_exists("SELECT 1 FROM plink WHERE pid=%d", rid) ){ |
|
3402
|
ci_page(); |
|
3403
|
}else |
|
3404
|
if( db_exists("SELECT 1 FROM attachment WHERE attachid=%d", rid) ){ |
|
3405
|
ainfo_page(); |
|
3406
|
}else |
|
3407
|
if( db_exists("SELECT 1 FROM tagxref WHERE rid=%d AND tagid=%d", |
|
3408
|
rid, TAG_CLUSTER) ){ |
|
3409
|
cluster_info(rid, zName); |
|
3410
|
}else |
|
3411
|
{ |
|
3412
|
artifact_page(); |
|
3413
|
} |
|
3414
|
} |
|
3415
|
|
|
3416
|
/* |
|
3417
|
** Do a comment comparison. |
|
3418
|
** |
|
3419
|
** + Leading and trailing whitespace are ignored. |
|
3420
|
** + \r\n characters compare equal to \n |
|
3421
|
** |
|
3422
|
** Return true if equal and false if not equal. |
|
3423
|
*/ |
|
3424
|
static int comment_compare(const char *zA, const char *zB){ |
|
3425
|
if( zA==0 ) zA = ""; |
|
3426
|
if( zB==0 ) zB = ""; |
|
3427
|
while( fossil_isspace(zA[0]) ) zA++; |
|
3428
|
while( fossil_isspace(zB[0]) ) zB++; |
|
3429
|
while( zA[0] && zB[0] ){ |
|
3430
|
if( zA[0]==zB[0] ){ zA++; zB++; continue; } |
|
3431
|
if( zA[0]=='\r' && zA[1]=='\n' && zB[0]=='\n' ){ |
|
3432
|
zA += 2; |
|
3433
|
zB++; |
|
3434
|
continue; |
|
3435
|
} |
|
3436
|
if( zB[0]=='\r' && zB[1]=='\n' && zA[0]=='\n' ){ |
|
3437
|
zB += 2; |
|
3438
|
zA++; |
|
3439
|
continue; |
|
3440
|
} |
|
3441
|
return 0; |
|
3442
|
} |
|
3443
|
while( fossil_isspace(zB[0]) ) zB++; |
|
3444
|
while( fossil_isspace(zA[0]) ) zA++; |
|
3445
|
return zA[0]==0 && zB[0]==0; |
|
3446
|
} |
|
3447
|
|
|
3448
|
/* |
|
3449
|
** The following methods operate on the newtags temporary table |
|
3450
|
** that is used to collect various changes to be added to a control |
|
3451
|
** artifact for a check-in edit. |
|
3452
|
*/ |
|
3453
|
static void init_newtags(void){ |
|
3454
|
db_multi_exec("CREATE TEMP TABLE newtags(tag UNIQUE, prefix, value)"); |
|
3455
|
} |
|
3456
|
|
|
3457
|
static void change_special( |
|
3458
|
const char *zName, /* Name of the special tag */ |
|
3459
|
const char *zOp, /* Operation prefix (e.g. +,-,*) */ |
|
3460
|
const char *zValue /* Value of the tag */ |
|
3461
|
){ |
|
3462
|
db_multi_exec("REPLACE INTO newtags VALUES(%Q,'%q',%Q)", zName, zOp, zValue); |
|
3463
|
} |
|
3464
|
|
|
3465
|
static void change_sym_tag(const char *zTag, const char *zOp){ |
|
3466
|
db_multi_exec("REPLACE INTO newtags VALUES('sym-%q',%Q,NULL)", zTag, zOp); |
|
3467
|
} |
|
3468
|
|
|
3469
|
static void cancel_special(const char *zTag){ |
|
3470
|
change_special(zTag,"-",0); |
|
3471
|
} |
|
3472
|
|
|
3473
|
static void add_color(const char *zNewColor, int fPropagateColor){ |
|
3474
|
change_special("bgcolor",fPropagateColor ? "*" : "+", zNewColor); |
|
3475
|
} |
|
3476
|
|
|
3477
|
static void cancel_color(void){ |
|
3478
|
change_special("bgcolor","-",0); |
|
3479
|
} |
|
3480
|
|
|
3481
|
static void add_comment(const char *zNewComment){ |
|
3482
|
change_special("comment","+",zNewComment); |
|
3483
|
} |
|
3484
|
|
|
3485
|
static void add_date(const char *zNewDate){ |
|
3486
|
change_special("date","+",zNewDate); |
|
3487
|
} |
|
3488
|
|
|
3489
|
static void add_user(const char *zNewUser){ |
|
3490
|
change_special("user","+",zNewUser); |
|
3491
|
} |
|
3492
|
|
|
3493
|
static void add_tag(const char *zNewTag){ |
|
3494
|
change_sym_tag(zNewTag,"+"); |
|
3495
|
} |
|
3496
|
|
|
3497
|
static void cancel_tag(int rid, const char *zCancelTag){ |
|
3498
|
if( db_exists("SELECT 1 FROM tagxref, tag" |
|
3499
|
" WHERE tagxref.rid=%d AND tagtype>0" |
|
3500
|
" AND tagxref.tagid=tag.tagid AND tagname='sym-%q'", |
|
3501
|
rid, zCancelTag) |
|
3502
|
) change_sym_tag(zCancelTag,"-"); |
|
3503
|
} |
|
3504
|
|
|
3505
|
static void hide_branch(void){ |
|
3506
|
change_special("hidden","*",0); |
|
3507
|
} |
|
3508
|
|
|
3509
|
static void close_leaf(int rid){ |
|
3510
|
change_special("closed",is_a_leaf(rid)?"+":"*",0); |
|
3511
|
} |
|
3512
|
|
|
3513
|
static void change_branch(int rid, const char *zNewBranch){ |
|
3514
|
db_multi_exec( |
|
3515
|
"REPLACE INTO newtags " |
|
3516
|
" SELECT tagname, '-', NULL FROM tagxref, tag" |
|
3517
|
" WHERE tagxref.rid=%d AND tagtype==2" |
|
3518
|
" AND tagname GLOB 'sym-*'" |
|
3519
|
" AND tag.tagid=tagxref.tagid", |
|
3520
|
rid |
|
3521
|
); |
|
3522
|
change_special("branch","*",zNewBranch); |
|
3523
|
change_sym_tag(zNewBranch,"*"); |
|
3524
|
} |
|
3525
|
|
|
3526
|
/* |
|
3527
|
** The apply_newtags method is called after all newtags have been added |
|
3528
|
** and the control artifact is completed and then written to the DB. |
|
3529
|
*/ |
|
3530
|
static void apply_newtags( |
|
3531
|
Blob *ctrl, |
|
3532
|
int rid, |
|
3533
|
const char *zUuid, |
|
3534
|
const char *zUserOvrd, /* The user name on the control artifact */ |
|
3535
|
int fDryRun /* Print control artifact, but make no changes */ |
|
3536
|
){ |
|
3537
|
Stmt q; |
|
3538
|
int nChng = 0; |
|
3539
|
|
|
3540
|
db_prepare(&q, "SELECT tag, prefix, value FROM newtags" |
|
3541
|
" ORDER BY prefix || tag"); |
|
3542
|
while( db_step(&q)==SQLITE_ROW ){ |
|
3543
|
const char *zTag = db_column_text(&q, 0); |
|
3544
|
const char *zPrefix = db_column_text(&q, 1); |
|
3545
|
const char *zValue = db_column_text(&q, 2); |
|
3546
|
nChng++; |
|
3547
|
if( zValue ){ |
|
3548
|
blob_appendf(ctrl, "T %s%F %s %F\n", zPrefix, zTag, zUuid, zValue); |
|
3549
|
}else{ |
|
3550
|
blob_appendf(ctrl, "T %s%F %s\n", zPrefix, zTag, zUuid); |
|
3551
|
} |
|
3552
|
} |
|
3553
|
db_finalize(&q); |
|
3554
|
if( nChng>0 ){ |
|
3555
|
int nrid; |
|
3556
|
Blob cksum; |
|
3557
|
if( zUserOvrd && zUserOvrd[0] ){ |
|
3558
|
blob_appendf(ctrl, "U %F\n", zUserOvrd); |
|
3559
|
}else{ |
|
3560
|
blob_appendf(ctrl, "U %F\n", login_name()); |
|
3561
|
} |
|
3562
|
md5sum_blob(ctrl, &cksum); |
|
3563
|
blob_appendf(ctrl, "Z %b\n", &cksum); |
|
3564
|
if( fDryRun ){ |
|
3565
|
assert( g.isHTTP==0 ); /* Only print control artifact in console mode. */ |
|
3566
|
fossil_print("%s", blob_str(ctrl)); |
|
3567
|
blob_reset(ctrl); |
|
3568
|
}else{ |
|
3569
|
db_begin_transaction(); |
|
3570
|
g.markPrivate = content_is_private(rid); |
|
3571
|
nrid = content_put(ctrl); |
|
3572
|
manifest_crosslink(nrid, ctrl, MC_PERMIT_HOOKS); |
|
3573
|
db_end_transaction(0); |
|
3574
|
} |
|
3575
|
assert( blob_is_reset(ctrl) ); |
|
3576
|
} |
|
3577
|
} |
|
3578
|
|
|
3579
|
/* |
|
3580
|
** This method checks that the date can be parsed. |
|
3581
|
** Returns 1 if datetime() can validate, 0 otherwise. |
|
3582
|
*/ |
|
3583
|
int is_datetime(const char* zDate){ |
|
3584
|
return db_int(0, "SELECT datetime(%Q) NOT NULL", zDate); |
|
3585
|
} |
|
3586
|
|
|
3587
|
/* |
|
3588
|
** WEBPAGE: ci_edit |
|
3589
|
** |
|
3590
|
** Edit a check-in. (Check-ins are immutable and do not really change. |
|
3591
|
** This page really creates supplemental tags that affect the display |
|
3592
|
** of the check-in.) |
|
3593
|
** |
|
3594
|
** Query parameters: |
|
3595
|
** |
|
3596
|
** rid=INTEGER Record ID of the check-in to edit (REQUIRED) |
|
3597
|
** |
|
3598
|
** POST parameters after pressing "Preview", "Cancel", or "Apply": |
|
3599
|
** |
|
3600
|
** c=TEXT New check-in comment |
|
3601
|
** u=TEXT New user name |
|
3602
|
** newclr Apply a background color |
|
3603
|
** clr=TEXT New background color (only if newclr) |
|
3604
|
** pclr Propagate new background color (only if newclr) |
|
3605
|
** dt=TEXT New check-in date/time (ISO8610 format) |
|
3606
|
** newtag Add a new tag to the check-in |
|
3607
|
** tagname=TEXT Name of the new tag to be added (only if newtag) |
|
3608
|
** newbr Put the check-in on a new branch |
|
3609
|
** brname=TEXT Name of the new branch (only if newbr) |
|
3610
|
** close Close this check-in |
|
3611
|
** hide Hide this check-in |
|
3612
|
** cNNN Cancel tag with tagid=NNN |
|
3613
|
** |
|
3614
|
** cancel Cancel the edit. Return to the check-in view |
|
3615
|
** preview Show a preview of the edited check-in comment |
|
3616
|
** apply Apply changes |
|
3617
|
*/ |
|
3618
|
void ci_edit_page(void){ |
|
3619
|
int rid; |
|
3620
|
const char *zComment; /* Current comment on the check-in */ |
|
3621
|
const char *zNewComment; /* Revised check-in comment */ |
|
3622
|
const char *zUser; /* Current user for the check-in */ |
|
3623
|
const char *zNewUser; /* Revised user */ |
|
3624
|
const char *zDate; /* Current date of the check-in */ |
|
3625
|
const char *zNewDate; /* Revised check-in date */ |
|
3626
|
const char *zNewColorFlag; /* "checked" if "Change color" is checked */ |
|
3627
|
const char *zColor; /* Current background color */ |
|
3628
|
const char *zNewColor; /* Revised background color */ |
|
3629
|
const char *zNewTagFlag; /* "checked" if "Add tag" is checked */ |
|
3630
|
const char *zNewTag; /* Name of the new tag */ |
|
3631
|
const char *zNewBrFlag; /* "checked" if "New branch" is checked */ |
|
3632
|
const char *zNewBranch; /* Name of the new branch */ |
|
3633
|
const char *zCloseFlag; /* "checked" if "Close" is checked */ |
|
3634
|
const char *zHideFlag; /* "checked" if "Hide" is checked */ |
|
3635
|
int fPropagateColor; /* True if color propagates before edit */ |
|
3636
|
int fNewPropagateColor; /* True if color propagates after edit */ |
|
3637
|
int fHasHidden = 0; /* True if hidden tag already set */ |
|
3638
|
int fHasClosed = 0; /* True if closed tag already set */ |
|
3639
|
const char *zChngTime = 0; /* Value of chngtime= query param, if any */ |
|
3640
|
char *zUuid; |
|
3641
|
Blob comment; |
|
3642
|
char *zBranchName = 0; |
|
3643
|
Stmt q; |
|
3644
|
|
|
3645
|
login_check_credentials(); |
|
3646
|
if( !g.perm.Write ){ login_needed(g.anon.Write); return; } |
|
3647
|
rid = name_to_typed_rid(P("r"), "ci"); |
|
3648
|
zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); |
|
3649
|
zComment = db_text(0, "SELECT coalesce(ecomment,comment)" |
|
3650
|
" FROM event WHERE objid=%d", rid); |
|
3651
|
if( zComment==0 ) fossil_redirect_home(); |
|
3652
|
if( P("cancel") ){ |
|
3653
|
cgi_redirectf("%R/ci/%S", zUuid); |
|
3654
|
} |
|
3655
|
if( g.perm.Setup ) zChngTime = P("chngtime"); |
|
3656
|
zNewComment = PD("c",zComment); |
|
3657
|
zUser = db_text(0, "SELECT coalesce(euser,user)" |
|
3658
|
" FROM event WHERE objid=%d", rid); |
|
3659
|
if( zUser==0 ) fossil_redirect_home(); |
|
3660
|
zNewUser = PDT("u",zUser); |
|
3661
|
zDate = db_text(0, "SELECT datetime(mtime)" |
|
3662
|
" FROM event WHERE objid=%d", rid); |
|
3663
|
if( zDate==0 ) fossil_redirect_home(); |
|
3664
|
zNewDate = PDT("dt",zDate); |
|
3665
|
zColor = db_text("", "SELECT bgcolor" |
|
3666
|
" FROM event WHERE objid=%d", rid); |
|
3667
|
zNewColor = PDT("clr",zColor); |
|
3668
|
fPropagateColor = db_int(0, "SELECT tagtype FROM tagxref" |
|
3669
|
" WHERE rid=%d AND tagid=%d", |
|
3670
|
rid, TAG_BGCOLOR)==2; |
|
3671
|
fNewPropagateColor = P("clr")!=0 ? P("pclr")!=0 : fPropagateColor; |
|
3672
|
zNewColorFlag = P("newclr") ? " checked" : ""; |
|
3673
|
zNewTagFlag = P("newtag") ? " checked" : ""; |
|
3674
|
zNewTag = PDT("tagname",""); |
|
3675
|
zNewBrFlag = P("newbr") ? " checked" : ""; |
|
3676
|
zNewBranch = PDT("brname",""); |
|
3677
|
zBranchName = branch_of_rid(rid); |
|
3678
|
zCloseFlag = P("close") ? " checked" : ""; |
|
3679
|
zHideFlag = P("hide") ? " checked" : ""; |
|
3680
|
if( P("apply") && cgi_csrf_safe(2) ){ |
|
3681
|
Blob ctrl; |
|
3682
|
char *zNow; |
|
3683
|
|
|
3684
|
blob_zero(&ctrl); |
|
3685
|
zNow = date_in_standard_format(zChngTime ? zChngTime : "now"); |
|
3686
|
blob_appendf(&ctrl, "D %s\n", zNow); |
|
3687
|
init_newtags(); |
|
3688
|
if( zNewColorFlag[0] |
|
3689
|
&& zNewColor[0] |
|
3690
|
&& (fPropagateColor!=fNewPropagateColor |
|
3691
|
|| fossil_strcmp(zColor,zNewColor)!=0) |
|
3692
|
){ |
|
3693
|
add_color(zNewColor,fNewPropagateColor); |
|
3694
|
} |
|
3695
|
if( comment_compare(zComment,zNewComment)==0 ) add_comment(zNewComment); |
|
3696
|
if( fossil_strcmp(zDate,zNewDate)!=0 ) add_date(zNewDate); |
|
3697
|
if( fossil_strcmp(zUser,zNewUser)!=0 ) add_user(zNewUser); |
|
3698
|
db_prepare(&q, |
|
3699
|
"SELECT tag.tagid, tagname FROM tagxref, tag" |
|
3700
|
" WHERE tagxref.rid=%d AND tagtype>0 AND tagxref.tagid=tag.tagid", |
|
3701
|
rid |
|
3702
|
); |
|
3703
|
while( db_step(&q)==SQLITE_ROW ){ |
|
3704
|
int tagid = db_column_int(&q, 0); |
|
3705
|
const char *zTag = db_column_text(&q, 1); |
|
3706
|
char zLabel[30]; |
|
3707
|
sqlite3_snprintf(sizeof(zLabel), zLabel, "c%d", tagid); |
|
3708
|
if( P(zLabel) ) cancel_special(zTag); |
|
3709
|
} |
|
3710
|
db_finalize(&q); |
|
3711
|
if( zHideFlag[0] ) hide_branch(); |
|
3712
|
if( zCloseFlag[0] ) close_leaf(rid); |
|
3713
|
if( zNewTagFlag[0] && zNewTag[0] ) add_tag(zNewTag); |
|
3714
|
if( zNewBrFlag[0] && zNewBranch[0] ) change_branch(rid,zNewBranch); |
|
3715
|
apply_newtags(&ctrl, rid, zUuid, 0, 0); |
|
3716
|
cgi_redirectf("%R/ci/%S", zUuid); |
|
3717
|
} |
|
3718
|
blob_zero(&comment); |
|
3719
|
blob_append(&comment, zNewComment, -1); |
|
3720
|
zUuid[10] = 0; |
|
3721
|
style_header("Edit Check-in [%s]", zUuid); |
|
3722
|
if( P("preview") ){ |
|
3723
|
Blob suffix; |
|
3724
|
int nTag = 0; |
|
3725
|
const char *zDplyBr; /* Branch name used to determine BG color */ |
|
3726
|
const char *zMainBranch = db_main_branch(); |
|
3727
|
if( zNewBrFlag[0] && zNewBranch[0] ){ |
|
3728
|
zDplyBr = zNewBranch; |
|
3729
|
}else{ |
|
3730
|
zDplyBr = zBranchName; |
|
3731
|
} |
|
3732
|
@ <b>Preview:</b> |
|
3733
|
@ <blockquote> |
|
3734
|
@ <table border=0> |
|
3735
|
if( zNewColorFlag[0] && zNewColor && zNewColor[0] ){ |
|
3736
|
@ <tr><td style="background-color:%h(reasonable_bg_color(zNewColor,0));"> |
|
3737
|
}else if( zColor[0] ){ |
|
3738
|
@ <tr><td style="background-color:%h(reasonable_bg_color(zColor,0));"> |
|
3739
|
}else if( zDplyBr && fossil_strcmp(zDplyBr, zMainBranch)!=0 ){ |
|
3740
|
@ <tr><td style="background-color:%h(hash_color(zDplyBr));"> |
|
3741
|
}else{ |
|
3742
|
@ <tr><td> |
|
3743
|
} |
|
3744
|
@ %!W(blob_str(&comment)) |
|
3745
|
blob_zero(&suffix); |
|
3746
|
blob_appendf(&suffix, "(user: %h", zNewUser); |
|
3747
|
db_prepare(&q, "SELECT substr(tagname,5) FROM tagxref, tag" |
|
3748
|
" WHERE tagname GLOB 'sym-*' AND tagxref.rid=%d" |
|
3749
|
" AND tagtype>1 AND tag.tagid=tagxref.tagid", |
|
3750
|
rid); |
|
3751
|
while( db_step(&q)==SQLITE_ROW ){ |
|
3752
|
const char *zTag = db_column_text(&q, 0); |
|
3753
|
if( nTag==0 ){ |
|
3754
|
blob_appendf(&suffix, ", tags: %h", zTag); |
|
3755
|
}else{ |
|
3756
|
blob_appendf(&suffix, ", %h", zTag); |
|
3757
|
} |
|
3758
|
nTag++; |
|
3759
|
} |
|
3760
|
db_finalize(&q); |
|
3761
|
blob_appendf(&suffix, ")"); |
|
3762
|
@ %s(blob_str(&suffix)) |
|
3763
|
@ </td></tr></table> |
|
3764
|
if( zChngTime ){ |
|
3765
|
@ <p>The timestamp on the tag used to make the changes above |
|
3766
|
@ will be overridden as: %s(date_in_standard_format(zChngTime))</p> |
|
3767
|
} |
|
3768
|
@ </blockquote> |
|
3769
|
@ <hr> |
|
3770
|
blob_reset(&suffix); |
|
3771
|
} |
|
3772
|
@ <p>Make changes to attributes of check-in |
|
3773
|
@ [%z(href("%R/ci/%!S",zUuid))%s(zUuid)</a>]:</p> |
|
3774
|
form_begin(0, "%R/ci_edit"); |
|
3775
|
@ <div><input type="hidden" name="r" value="%s(zUuid)"> |
|
3776
|
@ <table border="0" cellspacing="10"> |
|
3777
|
|
|
3778
|
@ <tr><th align="right" valign="top">User:</th> |
|
3779
|
@ <td valign="top"> |
|
3780
|
@ <input type="text" name="u" size="20" value="%h(zNewUser)"> |
|
3781
|
@ </td></tr> |
|
3782
|
|
|
3783
|
@ <tr><th align="right" valign="top">Comment:</th> |
|
3784
|
@ <td valign="top"> |
|
3785
|
@ <textarea name="c" rows="10" cols="80">%h(zNewComment)</textarea> |
|
3786
|
@ </td></tr> |
|
3787
|
|
|
3788
|
@ <tr><th align="right" valign="top">Check-in Time:</th> |
|
3789
|
@ <td valign="top"> |
|
3790
|
@ <input type="text" name="dt" size="20" value="%h(zNewDate)"> |
|
3791
|
@ </td></tr> |
|
3792
|
|
|
3793
|
if( zChngTime ){ |
|
3794
|
@ <tr><th align="right" valign="top">Timestamp of this change:</th> |
|
3795
|
@ <td valign="top"> |
|
3796
|
@ <input type="text" name="chngtime" size="20" value="%h(zChngTime)"> |
|
3797
|
@ </td></tr> |
|
3798
|
} |
|
3799
|
|
|
3800
|
@ <tr><th align="right" valign="top">Background Color:</th> |
|
3801
|
@ <td valign="top"> |
|
3802
|
@ <div><label><input type='checkbox' name='newclr'%s(zNewColorFlag)> |
|
3803
|
@ Change background color: \ |
|
3804
|
@ <input type='color' name='clr'\ |
|
3805
|
@ value='%s(zNewColor[0]?zNewColor:"#808080")'></label></div> |
|
3806
|
@ <div><label> |
|
3807
|
if( fNewPropagateColor ){ |
|
3808
|
@ <input type="checkbox" name="pclr" checked="checked"> |
|
3809
|
}else{ |
|
3810
|
@ <input type="checkbox" name="pclr"> |
|
3811
|
} |
|
3812
|
@ Propagate color to descendants</label></div> |
|
3813
|
@ <div class='font-size-80'>Be aware that fixed background |
|
3814
|
@ colors will not interact well with all available skins. |
|
3815
|
@ It is recommended that Fossil be allowed to select these |
|
3816
|
@ colors automatically so that it can take the skin's |
|
3817
|
@ preferences into account.</div> |
|
3818
|
@ </td></tr> |
|
3819
|
|
|
3820
|
@ <tr><th align="right" valign="top">Tags:</th> |
|
3821
|
@ <td valign="top"> |
|
3822
|
@ <label><input type="checkbox" id="newtag" name="newtag"%s(zNewTagFlag)> |
|
3823
|
@ Add the following new tag name to this check-in:</label> |
|
3824
|
@ <input size="15" name="tagname" id="tagname" value="%h(zNewTag)"> |
|
3825
|
db_prepare(&q, |
|
3826
|
"SELECT tag.tagid, tagname, tagxref.value FROM tagxref, tag" |
|
3827
|
" WHERE tagxref.rid=%d AND tagtype>0 AND tagxref.tagid=tag.tagid" |
|
3828
|
" ORDER BY CASE WHEN tagname GLOB 'sym-*' THEN substr(tagname,5)" |
|
3829
|
" ELSE tagname END /*sort*/", |
|
3830
|
rid |
|
3831
|
); |
|
3832
|
while( db_step(&q)==SQLITE_ROW ){ |
|
3833
|
int tagid = db_column_int(&q, 0); |
|
3834
|
const char *zTagName = db_column_text(&q, 1); |
|
3835
|
int isSpecialTag = fossil_strncmp(zTagName, "sym-", 4)!=0; |
|
3836
|
char zLabel[30]; |
|
3837
|
|
|
3838
|
if( tagid == TAG_CLOSED ){ |
|
3839
|
fHasClosed = 1; |
|
3840
|
}else if( (tagid == TAG_COMMENT) || (tagid == TAG_BRANCH) ){ |
|
3841
|
continue; |
|
3842
|
}else if( tagid==TAG_HIDDEN ){ |
|
3843
|
fHasHidden = 1; |
|
3844
|
}else if( !isSpecialTag && zTagName && |
|
3845
|
fossil_strcmp(&zTagName[4], zBranchName)==0){ |
|
3846
|
continue; |
|
3847
|
} |
|
3848
|
sqlite3_snprintf(sizeof(zLabel), zLabel, "c%d", tagid); |
|
3849
|
@ <br><label> |
|
3850
|
if( P(zLabel) ){ |
|
3851
|
@ <input type="checkbox" name="c%d(tagid)" checked="checked"> |
|
3852
|
}else{ |
|
3853
|
@ <input type="checkbox" name="c%d(tagid)"> |
|
3854
|
} |
|
3855
|
if( isSpecialTag ){ |
|
3856
|
@ Cancel special tag <b>%h(zTagName)</b></label> |
|
3857
|
}else{ |
|
3858
|
@ Cancel tag <b>%h(&zTagName[4])</b></label> |
|
3859
|
} |
|
3860
|
} |
|
3861
|
db_finalize(&q); |
|
3862
|
@ </td></tr> |
|
3863
|
|
|
3864
|
if( !zBranchName ){ |
|
3865
|
zBranchName = fossil_strdup(db_main_branch()); |
|
3866
|
} |
|
3867
|
if( !zNewBranch || !zNewBranch[0]){ |
|
3868
|
zNewBranch = zBranchName; |
|
3869
|
} |
|
3870
|
@ <tr><th align="right" valign="top">Branching:</th> |
|
3871
|
@ <td valign="top"> |
|
3872
|
@ <label><input id="newbr" type="checkbox" name="newbr" \ |
|
3873
|
@ data-branch='%h(zBranchName)'%s(zNewBrFlag)> |
|
3874
|
@ Starting from this check-in, rename the branch to:</label> |
|
3875
|
@ <input id="brname" type="text" style="width:15;" name="brname" \ |
|
3876
|
@ value="%h(zNewBranch)"></td></tr> |
|
3877
|
if( !fHasHidden ){ |
|
3878
|
@ <tr><th align="right" valign="top">Branch Hiding:</th> |
|
3879
|
@ <td valign="top"> |
|
3880
|
@ <label><input type="checkbox" id="hidebr" name="hide"%s(zHideFlag)> |
|
3881
|
@ Hide branch |
|
3882
|
@ <span style="font-weight:bold" id="hbranch">%h(zBranchName)</span> |
|
3883
|
@ from the timeline starting from this check-in</label> |
|
3884
|
@ </td></tr> |
|
3885
|
} |
|
3886
|
if( !fHasClosed ){ |
|
3887
|
if( is_a_leaf(rid) ){ |
|
3888
|
@ <tr><th align="right" valign="top">Leaf Closure:</th> |
|
3889
|
@ <td valign="top"> |
|
3890
|
@ <label><input type="checkbox" name="close"%s(zCloseFlag)> |
|
3891
|
@ Mark this leaf as "closed" so that it no longer appears on the |
|
3892
|
@ "leaves" page and is no longer labeled as a "<b>Leaf</b>"</label> |
|
3893
|
@ </td></tr> |
|
3894
|
}else if( zBranchName ){ |
|
3895
|
@ <tr><th align="right" valign="top">Branch Closure:</th> |
|
3896
|
@ <td valign="top"> |
|
3897
|
@ <label><input type="checkbox" name="close"%s(zCloseFlag)> |
|
3898
|
@ Mark branch |
|
3899
|
@ <span style="font-weight:bold" id="cbranch">%h(zBranchName)</span> |
|
3900
|
@ as "closed".</label> |
|
3901
|
@ </td></tr> |
|
3902
|
} |
|
3903
|
} |
|
3904
|
if( zBranchName ) fossil_free(zBranchName); |
|
3905
|
|
|
3906
|
|
|
3907
|
@ <tr><td colspan="2"> |
|
3908
|
@ <input type="submit" name="cancel" value="Cancel"> |
|
3909
|
@ <input type="submit" name="preview" value="Preview"> |
|
3910
|
if( P("preview") ){ |
|
3911
|
@ <input type="submit" name="apply" value="Apply Changes"> |
|
3912
|
} |
|
3913
|
@ </td></tr> |
|
3914
|
@ </table> |
|
3915
|
@ </div></form> |
|
3916
|
builtin_request_js("ci_edit.js"); |
|
3917
|
style_finish_page(); |
|
3918
|
} |
|
3919
|
|
|
3920
|
/* |
|
3921
|
** Prepare an ammended commit comment. Let the user modify it using the |
|
3922
|
** editor specified in the global_config table or either |
|
3923
|
** the VISUAL or EDITOR environment variable. |
|
3924
|
** |
|
3925
|
** Store the final commit comment in pComment. pComment is assumed |
|
3926
|
** to be uninitialized - any prior content is overwritten. |
|
3927
|
** |
|
3928
|
** Use zInit to initialize the check-in comment so that the user does |
|
3929
|
** not have to retype. |
|
3930
|
*/ |
|
3931
|
static void prepare_amend_comment( |
|
3932
|
Blob *pComment, |
|
3933
|
const char *zInit, |
|
3934
|
const char *zUuid |
|
3935
|
){ |
|
3936
|
Blob prompt; |
|
3937
|
#if defined(_WIN32) || defined(__CYGWIN__) |
|
3938
|
int bomSize; |
|
3939
|
const unsigned char *bom = get_utf8_bom(&bomSize); |
|
3940
|
blob_init(&prompt, (const char *) bom, bomSize); |
|
3941
|
if( zInit && zInit[0]){ |
|
3942
|
blob_append(&prompt, zInit, -1); |
|
3943
|
} |
|
3944
|
#else |
|
3945
|
blob_init(&prompt, zInit, -1); |
|
3946
|
#endif |
|
3947
|
blob_append(&prompt, "\n# Enter a new comment for check-in ", -1); |
|
3948
|
if( zUuid && zUuid[0] ){ |
|
3949
|
blob_append(&prompt, zUuid, -1); |
|
3950
|
} |
|
3951
|
blob_append(&prompt, ".\n# Lines beginning with a # are ignored.\n", -1); |
|
3952
|
prompt_for_user_comment(pComment, &prompt); |
|
3953
|
blob_reset(&prompt); |
|
3954
|
} |
|
3955
|
|
|
3956
|
#define AMEND_USAGE_STMT "HASH OPTION ?OPTION ...?" |
|
3957
|
/* |
|
3958
|
** COMMAND: amend |
|
3959
|
** |
|
3960
|
** Usage: %fossil amend HASH OPTION ?OPTION ...? |
|
3961
|
** |
|
3962
|
** Amend the tags on check-in HASH to change how it displays in the timeline. |
|
3963
|
** |
|
3964
|
** Options: |
|
3965
|
** --author USER Make USER the author for check-in |
|
3966
|
** --bgcolor COLOR Apply COLOR to this check-in |
|
3967
|
** --branch NAME Rename branch of check-in to NAME |
|
3968
|
** --branchcolor COLOR Apply and propagate COLOR to the branch |
|
3969
|
** --cancel TAG Cancel TAG from this check-in |
|
3970
|
** --close Mark this "leaf" as closed |
|
3971
|
** --date DATETIME Make DATETIME the check-in time |
|
3972
|
** --date-override DATETIME Set the change time on the control artifact |
|
3973
|
** -e|--edit-comment Launch editor to revise comment |
|
3974
|
** --editor NAME Text editor to use for check-in comment |
|
3975
|
** --hide Hide branch starting from this check-in |
|
3976
|
** -m|--comment COMMENT Make COMMENT the check-in comment |
|
3977
|
** -M|--message-file FILE Read the amended comment from FILE |
|
3978
|
** -n|--dry-run Print control artifact, but make no changes |
|
3979
|
** --no-verify-comment Do not validate the check-in comment |
|
3980
|
** --tag TAG Add new TAG to this check-in |
|
3981
|
** --user-override USER Set the user name on the control artifact |
|
3982
|
** |
|
3983
|
** DATETIME may be "now" or "YYYY-MM-DDTHH:MM:SS.SSS". If in |
|
3984
|
** year-month-day form, it may be truncated, the "T" may be replaced by |
|
3985
|
** a space, and it may also name a timezone offset from UTC as "-HH:MM" |
|
3986
|
** (westward) or "+HH:MM" (eastward). Either no timezone suffix or "Z" |
|
3987
|
** means UTC. |
|
3988
|
*/ |
|
3989
|
void ci_amend_cmd(void){ |
|
3990
|
int rid; |
|
3991
|
const char *zComment; /* Current comment on the check-in */ |
|
3992
|
const char *zNewComment; /* Revised check-in comment */ |
|
3993
|
const char *zComFile; /* Filename from which to read comment */ |
|
3994
|
const char *zUser; /* Current user for the check-in */ |
|
3995
|
const char *zNewUser; /* Revised user */ |
|
3996
|
const char *zDate; /* Current date of the check-in */ |
|
3997
|
const char *zNewDate; /* Revised check-in date */ |
|
3998
|
const char *zColor; |
|
3999
|
const char *zNewColor; |
|
4000
|
const char *zNewBrColor; |
|
4001
|
const char *zNewBranch; |
|
4002
|
const char **pzNewTags = 0; |
|
4003
|
const char **pzCancelTags = 0; |
|
4004
|
int fClose; /* True if leaf should be closed */ |
|
4005
|
int fHide; /* True if branch should be hidden */ |
|
4006
|
int fPropagateColor; /* True if color propagates before amend */ |
|
4007
|
int fNewPropagateColor = 0; /* True if color propagates after amend */ |
|
4008
|
int fHasHidden = 0; /* True if hidden tag already set */ |
|
4009
|
int fHasClosed = 0; /* True if closed tag already set */ |
|
4010
|
int fEditComment; /* True if editor to be used for comment */ |
|
4011
|
int fDryRun; /* Print control artifact, make no changes */ |
|
4012
|
int noVerifyCom = 0; /* Allow suspicious check-in comments */ |
|
4013
|
const char *zChngTime; /* The change time on the control artifact */ |
|
4014
|
const char *zUserOvrd; /* The user name on the control artifact */ |
|
4015
|
const char *zUuid; |
|
4016
|
Blob ctrl; |
|
4017
|
Blob comment; |
|
4018
|
char *zNow; |
|
4019
|
int nTags, nCancels; |
|
4020
|
int i; |
|
4021
|
Stmt q; |
|
4022
|
int ckComFlgs; /* Flags passed to verify_comment() */ |
|
4023
|
|
|
4024
|
|
|
4025
|
fEditComment = find_option("edit-comment","e",0)!=0; |
|
4026
|
zNewComment = find_option("comment","m",1); |
|
4027
|
zComFile = find_option("message-file","M",1); |
|
4028
|
zNewBranch = find_option("branch",0,1); |
|
4029
|
zNewColor = find_option("bgcolor",0,1); |
|
4030
|
zNewBrColor = find_option("branchcolor",0,1); |
|
4031
|
if( zNewBrColor ){ |
|
4032
|
zNewColor = zNewBrColor; |
|
4033
|
fNewPropagateColor = 1; |
|
4034
|
} |
|
4035
|
zNewDate = find_option("date",0,1); |
|
4036
|
zNewUser = find_option("author",0,1); |
|
4037
|
pzNewTags = find_repeatable_option("tag",0,&nTags); |
|
4038
|
pzCancelTags = find_repeatable_option("cancel",0,&nCancels); |
|
4039
|
fClose = find_option("close",0,0)!=0; |
|
4040
|
fHide = find_option("hide",0,0)!=0; |
|
4041
|
fDryRun = find_option("dry-run","n",0)!=0; |
|
4042
|
zChngTime = find_option("date-override",0,1); |
|
4043
|
if( zChngTime==0 ) zChngTime = find_option("chngtime",0,1); |
|
4044
|
zUserOvrd = find_option("user-override",0,1); |
|
4045
|
noVerifyCom = find_option("no-verify-comment",0,0)!=0; |
|
4046
|
db_find_and_open_repository(0,0); |
|
4047
|
user_select(); |
|
4048
|
(void)fossil_text_editor(); |
|
4049
|
verify_all_options(); |
|
4050
|
if( g.argc<3 || g.argc>=4 ) usage(AMEND_USAGE_STMT); |
|
4051
|
rid = name_to_typed_rid(g.argv[2], "ci"); |
|
4052
|
if( rid==0 && !is_a_version(rid) ) fossil_fatal("no such check-in"); |
|
4053
|
zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); |
|
4054
|
if( zUuid==0 ) fossil_fatal("Unable to find artifact hash"); |
|
4055
|
zComment = db_text(0, "SELECT coalesce(ecomment,comment)" |
|
4056
|
" FROM event WHERE objid=%d", rid); |
|
4057
|
zUser = db_text(0, "SELECT coalesce(euser,user)" |
|
4058
|
" FROM event WHERE objid=%d", rid); |
|
4059
|
zDate = db_text(0, "SELECT datetime(mtime)" |
|
4060
|
" FROM event WHERE objid=%d", rid); |
|
4061
|
zColor = db_text("", "SELECT bgcolor" |
|
4062
|
" FROM event WHERE objid=%d", rid); |
|
4063
|
fPropagateColor = db_int(0, "SELECT tagtype FROM tagxref" |
|
4064
|
" WHERE rid=%d AND tagid=%d", |
|
4065
|
rid, TAG_BGCOLOR)==2; |
|
4066
|
fNewPropagateColor = zNewColor && zNewColor[0] |
|
4067
|
? fNewPropagateColor : fPropagateColor; |
|
4068
|
db_prepare(&q, |
|
4069
|
"SELECT tag.tagid FROM tagxref, tag" |
|
4070
|
" WHERE tagxref.rid=%d AND tagtype>0 AND tagxref.tagid=tag.tagid", |
|
4071
|
rid |
|
4072
|
); |
|
4073
|
while( db_step(&q)==SQLITE_ROW ){ |
|
4074
|
int tagid = db_column_int(&q, 0); |
|
4075
|
|
|
4076
|
if( tagid == TAG_CLOSED ){ |
|
4077
|
fHasClosed = 1; |
|
4078
|
}else if( tagid==TAG_HIDDEN ){ |
|
4079
|
fHasHidden = 1; |
|
4080
|
}else{ |
|
4081
|
continue; |
|
4082
|
} |
|
4083
|
} |
|
4084
|
db_finalize(&q); |
|
4085
|
blob_zero(&ctrl); |
|
4086
|
zNow = date_in_standard_format(zChngTime && zChngTime[0] ? zChngTime : "now"); |
|
4087
|
blob_appendf(&ctrl, "D %s\n", zNow); |
|
4088
|
init_newtags(); |
|
4089
|
if( zNewColor && zNewColor[0] |
|
4090
|
&& (fPropagateColor!=fNewPropagateColor |
|
4091
|
|| fossil_strcmp(zColor,zNewColor)!=0) |
|
4092
|
){ |
|
4093
|
add_color( |
|
4094
|
mprintf("%s%s", (zNewColor[0]!='#' && |
|
4095
|
validate16(zNewColor,strlen(zNewColor)) && |
|
4096
|
(strlen(zNewColor)==6 || strlen(zNewColor)==3)) ? "#" : "", |
|
4097
|
zNewColor |
|
4098
|
), |
|
4099
|
fNewPropagateColor |
|
4100
|
); |
|
4101
|
} |
|
4102
|
if( (zNewColor!=0 && zNewColor[0]==0) && (zColor && zColor[0] ) ){ |
|
4103
|
cancel_color(); |
|
4104
|
} |
|
4105
|
if( fEditComment || zNewComment || zComFile ){ |
|
4106
|
blob_init(&comment, 0, 0); |
|
4107
|
|
|
4108
|
/* Figure out how much comment verification is requested */ |
|
4109
|
if( noVerifyCom ){ |
|
4110
|
ckComFlgs = 0; |
|
4111
|
}else{ |
|
4112
|
const char *zVerComs = db_get("verify-comments","on"); |
|
4113
|
if( is_false(zVerComs) ){ |
|
4114
|
ckComFlgs = 0; |
|
4115
|
}else if( strcmp(zVerComs,"preview")==0 ){ |
|
4116
|
ckComFlgs = COMCK_PREVIEW | COMCK_MARKUP; |
|
4117
|
}else{ |
|
4118
|
ckComFlgs = COMCK_MARKUP; |
|
4119
|
} |
|
4120
|
} |
|
4121
|
if( fEditComment ){ |
|
4122
|
prepare_amend_comment(&comment, zComment, zUuid); |
|
4123
|
}else if( zComFile ){ |
|
4124
|
blob_read_from_file(&comment, zComFile, ExtFILE); |
|
4125
|
blob_to_utf8_no_bom(&comment, 1); |
|
4126
|
}else if( zNewComment ){ |
|
4127
|
blob_init(&comment, zNewComment, -1); |
|
4128
|
} |
|
4129
|
if( blob_size(&comment)>0 |
|
4130
|
&& comment_compare(zComment, blob_str(&comment))==0 |
|
4131
|
){ |
|
4132
|
int rc; |
|
4133
|
while( (rc = verify_comment(&comment, ckComFlgs))!=0 ){ |
|
4134
|
char cReply; |
|
4135
|
Blob ans; |
|
4136
|
if( !fEditComment ){ |
|
4137
|
fossil_fatal("Amend aborted; " |
|
4138
|
"use --no-verify-comment to override"); |
|
4139
|
} |
|
4140
|
if( rc==COMCK_PREVIEW ){ |
|
4141
|
prompt_user("Continue, abort, or edit (C/a/e)? ", &ans); |
|
4142
|
}else{ |
|
4143
|
prompt_user("Edit, abort, or continue (E/a/c)? ", &ans); |
|
4144
|
} |
|
4145
|
cReply = blob_str(&ans)[0]; |
|
4146
|
cReply = fossil_tolower(cReply); |
|
4147
|
blob_reset(&ans); |
|
4148
|
if( cReply=='a' ){ |
|
4149
|
fossil_fatal("Amend aborted."); |
|
4150
|
} |
|
4151
|
if( cReply=='e' || (cReply!='c' && rc!=COMCK_PREVIEW) ){ |
|
4152
|
char *zPrior = blob_materialize(&comment); |
|
4153
|
blob_init(&comment, 0, 0); |
|
4154
|
prepare_amend_comment(&comment, zPrior, zUuid); |
|
4155
|
fossil_free(zPrior); |
|
4156
|
continue; |
|
4157
|
}else{ |
|
4158
|
break; |
|
4159
|
} |
|
4160
|
} |
|
4161
|
} |
|
4162
|
add_comment(blob_str(&comment)); |
|
4163
|
} |
|
4164
|
if( zNewDate && zNewDate[0] && fossil_strcmp(zDate,zNewDate)!=0 ){ |
|
4165
|
if( is_datetime(zNewDate) ){ |
|
4166
|
add_date(zNewDate); |
|
4167
|
}else{ |
|
4168
|
fossil_fatal("Unsupported date format, use YYYY-MM-DD HH:MM:SS"); |
|
4169
|
} |
|
4170
|
} |
|
4171
|
if( zNewUser && zNewUser[0] && fossil_strcmp(zUser,zNewUser)!=0 ){ |
|
4172
|
add_user(zNewUser); |
|
4173
|
} |
|
4174
|
if( pzNewTags!=0 ){ |
|
4175
|
for(i=0; i<nTags; i++){ |
|
4176
|
if( pzNewTags[i] && pzNewTags[i][0] ) add_tag(pzNewTags[i]); |
|
4177
|
} |
|
4178
|
fossil_free((void *)pzNewTags); |
|
4179
|
} |
|
4180
|
if( pzCancelTags!=0 ){ |
|
4181
|
for(i=0; i<nCancels; i++){ |
|
4182
|
if( pzCancelTags[i] && pzCancelTags[i][0] ) |
|
4183
|
cancel_tag(rid,pzCancelTags[i]); |
|
4184
|
} |
|
4185
|
fossil_free((void *)pzCancelTags); |
|
4186
|
} |
|
4187
|
if( fHide && !fHasHidden ) hide_branch(); |
|
4188
|
if( fClose && !fHasClosed ) close_leaf(rid); |
|
4189
|
if( zNewBranch && zNewBranch[0] ) change_branch(rid,zNewBranch); |
|
4190
|
apply_newtags(&ctrl, rid, zUuid, zUserOvrd, fDryRun); |
|
4191
|
if( fDryRun==0 ){ |
|
4192
|
show_common_info(rid, "hash:", 1, 0); |
|
4193
|
} |
|
4194
|
if( g.localOpen ){ |
|
4195
|
manifest_to_disk(rid); |
|
4196
|
} |
|
4197
|
} |
|
4198
|
|
|
4199
|
|
|
4200
|
/* |
|
4201
|
** COMMAND: test-symlink-list |
|
4202
|
** |
|
4203
|
** Show all symlinks that have been checked into a Fossil repository. |
|
4204
|
** |
|
4205
|
** This command does a linear scan through all check-ins and so might take |
|
4206
|
** several seconds on a large repository. |
|
4207
|
*/ |
|
4208
|
void test_symlink_list_cmd(void){ |
|
4209
|
Stmt q; |
|
4210
|
db_find_and_open_repository(0,0); |
|
4211
|
add_content_sql_commands(g.db); |
|
4212
|
db_prepare(&q, |
|
4213
|
"SELECT min(date(e.mtime))," |
|
4214
|
" b.uuid," |
|
4215
|
" f.filename," |
|
4216
|
" content(f.uuid)" |
|
4217
|
" FROM event AS e, blob AS b, files_of_checkin(b.uuid) AS f" |
|
4218
|
" WHERE e.type='ci'" |
|
4219
|
" AND b.rid=e.objid" |
|
4220
|
" AND f.perm LIKE '%%l%%'" |
|
4221
|
" GROUP BY 3, 4" |
|
4222
|
" ORDER BY 1 DESC" |
|
4223
|
); |
|
4224
|
while( db_step(&q)==SQLITE_ROW ){ |
|
4225
|
fossil_print("%s %.16s %s -> %s\n", |
|
4226
|
db_column_text(&q,0), |
|
4227
|
db_column_text(&q,1), |
|
4228
|
db_column_text(&q,2), |
|
4229
|
db_column_text(&q,3)); |
|
4230
|
} |
|
4231
|
db_finalize(&q); |
|
4232
|
} |
|
4233
|
|
|
4234
|
#if INTERFACE |
|
4235
|
/* |
|
4236
|
** Description of a check-in relative to an earlier, tagged check-in. |
|
4237
|
*/ |
|
4238
|
typedef struct CommitDescr { |
|
4239
|
char *zRelTagname; /* Tag name on the relative check-in */ |
|
4240
|
int nCommitsSince; /* Number of commits since then */ |
|
4241
|
char *zCommitHash; /* Hash of the described check-in */ |
|
4242
|
int isDirty; /* Working directory has uncommitted changes */ |
|
4243
|
} CommitDescr; |
|
4244
|
#endif |
|
4245
|
|
|
4246
|
/* |
|
4247
|
** Describe the check-in given by 'zName', and possibly matching 'matchGlob', |
|
4248
|
** relative to an earlier, tagged check-in. Use 'descr' for the output. |
|
4249
|
** |
|
4250
|
** Finds the closest ancestor (ignoring merge-ins) that has a non-propagating |
|
4251
|
** label tag and the number of steps backwards that we had to search in |
|
4252
|
** order to find that tag. Tags applied to more than one check-in are ignored. |
|
4253
|
** |
|
4254
|
** Return values: |
|
4255
|
** 0: ok |
|
4256
|
** -1: zName does not resolve to a commit |
|
4257
|
** -2: zName resolves to more than a commit |
|
4258
|
** -3: no ancestor commit with a fitting non-propagating tag found |
|
4259
|
*/ |
|
4260
|
int describe_commit( |
|
4261
|
const char *zName, /* Name of the commit to be described */ |
|
4262
|
const char *matchGlob, /* Glob pattern for the tag */ |
|
4263
|
CommitDescr *descr /* Write the description here */ |
|
4264
|
){ |
|
4265
|
int rid; /* rid for zName */ |
|
4266
|
const char *zUuid; /* Hash of rid */ |
|
4267
|
int nRet = 0; /* Value to be returned */ |
|
4268
|
Stmt q; /* Query for tagged ancestors */ |
|
4269
|
|
|
4270
|
rid = symbolic_name_to_rid(zName, "ci"); /* only commits */ |
|
4271
|
|
|
4272
|
if( rid<=0 ){ |
|
4273
|
/* Commit does not exist or is ambiguous */ |
|
4274
|
descr->zRelTagname = mprintf(""); |
|
4275
|
descr->nCommitsSince = -1; |
|
4276
|
descr->zCommitHash = mprintf(""); |
|
4277
|
descr->isDirty = -1; |
|
4278
|
return (rid-1); |
|
4279
|
} |
|
4280
|
|
|
4281
|
zUuid = rid_to_uuid(rid); |
|
4282
|
descr->zCommitHash = fossil_strdup(zUuid); |
|
4283
|
descr->isDirty = unsaved_changes(0); |
|
4284
|
|
|
4285
|
db_multi_exec( |
|
4286
|
"DROP TABLE IF EXISTS temp.singletonTag;" |
|
4287
|
"CREATE TEMP TABLE singletonTag(" |
|
4288
|
" rid INT," |
|
4289
|
" tagname TEXT," |
|
4290
|
" PRIMARY KEY (rid,tagname)" |
|
4291
|
") WITHOUT ROWID;" |
|
4292
|
"INSERT OR IGNORE INTO singletonTag(rid, tagname)" |
|
4293
|
" SELECT min(rid)," |
|
4294
|
" substr(tagname,5)" |
|
4295
|
" FROM tag, tagxref" |
|
4296
|
" WHERE tag.tagid=tagxref.tagid" |
|
4297
|
" AND tagxref.tagtype=1" |
|
4298
|
" AND tagname GLOB 'sym-%q'" |
|
4299
|
" GROUP BY tagname" |
|
4300
|
" HAVING count(*)==1;", |
|
4301
|
(matchGlob ? matchGlob : "*") |
|
4302
|
); |
|
4303
|
|
|
4304
|
db_prepare(&q, |
|
4305
|
"WITH RECURSIVE" |
|
4306
|
" ancestor(rid,mtime,tagname,n) AS (" |
|
4307
|
" SELECT %d, event.mtime, singletonTag.tagname, 0 " |
|
4308
|
" FROM event" |
|
4309
|
" LEFT JOIN singletonTag ON singletonTag.rid=event.objid" |
|
4310
|
" WHERE event.objid=%d" |
|
4311
|
" UNION ALL" |
|
4312
|
" SELECT plink.pid, event.mtime, singletonTag.tagname, n+1" |
|
4313
|
" FROM ancestor, plink, event" |
|
4314
|
" LEFT JOIN singletonTag ON singletonTag.rid=plink.pid" |
|
4315
|
" WHERE plink.cid=ancestor.rid" |
|
4316
|
" AND plink.isprim=1" |
|
4317
|
" AND event.objid=plink.pid" |
|
4318
|
" AND ancestor.tagname IS NULL" |
|
4319
|
" ORDER BY mtime DESC" |
|
4320
|
" LIMIT 100000" |
|
4321
|
" )" |
|
4322
|
"SELECT tagname, n" |
|
4323
|
" FROM ancestor" |
|
4324
|
" WHERE tagname IS NOT NULL" |
|
4325
|
" ORDER BY n LIMIT 1;", |
|
4326
|
rid, rid |
|
4327
|
); |
|
4328
|
|
|
4329
|
if( db_step(&q)==SQLITE_ROW ){ |
|
4330
|
const char *lastTag = db_column_text(&q, 0); |
|
4331
|
descr->zRelTagname = fossil_strdup(lastTag); |
|
4332
|
descr->nCommitsSince = db_column_int(&q, 1); |
|
4333
|
nRet = 0; |
|
4334
|
}else{ |
|
4335
|
/* no ancestor commit with a fitting singleton tag found */ |
|
4336
|
descr->zRelTagname = mprintf(""); |
|
4337
|
descr->nCommitsSince = -1; |
|
4338
|
nRet = -3; |
|
4339
|
} |
|
4340
|
|
|
4341
|
db_finalize(&q); |
|
4342
|
return nRet; |
|
4343
|
} |
|
4344
|
|
|
4345
|
/* |
|
4346
|
** COMMAND: describe |
|
4347
|
** |
|
4348
|
** Usage: %fossil describe ?VERSION? ?OPTIONS? |
|
4349
|
** |
|
4350
|
** Provide a description of the given VERSION by showing a non-propagating |
|
4351
|
** tag of the youngest tagged ancestor, followed by the number of commits |
|
4352
|
** since that, and the short hash of VERSION. Only tags applied to a single |
|
4353
|
** check-in are considered. |
|
4354
|
** |
|
4355
|
** If no VERSION is provided, describe the currently checked-out version. |
|
4356
|
** |
|
4357
|
** If VERSION and the found ancestor refer to the same commit, the last two |
|
4358
|
** components are omitted, unless --long is provided. When no fitting tagged |
|
4359
|
** ancestor is found, show only the short hash of VERSION. |
|
4360
|
** |
|
4361
|
** Options: |
|
4362
|
** --digits Display so many hex digits of the hash |
|
4363
|
** (default: the larger of 6 and the 'hash-digit' setting) |
|
4364
|
** -d|--dirty Show whether there are changes to be committed |
|
4365
|
** --long Always show all three components |
|
4366
|
** --match GLOB Consider only non-propagating tags matching GLOB |
|
4367
|
*/ |
|
4368
|
void describe_cmd(void){ |
|
4369
|
const char *zName; |
|
4370
|
const char *zMatchGlob; |
|
4371
|
const char *zDigits; |
|
4372
|
int nDigits; |
|
4373
|
int bDirtyFlag = 0; |
|
4374
|
int bLongFlag = 0; |
|
4375
|
CommitDescr descr; |
|
4376
|
|
|
4377
|
db_find_and_open_repository(0,0); |
|
4378
|
bDirtyFlag = find_option("dirty","d",0)!=0; |
|
4379
|
bLongFlag = find_option("long","",0)!=0; |
|
4380
|
zMatchGlob = find_option("match", 0, 1); |
|
4381
|
zDigits = find_option("digits", 0, 1); |
|
4382
|
|
|
4383
|
if ( !zDigits || ((nDigits=atoi(zDigits))==0) ){ |
|
4384
|
nDigits = hash_digits(0); |
|
4385
|
} |
|
4386
|
|
|
4387
|
/* We should be done with options.. */ |
|
4388
|
verify_all_options(); |
|
4389
|
if( g.argc<3 ){ |
|
4390
|
zName = "current"; |
|
4391
|
}else{ |
|
4392
|
zName = g.argv[2]; |
|
4393
|
} |
|
4394
|
|
|
4395
|
if( bDirtyFlag ){ |
|
4396
|
if ( g.argc>=3 ) fossil_fatal("cannot use --dirty with specific check-in"); |
|
4397
|
} |
|
4398
|
|
|
4399
|
switch( describe_commit(zName, zMatchGlob, &descr) ){ |
|
4400
|
case -1: |
|
4401
|
fossil_fatal("commit %s does not exist", zName); |
|
4402
|
break; |
|
4403
|
case -2: |
|
4404
|
fossil_fatal("commit %s is ambiguous", zName); |
|
4405
|
break; |
|
4406
|
case -3: |
|
4407
|
fossil_print("%.*s%s\n", nDigits, descr.zCommitHash, |
|
4408
|
bDirtyFlag ? (descr.isDirty ? "-dirty" : "") : ""); |
|
4409
|
break; |
|
4410
|
case 0: |
|
4411
|
if( descr.nCommitsSince==0 && !bLongFlag ){ |
|
4412
|
fossil_print("%s%s\n", descr.zRelTagname, |
|
4413
|
bDirtyFlag ? (descr.isDirty ? "-dirty" : "") : ""); |
|
4414
|
}else{ |
|
4415
|
fossil_print("%s-%d-%.*s%s\n", descr.zRelTagname, |
|
4416
|
descr.nCommitsSince, nDigits, descr.zCommitHash, |
|
4417
|
bDirtyFlag ? (descr.isDirty ? "-dirty" : "") : ""); |
|
4418
|
} |
|
4419
|
break; |
|
4420
|
default: |
|
4421
|
fossil_fatal("cannot describe commit"); |
|
4422
|
break; |
|
4423
|
} |
|
4424
|
} |
|
4425
|
|