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       @ &nbsp;&nbsp;
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&nbsp;&amp;&nbsp;Date:</th><td>
774     hyperlink_to_user(zUser,zDate," on ");
775     hyperlink_to_date(zDate, "</td></tr>");
776     if( zEComment ){
777       @ <tr><th>Original&nbsp;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&nbsp;User&nbsp;&amp;&nbsp;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&nbsp;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&nbsp;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&nbsp;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&nbsp;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&nbsp;Diffs</a>
884   }
885   if( diffType!=1 ){
886     @ %z(chref("button","%R/%s/%T?diff=1%s",zPage,zName,zW))\
887     @ Unified&nbsp;Diffs</a>
888   }
889   if( diffType!=2 ){
890     @ %z(chref("button","%R/%s/%T?diff=2%s",zPage,zName,zW))\
891     @ Side-by-Side&nbsp;Diffs</a>
892   }
893   if( diffType!=0 ){
894     if( *zW ){
895       @ %z(chref("button","%R/%s/%T",zPage,zName))
896       @ Show&nbsp;Whitespace&nbsp;Changes</a>
897     }else{
898       @ %z(chref("button","%R/%s/%T?w",zPage,zName))
899       @ Ignore&nbsp;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&nbsp;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&nbsp;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&nbsp;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       @ &mdash; part of check-in
1452       hyperlink_to_version(zVers);
1453     }else{
1454       @ &mdash; 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     @ &mdash; %!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&nbsp;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&nbsp;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&nbsp;&amp;&nbsp;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&nbsp;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