Fossil SCM

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

Keyboard Shortcuts

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