Fossil SCM

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

Keyboard Shortcuts

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