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) → %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: %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: %d(frid)←%d(srcId)
707 }else{
708 @ id: %d(frid)
709 }
710 }
711 }
712 @ check-in: \
713 hyperlink_to_version(zCkin);
714 if( fShowId ){
715 @ (%d(fmid))
716 }
717 @ user: \
718 hyperlink_to_user(zUser, zDate, ",");
719 @ branch: %z(href("%R/timeline?t=%T",zBr))%h(zBr)</a>,
720 if( tmFlags & (TIMELINE_COMPACT|TIMELINE_VERBOSE) ){
721 @ size: %d(szFile))
722 }else{
723 @ size: %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 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?"✓":"")</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?"✓":"")</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?"✓":"")</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?"✓":"")</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