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