1 /*
2 ** Copyright (c) 2009 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 "finfo" command.
19 */
20 #include "config.h"
21 #include "finfo.h"
22 
23 /*
24 ** COMMAND: finfo
25 **
26 ** Usage: %fossil finfo ?OPTIONS? FILENAME
27 **
28 ** Print the complete change history for a single file going backwards
29 ** in time.  The default mode is -l.
30 **
31 ** For the -l|--log mode: If "-b|--brief" is specified one line per revision
32 ** is printed, otherwise the full comment is printed.  The "-n|--limit N"
33 ** and "--offset P" options limits the output to the first N changes
34 ** after skipping P changes.
35 **
36 ** The -i mode will print the artifact ID of FILENAME given the REVISION
37 ** provided by the -r flag (which is required).
38 **
39 ** In the -s mode prints the status as <status> <revision>.  This is
40 ** a quick status and does not check for up-to-date-ness of the file.
41 **
42 ** In the -p mode, there's an optional flag "-r|--revision REVISION".
43 ** The specified version (or the latest checked out version) is printed
44 ** to stdout.  The -p mode is another form of the "cat" command.
45 **
46 ** Options:
47 **   -b|--brief           Display a brief (one line / revision) summary
48 **   --case-sensitive B   Enable or disable case-sensitive filenames.  B is a
49 **                        boolean: "yes", "no", "true", "false", etc.
50 **   -i|--id              Print the artifact ID (requires -r)
51 **   -l|--log             Select log mode (the default)
52 **   -n|--limit N         Display the first N changes (default unlimited).
53 **                        N less than 0 means no limit.
54 **   --offset P           Skip P changes
55 **   -p|--print           Select print mode
56 **   -r|--revision R      Print the given revision (or ckout, if none is given)
57 **                        to stdout (only in print mode)
58 **   -s|--status          Select status mode (print a status indicator for FILE)
59 **   -W|--width N         Width of lines (default is to auto-detect). Must be
60 **                        more than 22 or else 0 to indicate no limit.
61 **
62 ** See also: [[artifact]], [[cat]], [[descendants]], [[info]], [[leaves]]
63 */
finfo_cmd(void)64 void finfo_cmd(void){
65   db_must_be_within_tree();
66   if( find_option("status","s",0) ){
67     Stmt q;
68     Blob line;
69     Blob fname;
70     int vid;
71 
72     /* We should be done with options.. */
73     verify_all_options();
74 
75     if( g.argc!=3 ) usage("-s|--status FILENAME");
76     vid = db_lget_int("checkout", 0);
77     if( vid==0 ){
78       fossil_fatal("no checkout to finfo files in");
79     }
80     vfile_check_signature(vid, CKSIG_ENOTFILE);
81     file_tree_name(g.argv[2], &fname, 0, 1);
82     db_prepare(&q,
83         "SELECT pathname, deleted, rid, chnged, coalesce(origname!=pathname,0)"
84         "  FROM vfile WHERE vfile.pathname=%B %s",
85         &fname, filename_collation());
86     blob_zero(&line);
87     if( db_step(&q)==SQLITE_ROW ) {
88       Blob uuid;
89       int isDeleted = db_column_int(&q, 1);
90       int isNew = db_column_int(&q,2) == 0;
91       int chnged = db_column_int(&q,3);
92       int renamed = db_column_int(&q,4);
93 
94       blob_zero(&uuid);
95       db_blob(&uuid,
96            "SELECT uuid FROM blob, mlink, vfile WHERE "
97            "blob.rid = mlink.mid AND mlink.fid = vfile.rid AND "
98            "vfile.pathname=%B %s",
99            &fname, filename_collation()
100       );
101       if( isNew ){
102         blob_appendf(&line, "new");
103       }else if( isDeleted ){
104         blob_appendf(&line, "deleted");
105       }else if( renamed ){
106         blob_appendf(&line, "renamed");
107       }else if( chnged ){
108         blob_appendf(&line, "edited");
109       }else{
110         blob_appendf(&line, "unchanged");
111       }
112       blob_appendf(&line, " ");
113       blob_appendf(&line, " %10.10s", blob_str(&uuid));
114       blob_reset(&uuid);
115     }else{
116       blob_appendf(&line, "unknown 0000000000");
117     }
118     db_finalize(&q);
119     fossil_print("%s\n", blob_str(&line));
120     blob_reset(&fname);
121     blob_reset(&line);
122   }else if( find_option("print","p",0) ){
123     Blob record;
124     Blob fname;
125     const char *zRevision = find_option("revision", "r", 1);
126 
127     /* We should be done with options.. */
128     verify_all_options();
129 
130     file_tree_name(g.argv[2], &fname, 0, 1);
131     if( zRevision ){
132       historical_blob(zRevision, blob_str(&fname), &record, 1);
133     }else{
134       int rid = db_int(0, "SELECT rid FROM vfile WHERE pathname=%B %s",
135                        &fname, filename_collation());
136       if( rid==0 ){
137         fossil_fatal("no history for file: %b", &fname);
138       }
139       content_get(rid, &record);
140     }
141     blob_write_to_file(&record, "-");
142     blob_reset(&record);
143     blob_reset(&fname);
144   }else if( find_option("id","i",0) ){
145     Blob fname;
146     int rid;
147     const char *zRevision = find_option("revision", "r", 1);
148 
149     verify_all_options();
150 
151     if( zRevision==0 ) usage("-i|--id also requires -r|--revision");
152     if( g.argc!=3 ) usage("-r|--revision REVISION FILENAME");
153     file_tree_name(g.argv[2], &fname, 0, 1);
154     rid = db_int(0, "SELECT rid FROM blob WHERE uuid ="
155                     "  (SELECT uuid FROM files_of_checkin(%Q)"
156                     "   WHERE filename=%B %s)",
157                  zRevision, &fname, filename_collation());
158     if( rid==0 ) {
159       fossil_fatal("file not found for revision %s: %s",
160                    zRevision, blob_str(&fname));
161     }
162     whatis_rid(rid,0);
163     blob_reset(&fname);
164   }else{
165     Blob line;
166     Stmt q;
167     Blob fname;
168     int rid;
169     const char *zFilename;
170     const char *zLimit;
171     const char *zWidth;
172     const char *zOffset;
173     int iLimit, iOffset, iBrief, iWidth;
174 
175     if( find_option("log","l",0) ){
176       /* this is the default, no-op */
177     }
178     zLimit = find_option("limit","n",1);
179     zWidth = find_option("width","W",1);
180     iLimit = zLimit ? atoi(zLimit) : -1;
181     zOffset = find_option("offset",0,1);
182     iOffset = zOffset ? atoi(zOffset) : 0;
183     iBrief = (find_option("brief","b",0) == 0);
184     if( iLimit==0 ){
185       iLimit = -1;
186     }
187     if( zWidth ){
188       iWidth = atoi(zWidth);
189       if( (iWidth!=0) && (iWidth<=22) ){
190         fossil_fatal("-W|--width value must be >22 or 0");
191       }
192     }else{
193       iWidth = -1;
194     }
195 
196     /* We should be done with options.. */
197     verify_all_options();
198 
199     if( g.argc!=3 ){
200       usage("?-l|--log? ?-b|--brief? FILENAME");
201     }
202     file_tree_name(g.argv[2], &fname, 0, 1);
203     rid = db_int(0, "SELECT rid FROM vfile WHERE pathname=%B %s",
204                  &fname, filename_collation());
205     if( rid==0 ){
206       fossil_fatal("no history for file: %b", &fname);
207     }
208     zFilename = blob_str(&fname);
209     db_prepare(&q,
210         "SELECT DISTINCT b.uuid, ci.uuid, date(event.mtime,toLocal()),"
211         "       coalesce(event.ecomment, event.comment),"
212         "       coalesce(event.euser, event.user),"
213         "       (SELECT value FROM tagxref WHERE tagid=%d AND tagtype>0"
214                                 " AND tagxref.rid=mlink.mid)" /* Tags */
215         "  FROM mlink, blob b, event, blob ci, filename"
216         " WHERE filename.name=%Q %s"
217         "   AND mlink.fnid=filename.fnid"
218         "   AND b.rid=mlink.fid"
219         "   AND event.objid=mlink.mid"
220         "   AND event.objid=ci.rid"
221         " ORDER BY event.mtime DESC LIMIT %d OFFSET %d",
222         TAG_BRANCH, zFilename, filename_collation(),
223         iLimit, iOffset
224     );
225     blob_zero(&line);
226     if( iBrief ){
227       fossil_print("History for %s\n", blob_str(&fname));
228     }
229     while( db_step(&q)==SQLITE_ROW ){
230       const char *zFileUuid = db_column_text(&q, 0);
231       const char *zCiUuid = db_column_text(&q,1);
232       const char *zDate = db_column_text(&q, 2);
233       const char *zCom = db_column_text(&q, 3);
234       const char *zUser = db_column_text(&q, 4);
235       const char *zBr = db_column_text(&q, 5);
236       char *zOut;
237       if( zBr==0 ) zBr = "trunk";
238       if( iBrief ){
239         fossil_print("%s ", zDate);
240         zOut = mprintf(
241            "[%S] %s (user: %s, artifact: [%S], branch: %s)",
242            zCiUuid, zCom, zUser, zFileUuid, zBr);
243         comment_print(zOut, zCom, 11, iWidth, get_comment_format());
244         fossil_free(zOut);
245       }else{
246         blob_reset(&line);
247         blob_appendf(&line, "%S ", zCiUuid);
248         blob_appendf(&line, "%.10s ", zDate);
249         blob_appendf(&line, "%8.8s ", zUser);
250         blob_appendf(&line, "%8.8s ", zBr);
251         blob_appendf(&line,"%-39.39s", zCom );
252         comment_print(blob_str(&line), zCom, 0, iWidth, get_comment_format());
253       }
254     }
255     db_finalize(&q);
256     blob_reset(&fname);
257   }
258 }
259 
260 /*
261 ** COMMAND: cat
262 **
263 ** Usage: %fossil cat FILENAME ... ?OPTIONS?
264 **
265 ** Print on standard output the content of one or more files as they exist
266 ** in the repository.  The version currently checked out is shown by default.
267 ** Other versions may be specified using the -r option.
268 **
269 ** Options:
270 **    -R|--repository REPO       Extract artifacts from repository REPO
271 **    -r VERSION                 The specific check-in containing the file
272 **
273 ** See also: [[finfo]]
274 */
cat_cmd(void)275 void cat_cmd(void){
276   int i;
277   Blob content, fname;
278   const char *zRev;
279   db_find_and_open_repository(0, 0);
280   zRev = find_option("r","r",1);
281 
282   /* We should be done with options.. */
283   verify_all_options();
284 
285   for(i=2; i<g.argc; i++){
286     file_tree_name(g.argv[i], &fname, 0, 1);
287     blob_zero(&content);
288     historical_blob(zRev, blob_str(&fname), &content, 1);
289     blob_write_to_file(&content, "-");
290     blob_reset(&fname);
291     blob_reset(&content);
292   }
293 }
294 
295 /* Values for the debug= query parameter to finfo */
296 #define FINFO_DEBUG_MLINK  0x01
297 
298 /*
299 ** WEBPAGE: finfo
300 ** Usage:
301 **   *  /finfo?name=FILENAME
302 **   *  /finfo?name=FILENAME&ci=HASH
303 **
304 ** Show the change history for a single file.  The name=FILENAME query
305 ** parameter gives the filename and is a required parameter.  If the
306 ** ci=HASH parameter is also supplied, then the FILENAME,HASH combination
307 ** identifies a particular version of a file, and in that case all changes
308 ** to that one file version are tracked across both edits and renames.
309 ** If only the name=FILENAME parameter is supplied (if ci=HASH is omitted)
310 ** then the graph shows all changes to any file while it happened
311 ** to be called FILENAME and changes are not tracked across renames.
312 **
313 ** Additional query parameters:
314 **
315 **    a=DATETIME      Only show changes after DATETIME
316 **    b=DATETIME      Only show changes before DATETIME
317 **    ci=HASH         identify a particular version of a file and then
318 **                    track changes to that file across renames
319 **    m=HASH          Mark this particular file version.
320 **    n=NUM           Show the first NUM changes only
321 **    name=FILENAME   (Required) name of file whose history to show
322 **    brbg            Background color by branch name
323 **    ubg             Background color by user name
324 **    from=HASH       Ancestors only (not descendants) of the version of
325 **                    the file in this particular check-in.
326 **    to=HASH         If both from= and to= are supplied, only show those
327 **                    changes on the direct path between the two given
328 **                    checkins.
329 **    showid          Show RID values for debugging
330 **    showsql         Show the SQL query used to gather the data for
331 **                    the graph
332 **
333 ** DATETIME may be in any of usual formats, including "now",
334 ** "YYYY-MM-DDTHH:MM:SS.SSS", "YYYYMMDDHHMM", and others.
335 */
finfo_page(void)336 void finfo_page(void){
337   Stmt q;
338   const char *zFilename = PD("name","");
339   char zPrevDate[20];
340   const char *zA;
341   const char *zB;
342   int n;
343   int ridFrom;
344   int ridTo = 0;
345   int ridCi = 0;
346   const char *zCI = P("ci");
347   int fnid;
348   Blob title;
349   Blob sql;
350   HQuery url;
351   GraphContext *pGraph;
352   int brBg = P("brbg")!=0;
353   int uBg = P("ubg")!=0;
354   int fDebug = atoi(PD("debug","0"));
355   int fShowId = P("showid")!=0;
356   Stmt qparent;
357   int iTableId = timeline_tableid();
358   int tmFlags = 0;            /* Viewing mode */
359   const char *zStyle;         /* Viewing mode name */
360   const char *zMark;          /* Mark this version of the file */
361   int selRid = 0;             /* RID of the marked file version */
362   int mxfnid;                 /* Maximum filename.fnid value */
363 
364   login_check_credentials();
365   if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
366   fnid = db_int(0, "SELECT fnid FROM filename WHERE name=%Q", zFilename);
367   ridCi = zCI ? name_to_rid_www("ci") : 0;
368   if( fnid==0 ){
369     style_header("No such file");
370   }else if( ridCi==0 ){
371     style_header("All files named \"%s\"", zFilename);
372   }else{
373     style_header("History of %s of %s",zFilename, zCI);
374   }
375   login_anonymous_available();
376   tmFlags = timeline_ss_submenu();
377   if( tmFlags & TIMELINE_COLUMNAR ){
378     zStyle = "Columnar";
379   }else if( tmFlags & TIMELINE_COMPACT ){
380     zStyle = "Compact";
381   }else if( tmFlags & TIMELINE_VERBOSE ){
382     zStyle = "Verbose";
383   }else if( tmFlags & TIMELINE_CLASSIC ){
384     zStyle = "Classic";
385   }else{
386     zStyle = "Modern";
387   }
388   url_initialize(&url, "finfo");
389   if( brBg ) url_add_parameter(&url, "brbg", 0);
390   if( uBg ) url_add_parameter(&url, "ubg", 0);
391   ridFrom = name_to_rid_www("from");
392   zPrevDate[0] = 0;
393   if( fnid==0 ){
394     @ No such file: %h(zFilename)
395     style_finish_page();
396     return;
397   }
398   if( g.perm.Admin ){
399     style_submenu_element("MLink Table", "%R/mlink?name=%t", zFilename);
400   }
401   if( ridFrom ){
402     if( P("to")!=0 ){
403       ridTo = name_to_typed_rid(P("to"),"ci");
404       path_shortest_stored_in_ancestor_table(ridFrom,ridTo);
405     }else{
406       compute_direct_ancestors(ridFrom);
407     }
408   }
409   url_add_parameter(&url, "name", zFilename);
410   blob_zero(&sql);
411   if( ridCi ){
412     /* If we will be tracking changes across renames, some extra temp
413     ** tables (implemented as CTEs) are required */
414     blob_append_sql(&sql,
415       /* The clade(fid,fnid) table is the set of all (fid,fnid) pairs
416       ** that should participate in the output.  Clade is computed by
417       ** walking the graph of mlink edges.
418       */
419       "WITH RECURSIVE clade(fid,fnid) AS (\n"
420       "  SELECT blob.rid, %d FROM blob\n"         /* %d is fnid */
421       "   WHERE blob.uuid=(SELECT uuid FROM files_of_checkin(%Q)"
422                          " WHERE filename=%Q)\n"  /* %Q is the filename */
423       "   UNION\n"
424       "  SELECT mlink.fid, mlink.fnid\n"
425       "    FROM clade, mlink\n"
426       "   WHERE clade.fid=mlink.pid\n"
427       "     AND ((mlink.pfnid=0 AND mlink.fnid=clade.fnid)\n"
428       "          OR mlink.pfnid=clade.fnid)\n"
429       "     AND (mlink.fid>0 OR NOT EXISTS(SELECT 1 FROM mlink AS mx"
430                  " WHERE mx.mid=mlink.mid AND mx.pid=mlink.pid"
431                  "   AND mx.fid>0 AND mx.pfnid=mlink.fnid))\n"
432       "   UNION\n"
433       "  SELECT mlink.pid,"
434               " CASE WHEN mlink.pfnid>0 THEN mlink.pfnid ELSE mlink.fnid END\n"
435       "    FROM clade, mlink\n"
436       "   WHERE mlink.pid>0\n"
437       "     AND mlink.fid=clade.fid\n"
438       "     AND mlink.fnid=clade.fnid\n"
439       ")\n",
440       fnid, zCI, zFilename
441     );
442   }else{
443     /* This is the case for all files with a given name.  We will still
444     ** create a "clade(fid,fnid)" table that identifies all participates
445     ** in the output graph, so that subsequent queries can all be the same,
446     ** but in the case the clade table is much simplier, being just a
447     ** single direct query against the mlink table.
448     */
449     blob_append_sql(&sql,
450       "WITH clade(fid,fnid) AS (\n"
451       "  SELECT DISTINCT fid, %d\n"
452       "    FROM mlink\n"
453       "   WHERE fnid=%d)",
454       fnid, fnid
455     );
456   }
457   blob_append_sql(&sql,
458     "SELECT\n"
459     "  datetime(min(event.mtime),toLocal()),\n"         /* Date of change */
460     "  coalesce(event.ecomment, event.comment),\n"      /* Check-in comment */
461     "  coalesce(event.euser, event.user),\n"            /* User who made chng */
462     "  mlink.pid,\n"                                    /* Parent file rid */
463     "  mlink.fid,\n"                                    /* File rid */
464     "  (SELECT uuid FROM blob WHERE rid=mlink.pid),\n"  /* Parent file hash */
465     "  blob.uuid,\n"                                    /* Current file hash */
466     "  (SELECT uuid FROM blob WHERE rid=mlink.mid),\n"  /* Check-in hash */
467     "  event.bgcolor,\n"                                /* Background color */
468     "  (SELECT value FROM tagxref WHERE tagid=%d AND tagtype>0"
469                              " AND tagxref.rid=mlink.mid),\n" /* Branchname */
470     "  mlink.mid,\n"                                    /* check-in ID */
471     "  mlink.pfnid,\n"                                  /* Previous filename */
472     "  blob.size,\n"                                    /* File size */
473     "  mlink.fnid,\n"                                   /* Current filename */
474     "  filename.name\n"                                 /* Current filename */
475     "FROM clade CROSS JOIN mlink, event"
476     " LEFT JOIN blob ON blob.rid=clade.fid"
477     " LEFT JOIN filename ON filename.fnid=clade.fnid\n"
478     "WHERE mlink.fnid=clade.fnid AND mlink.fid=clade.fid\n"
479     "  AND event.objid=mlink.mid\n",
480     TAG_BRANCH
481   );
482   if( (zA = P("a"))!=0 ){
483     blob_append_sql(&sql, "  AND event.mtime>=%.16g\n",
484          symbolic_name_to_mtime(zA,0));
485     url_add_parameter(&url, "a", zA);
486   }
487   if( (zB = P("b"))!=0 ){
488     blob_append_sql(&sql, "  AND event.mtime<=%.16g\n",
489          symbolic_name_to_mtime(zB,0));
490     url_add_parameter(&url, "b", zB);
491   }
492   if( ridFrom ){
493     blob_append_sql(&sql,
494       "  AND mlink.mid IN (SELECT rid FROM ancestor)\n"
495       "GROUP BY mlink.fid\n"
496     );
497   }else{
498     /* We only want each version of a file to appear on the graph once,
499     ** at its earliest appearance.  All the other times that it gets merged
500     ** into this or that branch can be ignored.  An exception is for when
501     ** files are deleted (when they have mlink.fid==0).  If the same file
502     ** is deleted in multiple places, we want to show each deletion, so
503     ** use a "fake fid" which is derived from the parent-fid for grouping.
504     ** The same fake-fid must be used on the graph.
505     */
506     blob_append_sql(&sql,
507       "GROUP BY"
508       " CASE WHEN mlink.fid>0 THEN mlink.fid ELSE mlink.pid+1000000000 END,"
509       " mlink.fnid\n"
510     );
511   }
512   blob_append_sql(&sql, "ORDER BY event.mtime DESC");
513   if( (n = atoi(PD("n","0")))>0 ){
514     blob_append_sql(&sql, " LIMIT %d", n);
515     url_add_parameter(&url, "n", P("n"));
516   }
517   blob_append_sql(&sql, " /*sort*/\n");
518   db_prepare(&q, "%s", blob_sql_text(&sql));
519   if( P("showsql")!=0 ){
520     @ <p>SQL: <blockquote><pre>%h(blob_str(&sql))</blockquote></pre>
521   }
522   zMark = P("m");
523   if( zMark ){
524     selRid = symbolic_name_to_rid(zMark, "*");
525   }
526   blob_reset(&sql);
527   blob_zero(&title);
528   if( ridFrom ){
529     char *zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", ridFrom);
530     char *zLink = href("%R/info/%!S", zUuid);
531     if( ridTo ){
532       blob_appendf(&title, "Changes to file ");
533     }else if( n>0 ){
534       blob_appendf(&title, "First %d ancestors of file ", n);
535     }else{
536       blob_appendf(&title, "Ancestors of file ");
537     }
538     blob_appendf(&title,"%z%h</a>",
539                  href("%R/file?name=%T&ci=%!S", zFilename, zUuid),
540                  zFilename);
541     if( fShowId ) blob_appendf(&title, " (%d)", fnid);
542     blob_append(&title, ridTo ? " between " : " from ", -1);
543     blob_appendf(&title, "check-in %z%S</a>", zLink, zUuid);
544     if( fShowId ) blob_appendf(&title, " (%d)", ridFrom);
545     fossil_free(zUuid);
546     if( ridTo ){
547       zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", ridTo);
548       zLink = href("%R/info/%!S", zUuid);
549       blob_appendf(&title, " and check-in %z%S</a>", zLink, zUuid);
550       fossil_free(zUuid);
551     }
552   }else if( ridCi ){
553     blob_appendf(&title, "History of the file that is called ");
554     hyperlinked_path(zFilename, &title, 0, "tree", "", LINKPATH_FILE);
555     if( fShowId ) blob_appendf(&title, " (%d)", fnid);
556     blob_appendf(&title, " at checkin %z%h</a>",
557         href("%R/info?name=%t",zCI), zCI);
558   }else{
559     blob_appendf(&title, "History for ");
560     hyperlinked_path(zFilename, &title, 0, "tree", "", LINKPATH_FILE);
561     if( fShowId ) blob_appendf(&title, " (%d)", fnid);
562   }
563   if( uBg ){
564     blob_append(&title, " (color-coded by user)", -1);
565   }
566   @ <h2>%b(&title)</h2>
567   blob_reset(&title);
568   pGraph = graph_init();
569   @ <table id="timelineTable%d(iTableId)" class="timelineTable">
570   mxfnid = db_int(0, "SELECT max(fnid) FROM filename");
571   if( ridFrom ){
572     db_prepare(&qparent,
573       "SELECT DISTINCT pid*%d+CASE WHEN pfnid>0 THEN pfnid ELSE fnid END"
574       "  FROM mlink"
575       " WHERE fid=:fid AND mid=:mid AND pid>0 AND fnid=:fnid"
576       "   AND pmid IN (SELECT rid FROM ancestor)"
577       " ORDER BY isaux /*sort*/", mxfnid+1
578     );
579   }else{
580     db_prepare(&qparent,
581       "SELECT DISTINCT pid*%d+CASE WHEN pfnid>0 THEN pfnid ELSE fnid END"
582       "  FROM mlink"
583       " WHERE fid=:fid AND mid=:mid AND pid>0 AND fnid=:fnid"
584       " ORDER BY isaux /*sort*/", mxfnid+1
585     );
586   }
587   while( db_step(&q)==SQLITE_ROW ){
588     const char *zDate = db_column_text(&q, 0);
589     const char *zCom = db_column_text(&q, 1);
590     const char *zUser = db_column_text(&q, 2);
591     int fpid = db_column_int(&q, 3);
592     int frid = db_column_int(&q, 4);
593     const char *zPUuid = db_column_text(&q, 5);
594     const char *zUuid = db_column_text(&q, 6);
595     const char *zCkin = db_column_text(&q,7);
596     const char *zBgClr = db_column_text(&q, 8);
597     const char *zBr = db_column_text(&q, 9);
598     int fmid = db_column_int(&q, 10);
599     int pfnid = db_column_int(&q, 11);
600     int szFile = db_column_int(&q, 12);
601     int fnid = db_column_int(&q, 13);
602     const char *zFName = db_column_text(&q,14);
603     int gidx;
604     char zTime[10];
605     int nParent = 0;
606     GraphRowId aParent[GR_MAX_RAIL];
607 
608     db_bind_int(&qparent, ":fid", frid);
609     db_bind_int(&qparent, ":mid", fmid);
610     db_bind_int(&qparent, ":fnid", fnid);
611     while( db_step(&qparent)==SQLITE_ROW && nParent<count(aParent) ){
612       aParent[nParent] = db_column_int64(&qparent, 0);
613       nParent++;
614     }
615     db_reset(&qparent);
616     if( zBr==0 ) zBr = "trunk";
617     if( uBg ){
618       zBgClr = user_color(zUser);
619     }else if( brBg || zBgClr==0 || zBgClr[0]==0 ){
620       zBgClr = strcmp(zBr,"trunk")==0 ? "" : hash_color(zBr);
621     }
622     gidx = graph_add_row(pGraph,
623                    frid>0 ? (GraphRowId)frid*(mxfnid+1)+fnid : fpid+1000000000,
624                    nParent, 0, aParent, zBr, zBgClr,
625                    zUuid, 0);
626     if( strncmp(zDate, zPrevDate, 10) ){
627       sqlite3_snprintf(sizeof(zPrevDate), zPrevDate, "%.10s", zDate);
628       @ <tr><td>
629       @   <div class="divider timelineDate">%s(zPrevDate)</div>
630       @ </td><td></td><td></td></tr>
631     }
632     memcpy(zTime, &zDate[11], 5);
633     zTime[5] = 0;
634     if( frid==selRid ){
635       @ <tr class='timelineSelected'>
636     }else{
637       @ <tr>
638     }
639     @ <td class="timelineTime">\
640     @ %z(href("%R/file?name=%T&ci=%!S",zFName,zCkin))%s(zTime)</a></td>
641     @ <td class="timelineGraph"><div id="m%d(gidx)" class="tl-nodemark"></div>
642     @ </td>
643     if( zBgClr && zBgClr[0] ){
644       @ <td class="timeline%s(zStyle)Cell" id='mc%d(gidx)'>
645     }else{
646       @ <td class="timeline%s(zStyle)Cell">
647     }
648     if( tmFlags & TIMELINE_COMPACT ){
649       @ <span class='timelineCompactComment' data-id='%d(frid)'>
650     }else{
651       @ <span class='timeline%s(zStyle)Comment'>
652       if( pfnid ){
653         char *zPrevName = db_text(0,"SELECT name FROM filename WHERE fnid=%d",
654                                    pfnid);
655         @ <b>Renamed</b> %h(zPrevName) &rarr; %h(zFName).
656         fossil_free(zPrevName);
657       }
658       if( zUuid && ridTo==0 && nParent==0 ){
659         @ <b>Added:</b>
660       }
661       if( zUuid==0 ){
662         char *zNewName;
663         zNewName = db_text(0,
664           "SELECT name FROM filename WHERE fnid = "
665           "   (SELECT fnid FROM mlink"
666           "     WHERE mid=%d"
667           "       AND pfnid IN (SELECT fnid FROM filename WHERE name=%Q))",
668           fmid, zFName);
669         if( zNewName ){
670           @ <b>Renamed</b> to
671           @ %z(href("%R/finfo?name=%t",zNewName))%h(zNewName)</a>.
672           fossil_free(zNewName);
673         }else{
674           @ <b>Deleted:</b>
675         }
676       }
677       if( (tmFlags & TIMELINE_VERBOSE)!=0 && zUuid ){
678         hyperlink_to_version(zUuid);
679         @ part of check-in \
680         hyperlink_to_version(zCkin);
681       }
682     }
683     @ %W(zCom)</span>
684     if( (tmFlags & TIMELINE_COMPACT)!=0 ){
685       @ <span class='timelineEllipsis' data-id='%d(frid)' \
686       @ id='ellipsis-%d(frid)'>...</span>
687     }
688     if( tmFlags & TIMELINE_COLUMNAR ){
689       if( zBgClr && zBgClr[0] ){
690         @ <td class="timelineDetailCell" id='md%d(gidx)'>
691       }else{
692         @ <td class="timelineDetailCell">
693       }
694     }
695     if( tmFlags & TIMELINE_COMPACT ){
696       cgi_printf("<span class='clutter' id='detail-%d'>",frid);
697     }
698     cgi_printf("<span class='timeline%sDetail'>", zStyle);
699     if( tmFlags & (TIMELINE_COMPACT|TIMELINE_VERBOSE) ) cgi_printf("(");
700     if( zUuid && (tmFlags & TIMELINE_VERBOSE)==0 ){
701       @ file:&nbsp;%z(href("%R/file?name=%T&ci=%!S",zFName,zCkin))\
702       @ [%S(zUuid)]</a>
703       if( fShowId ){
704         int srcId = delta_source_rid(frid);
705         if( srcId>0 ){
706           @ id:&nbsp;%d(frid)&larr;%d(srcId)
707         }else{
708           @ id:&nbsp;%d(frid)
709         }
710       }
711     }
712     @ check-in:&nbsp;\
713     hyperlink_to_version(zCkin);
714     if( fShowId ){
715       @ (%d(fmid))
716     }
717     @ user:&nbsp;\
718     hyperlink_to_user(zUser, zDate, ",");
719     @ branch:&nbsp;%z(href("%R/timeline?t=%T",zBr))%h(zBr)</a>,
720     if( tmFlags & (TIMELINE_COMPACT|TIMELINE_VERBOSE) ){
721       @ size:&nbsp;%d(szFile))
722     }else{
723       @ size:&nbsp;%d(szFile)
724     }
725     if( g.perm.Hyperlink && zUuid ){
726       const char *z = zFName;
727       @ <span id='links-%d(frid)'><span class='timelineExtraLinks'>
728       @ %z(href("%R/annotate?filename=%h&checkin=%s",z,zCkin))
729       @ [annotate]</a>
730       @ %z(href("%R/blame?filename=%h&checkin=%s",z,zCkin))
731       @ [blame]</a>
732       @ %z(href("%R/timeline?uf=%!S",zUuid))[check-ins&nbsp;using]</a>
733       if( fpid>0 ){
734         @ %z(href("%R/fdiff?v1=%!S&v2=%!S",zPUuid,zUuid))[diff]</a>
735       }
736       if( fileedit_is_editable(zFName) ){
737         @ %z(href("%R/fileedit?filename=%T&checkin=%!S",zFName,zCkin))\
738         @ [edit]</a>
739       }
740       @ </span></span>
741     }
742     if( fDebug & FINFO_DEBUG_MLINK ){
743       int ii;
744       char *zAncLink;
745       @ <br />fid=%d(frid) \
746       @ graph-id=%lld(frid>0?(GraphRowId)frid*(mxfnid+1)+fnid:fpid+1000000000) \
747       @ pid=%d(fpid) mid=%d(fmid) fnid=%d(fnid) \
748       @ pfnid=%d(pfnid) mxfnid=%d(mxfnid)
749       if( nParent>0 ){
750         @ parents=%lld(aParent[0])
751         for(ii=1; ii<nParent; ii++){
752           @ %lld(aParent[ii])
753         }
754       }
755       zAncLink = href("%R/finfo?name=%T&from=%!S&debug=1",zFName,zCkin);
756       @ %z(zAncLink)[ancestry]</a>
757     }
758     tag_private_status(frid);
759     /* End timelineDetail */
760     if( tmFlags & TIMELINE_COMPACT ){
761       @ </span></span>
762     }else{
763       @ </span>
764     }
765     @ </td></tr>
766   }
767   db_finalize(&q);
768   db_finalize(&qparent);
769   if( pGraph ){
770     graph_finish(pGraph, 0, TIMELINE_DISJOINT);
771     if( pGraph->nErr ){
772       graph_free(pGraph);
773       pGraph = 0;
774     }else{
775       @ <tr class="timelineBottom" id="btm-%d(iTableId)">\
776       @ <td></td><td></td><td></td></tr>
777     }
778   }
779   @ </table>
780   timeline_output_graph_javascript(pGraph, TIMELINE_FILEDIFF, iTableId);
781   style_finish_page();
782 }
783 
784 /*
785 ** WEBPAGE: mlink
786 ** URL: /mlink?name=FILENAME
787 ** URL: /mlink?ci=NAME
788 **
789 ** Show all MLINK table entries for a particular file, or for
790 ** a particular check-in.
791 **
792 ** This screen is intended for use by Fossil developers to help
793 ** in debugging Fossil itself.  Ordinary Fossil users are not
794 ** expected to know what the MLINK table is or why it is important.
795 **
796 ** To avoid confusing ordinary users, this page is only available
797 ** to administrators.
798 */
mlink_page(void)799 void mlink_page(void){
800   const char *zFName = P("name");
801   const char *zCI = P("ci");
802   Stmt q;
803 
804   login_check_credentials();
805   if( !g.perm.Admin ){ login_needed(g.anon.Admin); return; }
806   style_set_current_feature("finfo");
807   style_header("MLINK Table");
808   if( zFName==0 && zCI==0 ){
809     @ <span class='generalError'>
810     @ Requires either a name= or ci= query parameter
811     @ </span>
812   }else if( zFName ){
813     int fnid = db_int(0,"SELECT fnid FROM filename WHERE name=%Q",zFName);
814     if( fnid<=0 ) fossil_fatal("no such file: \"%s\"", zFName);
815     db_prepare(&q,
816        "SELECT"
817        /* 0 */ "  datetime(event.mtime,toLocal()),"
818        /* 1 */ "  (SELECT uuid FROM blob WHERE rid=mlink.mid),"
819        /* 2 */ "  (SELECT uuid FROM blob WHERE rid=mlink.pmid),"
820        /* 3 */ "  isaux,"
821        /* 4 */ "  (SELECT uuid FROM blob WHERE rid=mlink.fid),"
822        /* 5 */ "  (SELECT uuid FROM blob WHERE rid=mlink.pid),"
823        /* 6 */ "  mlink.pid,"
824        /* 7 */ "  mperm,"
825        /* 8 */ "  (SELECT name FROM filename WHERE fnid=mlink.pfnid)"
826        "  FROM mlink, event"
827        " WHERE mlink.fnid=%d"
828        "   AND event.objid=mlink.mid"
829        " ORDER BY 1 DESC",
830        fnid
831     );
832     style_table_sorter();
833     @ <h1>MLINK table for file
834     @ <a href='%R/finfo?name=%t(zFName)'>%h(zFName)</a></h1>
835     @ <div class='brlist'>
836     @ <table class='sortable' data-column-types='tttxtttt' data-init-sort='1'>
837     @ <thead><tr>
838     @ <th>Date</th>
839     @ <th>Check-in</th>
840     @ <th>Parent<br>Check-in</th>
841     @ <th>Merge?</th>
842     @ <th>New</th>
843     @ <th>Old</th>
844     @ <th>Exe<br>Bit?</th>
845     @ <th>Prior<br>Name</th>
846     @ </tr></thead>
847     @ <tbody>
848     while( db_step(&q)==SQLITE_ROW ){
849       const char *zDate = db_column_text(&q,0);
850       const char *zCkin = db_column_text(&q,1);
851       const char *zParent = db_column_text(&q,2);
852       int isMerge = db_column_int(&q,3);
853       const char *zFid = db_column_text(&q,4);
854       const char *zPid = db_column_text(&q,5);
855       int isExe = db_column_int(&q,7);
856       const char *zPrior = db_column_text(&q,8);
857       @ <tr>
858       @ <td><a href='%R/timeline?c=%!S(zCkin)'>%s(zDate)</a></td>
859       @ <td><a href='%R/info/%!S(zCkin)'>%S(zCkin)</a></td>
860       if( zParent ){
861         @ <td><a href='%R/info/%!S(zParent)'>%S(zParent)</a></td>
862       }else{
863         @ <td><i>(New)</i></td>
864       }
865       @ <td align='center'>%s(isMerge?"&#x2713;":"")</td>
866       if( zFid ){
867         @ <td><a href='%R/info/%!S(zFid)'>%S(zFid)</a></td>
868       }else{
869         @ <td><i>(Deleted)</i></td>
870       }
871       if( zPid ){
872         @ <td><a href='%R/info/%!S(zPid)'>%S(zPid)</a>
873       }else if( db_column_int(&q,6)<0 ){
874         @ <td><i>(Added by merge)</i></td>
875       }else{
876         @ <td><i>(New)</i></td>
877       }
878       @ <td align='center'>%s(isExe?"&#x2713;":"")</td>
879       if( zPrior ){
880         @ <td><a href='%R/finfo?name=%t(zPrior)'>%h(zPrior)</a></td>
881       }else{
882         @ <td></td>
883       }
884       @ </tr>
885     }
886     db_finalize(&q);
887     @ </tbody>
888     @ </table>
889     @ </div>
890   }else{
891     int mid = name_to_rid_www("ci");
892     db_prepare(&q,
893        "SELECT"
894        /* 0 */ "  (SELECT name FROM filename WHERE fnid=mlink.fnid),"
895        /* 1 */ "  (SELECT uuid FROM blob WHERE rid=mlink.fid),"
896        /* 2 */ "  pid,"
897        /* 3 */ "  (SELECT uuid FROM blob WHERE rid=mlink.pid),"
898        /* 4 */ "  (SELECT name FROM filename WHERE fnid=mlink.pfnid),"
899        /* 5 */ "  (SELECT uuid FROM blob WHERE rid=mlink.pmid),"
900        /* 6 */ "  mperm,"
901        /* 7 */ "  isaux"
902        "  FROM mlink WHERE mid=%d ORDER BY 1",
903        mid
904     );
905     @ <h1>MLINK table for check-in %h(zCI)</h1>
906     render_checkin_context(mid, 0, 1, 0);
907     style_table_sorter();
908     @ <hr />
909     @ <div class='brlist'>
910     @ <table class='sortable' data-column-types='ttxtttt' data-init-sort='1'>
911     @ <thead><tr>
912     @ <th>File</th>
913     @ <th>Parent<br>Check-in</th>
914     @ <th>Merge?</th>
915     @ <th>New</th>
916     @ <th>Old</th>
917     @ <th>Exe<br>Bit?</th>
918     @ <th>Prior<br>Name</th>
919     @ </tr></thead>
920     @ <tbody>
921     while( db_step(&q)==SQLITE_ROW ){
922       const char *zName = db_column_text(&q,0);
923       const char *zFid = db_column_text(&q,1);
924       const char *zPid = db_column_text(&q,3);
925       const char *zPrior = db_column_text(&q,4);
926       const char *zParent = db_column_text(&q,5);
927       int isExec = db_column_int(&q,6);
928       int isAux = db_column_int(&q,7);
929       @ <tr>
930       @ <td><a href='%R/finfo?name=%t(zName)'>%h(zName)</a></td>
931       if( zParent ){
932         @ <td><a href='%R/info/%!S(zParent)'>%S(zParent)</a></td>
933       }else{
934         @ <td><i>(New)</i></td>
935       }
936       @ <td align='center'>%s(isAux?"&#x2713;":"")</td>
937       if( zFid ){
938         @ <td><a href='%R/info/%!S(zFid)'>%S(zFid)</a></td>
939       }else{
940         @ <td><i>(Deleted)</i></td>
941       }
942       if( zPid ){
943         @ <td><a href='%R/info/%!S(zPid)'>%S(zPid)</a>
944       }else if( db_column_int(&q,2)<0 ){
945         @ <td><i>(Added by merge)</i></td>
946       }else{
947         @ <td><i>(New)</i></td>
948       }
949       @ <td align='center'>%s(isExec?"&#x2713;":"")</td>
950       if( zPrior ){
951         @ <td><a href='%R/finfo?name=%t(zPrior)'>%h(zPrior)</a></td>
952       }else{
953         @ <td></td>
954       }
955       @ </tr>
956     }
957     db_finalize(&q);
958     @ </tbody>
959     @ </table>
960     @ </div>
961   }
962   style_finish_page();
963 }
964