summaryrefslogtreecommitdiff
path: root/src/info.c
diff options
context:
space:
mode:
authorcashmere <cashmere@autistici.org>2026-09-04 09:05:24 +0200
committercashmere <cashmere@autistici.org>2026-09-04 09:05:24 +0200
commit4f49b16079b27d7ce633619e4cddfdf2bffbc952 (patch)
treec296c1e7f3c3e2f6859c6d3a1b1b208779b89679 /src/info.c
downloadrelic-4f49b16079b27d7ce633619e4cddfdf2bffbc952.tar.gz
initial commitHEADmaster
Diffstat (limited to 'src/info.c')
-rw-r--r--src/info.c4557
1 files changed, 4557 insertions, 0 deletions
diff --git a/src/info.c b/src/info.c
new file mode 100644
index 0000000..915c3ba
--- /dev/null
+++ b/src/info.c
@@ -0,0 +1,4557 @@
+/*
+** Copyright (c) 2007 D. Richard Hipp
+**
+** This program is free software; you can redistribute it and/or
+** modify it under the terms of the Simplified BSD License (also
+** known as the "2-Clause License" or "FreeBSD License".)
+
+** This program is distributed in the hope that it will be useful,
+** but without any warranty; without even the implied warranty of
+** merchantability or fitness for a particular purpose.
+**
+** Author contact information:
+** drh@hwaci.com
+** http://www.hwaci.com/drh/
+**
+*******************************************************************************
+**
+** This file contains code to implement the "info" command. The
+** "info" command gives command-line access to information about
+** the current tree, or a particular artifact or check-in.
+*/
+#include "config.h"
+#include "info.h"
+#include <assert.h>
+
+/*
+** Return a string (in memory obtained from malloc) holding a
+** comma-separated list of tags that apply to check-in with
+** record-id rid. If the "propagatingOnly" flag is true, then only
+** show branch tags (tags that propagate to children).
+**
+** Return NULL if there are no such tags.
+*/
+char *info_tags_of_checkin(int rid, int propagatingOnly){
+ char *zTags;
+ zTags = db_text(0, "SELECT group_concat(substr(tagname, 5), ', ')"
+ " FROM tagxref, tag"
+ " WHERE tagxref.rid=%d AND tagxref.tagtype>%d"
+ " AND tag.tagid=tagxref.tagid"
+ " AND tag.tagname GLOB 'sym-*'",
+ rid, propagatingOnly!=0);
+ return zTags;
+}
+
+
+/*
+** Print common information about a particular record.
+**
+** * The artifact hash
+** * The record ID
+** * mtime and ctime
+** * who signed it
+**
+*/
+void show_common_info(
+ int rid, /* The rid for the check-in to display info for */
+ const char *zRecDesc, /* Brief record description; e.g. "check-out:" */
+ int showComment, /* True to show the check-in comment */
+ int showFamily /* True to show parents and children */
+){
+ Stmt q;
+ char *zComment = 0;
+ char *zTags;
+ char *zDate;
+ char *zUuid;
+ zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
+ if( zUuid ){
+ zDate = db_text(0,
+ "SELECT datetime(mtime) || ' UTC' FROM event WHERE objid=%d",
+ rid
+ );
+ /* 01234567890123 */
+ fossil_print("%-13s %.40s %s\n", zRecDesc, zUuid, zDate ? zDate : "");
+ free(zDate);
+ if( showComment ){
+ zComment = db_text(0,
+ "SELECT coalesce(ecomment,comment) || "
+ " ' (user: ' || coalesce(euser,user,'?') || ')' "
+ " FROM event WHERE objid=%d",
+ rid
+ );
+ }
+ free(zUuid);
+ }
+ if( showFamily ){
+ db_prepare(&q, "SELECT uuid, pid, isprim FROM plink JOIN blob ON pid=rid "
+ " WHERE cid=%d"
+ " ORDER BY isprim DESC, mtime DESC /*sort*/", rid);
+ while( db_step(&q)==SQLITE_ROW ){
+ const char *zUuid = db_column_text(&q, 0);
+ const char *zType = db_column_int(&q, 2) ? "parent:" : "merged-from:";
+ zDate = db_text("",
+ "SELECT datetime(mtime) || ' UTC' FROM event WHERE objid=%d",
+ db_column_int(&q, 1)
+ );
+ fossil_print("%-13s %.40s %s\n", zType, zUuid, zDate);
+ free(zDate);
+ }
+ db_finalize(&q);
+ db_prepare(&q, "SELECT uuid, cid, isprim FROM plink JOIN blob ON cid=rid "
+ " WHERE pid=%d"
+ " ORDER BY isprim DESC, mtime DESC /*sort*/", rid);
+ while( db_step(&q)==SQLITE_ROW ){
+ const char *zUuid = db_column_text(&q, 0);
+ const char *zType = db_column_int(&q, 2) ? "child:" : "merged-into:";
+ zDate = db_text("",
+ "SELECT datetime(mtime) || ' UTC' FROM event WHERE objid=%d",
+ db_column_int(&q, 1)
+ );
+ fossil_print("%-13s %.40s %s\n", zType, zUuid, zDate);
+ free(zDate);
+ }
+ db_finalize(&q);
+ }
+ zTags = info_tags_of_checkin(rid, 0);
+ if( zTags && zTags[0] ){
+ fossil_print("tags: %s\n", zTags);
+ }
+ free(zTags);
+ if( zComment ){
+ fossil_print("comment: ");
+ comment_print(zComment, 0, 14, -1, get_comment_format());
+ free(zComment);
+ }
+}
+
+/*
+** Print information about the URLs used to access a repository and
+** checkouts in a repository.
+*/
+static void extraRepoInfo(void){
+ Stmt s;
+ db_prepare(&s, "SELECT substr(name,7), date(mtime,'unixepoch')"
+ " FROM config"
+ " WHERE name GLOB 'ckout:*' ORDER BY mtime DESC");
+ while( db_step(&s)==SQLITE_ROW ){
+ const char *zName;
+ const char *zCkout = db_column_text(&s, 0);
+ if( !vfile_top_of_checkout(zCkout) ) continue;
+ if( g.localOpen ){
+ if( fossil_strcmp(zCkout, g.zLocalRoot)==0 ) continue;
+ zName = "alt-root:";
+ }else{
+ zName = "check-out:";
+ }
+ fossil_print("%-11s %-54s %s\n", zName, zCkout,
+ db_column_text(&s, 1));
+ }
+ db_finalize(&s);
+ db_prepare(&s, "SELECT substr(name,9), date(mtime,'unixepoch')"
+ " FROM config"
+ " WHERE name GLOB 'baseurl:*' ORDER BY mtime DESC");
+ while( db_step(&s)==SQLITE_ROW ){
+ fossil_print("access-url: %-54s %s\n", db_column_text(&s, 0),
+ db_column_text(&s, 1));
+ }
+ db_finalize(&s);
+}
+
+/*
+** Show the parent project, if any
+*/
+static void showParentProject(void){
+ const char *zParentCode;
+ zParentCode = db_get("parent-project-code",0);
+ if( zParentCode ){
+ fossil_print("derived-from: %s %s\n", zParentCode,
+ db_get("parent-project-name",""));
+ }
+}
+
+/*
+** COMMAND: info
+**
+** Usage: %fossil info ?VERSION | REPOSITORY_FILENAME? ?OPTIONS?
+**
+** With no arguments, provide information about the current tree.
+** If an argument is specified, provide information about the object
+** in the repository of the current tree that the argument refers
+** to. Or if the argument is the name of a repository, show
+** information about that repository.
+**
+** If the argument is a repository name, then the --verbose option shows
+** all known check-out locations for that repository and all URLs used
+** to access the repository. The --verbose is (currently) a no-op if
+** the argument is the name of an object within the repository.
+**
+** Use the "finfo" command to get information about a specific
+** file in a check-out.
+**
+** Options:
+** -R|--repository REPO Extract info from repository REPO
+** -v|--verbose Show extra information about repositories
+**
+** See also: [[annotate]], [[artifact]], [[finfo]], [[timeline]]
+*/
+void info_cmd(void){
+ i64 fsize;
+ int verboseFlag = find_option("verbose","v",0)!=0;
+ if( !verboseFlag ){
+ verboseFlag = find_option("detail","l",0)!=0; /* deprecated */
+ }
+
+ if( g.argc==3
+ && file_isfile(g.argv[2], ExtFILE)
+ && (fsize = file_size(g.argv[2], ExtFILE))>0
+ && (fsize&0x1ff)==0
+ ){
+ db_open_config(0, 0);
+ db_open_repository(g.argv[2]);
+ db_record_repository_filename(g.argv[2]);
+ fossil_print("project-name: %s\n", db_get("project-name", "<unnamed>"));
+ fossil_print("project-code: %s\n", db_get("project-code", "<none>"));
+ showParentProject();
+ extraRepoInfo();
+ return;
+ }
+ db_find_and_open_repository(OPEN_OK_NOT_FOUND,0);
+ verify_all_options();
+ if( g.argc==2 ){
+ int vid;
+ if( g.repositoryOpen ){
+ db_record_repository_filename(0);
+ fossil_print("project-name: %s\n", db_get("project-name", "<unnamed>"));
+ }else{
+ db_open_config(0,1);
+ }
+ if( g.localOpen ){
+ fossil_print("repository: %s\n", db_repository_filename());
+ fossil_print("local-root: %s\n", g.zLocalRoot);
+ }
+ if( verboseFlag && g.repositoryOpen ){
+ extraRepoInfo();
+ }
+ if( g.zConfigDbName ){
+ fossil_print("config-db: %s\n", g.zConfigDbName);
+ }
+ if( g.repositoryOpen ){
+ fossil_print("project-code: %s\n", db_get("project-code", ""));
+ showParentProject();
+ vid = g.localOpen ? db_lget_int("checkout", 0) : 0;
+ if( vid ){
+ show_common_info(vid, "checkout:", 1, 1);
+ }
+ fossil_print("check-ins: %d\n",
+ db_int(-1, "SELECT count(*) FROM event WHERE type='ci' /*scan*/"));
+ }
+ if( verboseFlag || !g.repositoryOpen ){
+ Blob vx;
+ char *z;
+ fossil_version_blob(&vx, 0);
+ z = strstr(blob_str(&vx), "version");
+ if( z ){
+ z += 8;
+ }else{
+ z = blob_str(&vx);
+ }
+ fossil_print("fossil: %z\n", file_fullexename(g.nameOfExe));
+ fossil_print("version: %s", z);
+ blob_reset(&vx);
+ }
+ }else if( g.repositoryOpen ){
+ int rid;
+ rid = name_to_rid(g.argv[2]);
+ if( rid==0 ){
+ fossil_fatal("no such object: %s", g.argv[2]);
+ }
+ show_common_info(rid, "hash:", 1, 1);
+ }else{
+ fossil_fatal("Could not find or open a Fossil repository");
+ }
+}
+
+/*
+** Show the context graph (immediate parents and children) for
+** check-in rid and rid2. rid2 might be zero, in which case only
+** show the context for rid1.
+*/
+void render_checkin_context(
+ int rid, int rid2, /* One or two checkins for which to show context */
+ int mRCCFlags, /* Flags. 1: parents only, 2: shadow-select rid */
+ int mFlags /* Graph flags */
+){
+ Blob sql;
+ Stmt q;
+ int rx[2];
+ int i, n;
+ TimelineXtra xtra;
+
+ rx[0] = rid;
+ rx[1] = rid2;
+ n = rid2 ? 2 : 1;
+ blob_zero(&sql);
+ blob_append(&sql, timeline_query_for_www(), -1);
+
+ db_multi_exec(
+ "CREATE TEMP TABLE IF NOT EXISTS ok(rid INTEGER PRIMARY KEY);"
+ "DELETE FROM ok;"
+ );
+ for(i=0; i<n; i++){
+ db_multi_exec(
+ "INSERT OR IGNORE INTO ok VALUES(%d);"
+ "INSERT OR IGNORE INTO ok SELECT pid FROM plink WHERE cid=%d;",
+ rx[i], rx[i]
+ );
+ }
+ if( (mRCCFlags & 0x01)==0 ){
+ for(i=0; i<n; i++){
+ db_multi_exec(
+ "INSERT OR IGNORE INTO ok SELECT cid FROM plink WHERE pid=%d;", rx[i]
+ );
+ if( db_table_exists("repository","cherrypick") ){
+ db_multi_exec(
+ "INSERT OR IGNORE INTO ok "
+ " SELECT parentid FROM cherrypick WHERE childid=%d;"
+ "INSERT OR IGNORE INTO ok "
+ " SELECT childid FROM cherrypick WHERE parentid=%d;",
+ rx[i], rx[i]
+ );
+ }
+ }
+ }
+ blob_append_sql(&sql, " AND event.objid IN ok ORDER BY mtime DESC");
+ db_prepare(&q, "%s", blob_sql_text(&sql));
+ memset(&xtra, 0, sizeof(xtra));
+ if( mRCCFlags & 0x02 ){
+ xtra.currentRid = rid;
+ }else{
+ xtra.selectedRid = rid;
+ xtra.secondRid = rid2;
+ }
+ mFlags |= TIMELINE_GRAPH|TIMELINE_FILLGAPS|TIMELINE_NOSCROLL|
+ TIMELINE_XMERGE|TIMELINE_CHPICK;
+ www_print_timeline(&q, mFlags, &xtra);
+ db_finalize(&q);
+ blob_reset(&sql);
+}
+
+
+/*
+** Append the difference between artifacts to the output
+*/
+static void append_diff(
+ const char *zFrom, /* Diff from this artifact */
+ const char *zTo, /* ... to this artifact */
+ DiffConfig *pCfg /* The diff configuration */
+){
+ int fromid;
+ int toid;
+ Blob from, to;
+ if( zFrom ){
+ fromid = uuid_to_rid(zFrom, 0);
+ content_get(fromid, &from);
+ pCfg->zLeftHash = zFrom;
+ }else{
+ blob_zero(&from);
+ pCfg->zLeftHash = 0;
+ }
+ if( zTo ){
+ toid = uuid_to_rid(zTo, 0);
+ content_get(toid, &to);
+ }else{
+ blob_zero(&to);
+ }
+ if( pCfg->diffFlags & DIFF_SIDEBYSIDE ){
+ pCfg->diffFlags |= DIFF_HTML | DIFF_NOTTOOBIG;
+ }else{
+ pCfg->diffFlags |= DIFF_LINENO | DIFF_HTML | DIFF_NOTTOOBIG;
+ }
+ text_diff(&from, &to, cgi_output_blob(), pCfg);
+ pCfg->zLeftHash = 0;
+ blob_reset(&from);
+ blob_reset(&to);
+}
+
+/*
+** The append_file_change_line() routine updates an array of counts
+** for various kinds of changes.
+*/
+#define FCHNG_NEW 0
+#define FCHNG_DELETE 1
+#define FCHNG_RENAME 2
+#define FCHNG_MODE 3
+#define FCHNG_EDIT 4
+#define N_FCHNG 5
+
+/*
+** Write a line of web-page output that shows changes that have occurred
+** to a file between two check-ins.
+*/
+static void append_file_change_line(
+ const char *zCkin, /* The check-in on which the change occurs */
+ const char *zName, /* Name of the file that has changed */
+ const char *zOld, /* blob.uuid before change. NULL for added files */
+ const char *zNew, /* blob.uuid after change. NULL for deletes */
+ const char *zOldName, /* Prior name. NULL if no name change. */
+ DiffConfig *pCfg, /* Flags for text_diff() or NULL to omit all */
+ int mperm, /* executable or symlink permission for zNew */
+ int *aChng /* Change count array */
+){
+ @ <div class='file-change-line'><span>
+ /* Maintenance reminder: the extra level of SPAN is for
+ ** arranging new elements via JS. */
+ if( !g.perm.Hyperlink ){
+ if( zNew==0 ){
+ @ Deleted %h(zName).
+ aChng[FCHNG_DELETE]++;
+ }else if( zOld==0 ){
+ @ Added %h(zName).
+ aChng[FCHNG_NEW]++;
+ }else if( zOldName!=0 && fossil_strcmp(zName,zOldName)!=0 ){
+ @ Name change from %h(zOldName) to %h(zName).
+ aChng[FCHNG_RENAME]++;
+ }else if( fossil_strcmp(zNew, zOld)==0 ){
+ if( mperm==PERM_EXE ){
+ @ %h(zName) became executable.
+ }else if( mperm==PERM_LNK ){
+ @ %h(zName) became a symlink.
+ }else{
+ @ %h(zName) became a regular file.
+ }
+ aChng[FCHNG_MODE]++;
+ }else{
+ @ Changes to %h(zName).
+ aChng[FCHNG_EDIT]++;
+ }
+ @ </span></div>
+ if( pCfg ){
+ append_diff(zOld, zNew, pCfg);
+ }
+ }else{
+ const char *zCkin2 =
+ mprintf(validate16(zCkin, -1) ? "%!S" : "%T"/*works-like:"%s"*/, zCkin);
+ if( zOld && zNew ){
+ if( fossil_strcmp(zOld, zNew)!=0 ){
+ if( zOldName!=0 && fossil_strcmp(zName,zOldName)!=0 ){
+ @ Renamed and modified
+ @ %z(href("%R/finfo?name=%T&m=%!S&ci=%s",zOldName,zOld,zCkin2))\
+ @ %h(zOldName)</a>
+ @ %z(href("%R/artifact/%!S",zOld))[%S(zOld)]</a>
+ @ to %z(href("%R/finfo?name=%T&m=%!S&ci=%s",zName,zNew,zCkin2))\
+ @ %h(zName)</a>
+ @ %z(href("%R/artifact/%!S",zNew))[%S(zNew)]</a>.
+ aChng[FCHNG_RENAME]++;
+ }else{
+ @ Modified %z(href("%R/finfo?name=%T&m=%!S&ci=%s",zName,zNew,zCkin2))\
+ @ %h(zName)</a>
+ @ from %z(href("%R/artifact/%!S",zOld))[%S(zOld)]</a>
+ @ to %z(href("%R/artifact/%!S",zNew))[%S(zNew)]</a>.
+ }
+ aChng[FCHNG_EDIT]++;
+ }else if( zOldName!=0 && fossil_strcmp(zName,zOldName)!=0 ){
+ @ Name change
+ @ from %z(href("%R/finfo?name=%T&m=%!S&ci=%s",zOldName,zOld,zCkin2))\
+ @ %h(zOldName)</a>
+ @ to %z(href("%R/finfo?name=%T&m=%!S&ci=%s",zName,zNew,zCkin2))\
+ @ %h(zName)</a>.
+ aChng[FCHNG_RENAME]++;
+ }else{
+ @ %z(href("%R/finfo?name=%T&m=%!S&ci=%s",zName,zNew,zCkin2))\
+ @ %h(zName)</a> became
+ if( mperm==PERM_EXE ){
+ @ executable with contents
+ }else if( mperm==PERM_LNK ){
+ @ a symlink with target
+ }else{
+ @ a regular file with contents
+ }
+ @ %z(href("%R/artifact/%!S",zNew))[%S(zNew)]</a>.
+ aChng[FCHNG_MODE]++;
+ }
+ }else if( zOld ){
+ @ Deleted %z(href("%R/finfo?name=%T&m=%!S&ci=%s",zName,zOld,zCkin2))\
+ @ %h(zName)</a> version %z(href("%R/artifact/%!S",zOld))[%S(zOld)]</a>.
+ aChng[FCHNG_DELETE]++;
+ }else{
+ @ Added %z(href("%R/finfo?name=%T&m=%!S&ci=%s",zName,zNew,zCkin2))\
+ @ %h(zName)</a> version %z(href("%R/artifact/%!S",zNew))[%S(zNew)]</a>.
+ aChng[FCHNG_NEW]++;
+ }
+ if( zOld && zNew && fossil_strcmp(zOld,zNew)!=0 ){
+ if( pCfg ){
+ @ </span></div>
+ append_diff(zOld, zNew, pCfg);
+ }else{
+ @ %z(href("%R/fdiff?v1=%!S&v2=%!S",zOld,zNew))[diff]</a>
+ @ </span></div>
+ }
+ }else{
+ @ </span></div>
+ }
+ }
+}
+
+/*
+** Generate javascript to enhance HTML diffs.
+*/
+void append_diff_javascript(int diffType){
+ if( diffType==0 ) return;
+ builtin_fossil_js_bundle_or("diff", NULL);
+}
+
+/*
+** Construct an appropriate diffFlag for text_diff() based on query
+** parameters and the to boolean arguments.
+*/
+DiffConfig *construct_diff_flags(int diffType, DiffConfig *pCfg){
+ u64 diffFlags = 0; /* Zero means do not show any diff */
+ if( diffType>0 ){
+ int x;
+ if( diffType==2 ) diffFlags = DIFF_SIDEBYSIDE;
+ if( P_NoBot("w") ) diffFlags |= DIFF_IGNORE_ALLWS;
+ if( PD_NoBot("noopt",0)!=0 ) diffFlags |= DIFF_NOOPT;
+ diffFlags |= DIFF_STRIP_EOLCR;
+ diff_config_init(pCfg, diffFlags);
+
+ /* "dc" query parameter determines lines of context */
+ x = atoi(PD("dc","7"));
+ if( x>0 ) pCfg->nContext = x;
+
+ /* The "noopt" parameter disables diff optimization */
+ return pCfg;
+ }else{
+ diff_config_init(pCfg, 0);
+ return 0;
+ }
+}
+
+/*
+** WEBPAGE: ci_tags
+** URL: /ci_tags?name=ARTIFACTID
+**
+** Show all tags and properties for a given check-in.
+**
+** This information used to be part of the main /ci page, but it is of
+** marginal usefulness. Better to factor it out into a sub-screen.
+*/
+void ci_tags_page(void){
+ const char *zHash;
+ int rid;
+ Stmt q;
+ int cnt = 0;
+ Blob sql;
+ char const *zType;
+ TimelineXtra xtra;
+
+ login_check_credentials();
+ if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
+ rid = name_to_rid_www("name");
+ if( rid==0 ){
+ style_header("Check-in Information Error");
+ @ No such object: %h(PD("name",""))
+ style_finish_page();
+ return;
+ }
+ cgi_check_for_malice();
+ zHash = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
+ style_header("Tags and Properties");
+ zType = whatis_rid_type_label(rid);
+ if(!zType) zType = "Artifact";
+ @ <h1>Tags and Properties for %s(zType) \
+ @ %z(href("%R/ci/%!S",zHash))%S(zHash)</a></h1>
+ db_prepare(&q,
+ "SELECT tag.tagid, tagname, "
+ " (SELECT uuid FROM blob WHERE rid=tagxref.srcid AND rid!=%d),"
+ " value, datetime(tagxref.mtime,toLocal()), tagtype,"
+ " (SELECT uuid FROM blob WHERE rid=tagxref.origid AND rid!=%d)"
+ " FROM tagxref JOIN tag ON tagxref.tagid=tag.tagid"
+ " WHERE tagxref.rid=%d"
+ " ORDER BY tagname /*sort*/", rid, rid, rid
+ );
+ while( db_step(&q)==SQLITE_ROW ){
+ const char *zTagname = db_column_text(&q, 1);
+ const char *zSrcUuid = db_column_text(&q, 2);
+ const char *zValue = db_column_text(&q, 3);
+ const char *zDate = db_column_text(&q, 4);
+ int tagtype = db_column_int(&q, 5);
+ const char *zOrigUuid = db_column_text(&q, 6);
+ cnt++;
+ if( cnt==1 ){
+ @ <ul>
+ }
+ @ <li>
+ if( tagtype==0 ){
+ @ <span class="infoTagCancelled">%h(zTagname)</span> cancelled
+ }else if( zValue ){
+ @ <span class="infoTag">%h(zTagname)=%h(zValue)</span>
+ }else {
+ @ <span class="infoTag">%h(zTagname)</span>
+ }
+ if( tagtype==2 ){
+ if( zOrigUuid && zOrigUuid[0] ){
+ @ inherited from
+ hyperlink_to_version(zOrigUuid);
+ }else{
+ @ propagates to descendants
+ }
+ }
+ if( zSrcUuid && zSrcUuid[0] ){
+ if( tagtype==0 ){
+ @ by
+ }else{
+ @ added by
+ }
+ hyperlink_to_version(zSrcUuid);
+ @ on
+ hyperlink_to_date(zDate,0);
+ }
+ @ </li>
+ }
+ db_finalize(&q);
+ if( cnt ){
+ @ </ul>
+ }
+ @ <div class="section">Context</div>
+ db_multi_exec(
+ "CREATE TEMP TABLE IF NOT EXISTS ok(rid INTEGER PRIMARY KEY);"
+ "DELETE FROM ok;"
+ "INSERT INTO ok VALUES(%d);"
+ "INSERT OR IGNORE INTO ok "
+ " SELECT tagxref.srcid"
+ " FROM tagxref JOIN tag ON tagxref.tagid=tag.tagid"
+ " WHERE tagxref.rid=%d;"
+ "INSERT OR IGNORE INTO ok "
+ " SELECT tagxref.origid"
+ " FROM tagxref JOIN tag ON tagxref.tagid=tag.tagid"
+ " WHERE tagxref.rid=%d;",
+ rid, rid, rid
+ );
+#if 0
+ db_multi_exec(
+ "SELECT tag.tagid, tagname, "
+ " (SELECT uuid FROM blob WHERE rid=tagxref.srcid AND rid!=%d),"
+ " value, datetime(tagxref.mtime,toLocal()), tagtype,"
+ " (SELECT uuid FROM blob WHERE rid=tagxref.origid AND rid!=%d)"
+ " FROM tagxref JOIN tag ON tagxref.tagid=tag.tagid"
+ " WHERE tagxref.rid=%d"
+ " ORDER BY tagname /*sort*/", rid, rid, rid
+ );
+#endif
+ blob_zero(&sql);
+ blob_append(&sql, timeline_query_for_www(), -1);
+ blob_append_sql(&sql, " AND event.objid IN ok ORDER BY mtime DESC");
+ db_prepare(&q, "%s", blob_sql_text(&sql));
+ memset(&xtra, 0, sizeof(xtra));
+ xtra.selectedRid = rid;
+ www_print_timeline(&q, TIMELINE_DISJOINT|TIMELINE_GRAPH|TIMELINE_NOSCROLL,
+ &xtra);
+ db_finalize(&q);
+ style_finish_page();
+}
+
+/*
+** Generate JS code that will adjust the change counts in the title
+** of the "changes_section" panel where "changes_section" is a JS id.
+**
+** The change counts are taken from g.diffCnt[] for edits, and from
+** the aChng[] parameter for additions, deletions, and modifications.
+** The aChng parameter may be NULL, in which case those factors are
+** ignored.
+*/
+static void adjust_changes_section_title(
+ int iLine, /* Souce code line number of caller */
+ int *aChng /* Change categories */
+){
+ sqlite3_str *pTxt = sqlite3_str_new(0);
+ if( g.diffCnt[0]>0 ){
+ sqlite3_str_appendf(pTxt, "%d file%s edited [+%d -%d lines]",
+ g.diffCnt[0], g.diffCnt[0]==1 ? "" : "s",
+ g.diffCnt[1], g.diffCnt[2]);
+ }
+ if( aChng ){
+ static const int aiType[] =
+ { FCHNG_NEW, FCHNG_DELETE, FCHNG_RENAME, FCHNG_MODE };
+ static const char *azType[] =
+ { "added", "deleted", "renamed", "mode change"};
+ int i;
+ for(i=0; i<4; i++){
+ int j = aiType[i];
+ if( aChng[j] ){
+ const char *zFiles = "";
+ if( sqlite3_str_length(pTxt)>0 ){
+ sqlite3_str_append(pTxt,", ", 2);
+ }else{
+ zFiles = i<3 && aChng[j]>1 ? " files" : " file";
+ }
+ sqlite3_str_appendf(pTxt,"%d%s %s", aChng[j], zFiles, azType[i]);
+ if( i==3 && aChng[j]>1 ) sqlite3_str_appendf(pTxt, "s");
+ }
+ }
+ }
+ if( sqlite3_str_length(pTxt)>0 ){
+ @ <script nonce='%h(style_nonce())'>;/* info.c:%d(iLine) */
+ @ document.getElementById('changes_section').textContent = \
+ @ 'Changes (%h(sqlite3_str_value(pTxt)))'
+ @ </script>
+ }
+ sqlite3_str_free(pTxt);
+}
+
+/*
+** Render a web-page diff of the changes in the working check-out
+*/
+static void ckout_normal_diff(int vid){
+ int diffType; /* 0: no diff, 1: unified, 2: side-by-side */
+ DiffConfig DCfg,*pCfg; /* Diff details */
+ const char *zW; /* The "w" query parameter */
+ int nChng; /* Number of changes */
+ Stmt q;
+
+ diffType = preferred_diff_type();
+ pCfg = construct_diff_flags(diffType, &DCfg);
+ nChng = db_int(0, "SELECT count(*) FROM vfile"
+ " WHERE vid=%d AND (deleted OR chnged OR rid==0)", vid);
+ if( nChng==0 ){
+ @ <p>No uncommitted changes</p>
+ return;
+ }
+ db_prepare(&q,
+ /* 0 1 2 3 4 5 6 */
+ "SELECT pathname, deleted, chnged , rid==0, rid, islink, uuid"
+ " FROM vfile LEFT JOIN blob USING(rid)"
+ " WHERE vid=%d"
+ " AND (deleted OR chnged OR rid==0)"
+ " ORDER BY pathname /*scan*/",
+ vid
+ );
+ if( DCfg.diffFlags & DIFF_SIDEBYSIDE ){
+ DCfg.diffFlags |= DIFF_HTML | DIFF_NOTTOOBIG;
+ }else{
+ DCfg.diffFlags |= DIFF_LINENO | DIFF_HTML | DIFF_NOTTOOBIG;
+ }
+ @ <div class="section" id="changes_section">Changes</div>
+ DCfg.diffFlags |= DIFF_NUMSTAT; /* Show stats in the 'Changes' section */
+ @ <div class="sectionmenu info-changes-menu">
+ zW = (DCfg.diffFlags&DIFF_IGNORE_ALLWS)?"&w":"";
+ if( diffType!=1 ){
+ @ %z(chref("button","%R?diff=1%s",zW))Unified&nbsp;Diff</a>
+ }
+ if( diffType!=2 ){
+ @ %z(chref("button","%R?diff=2%s",zW))Side-by-Side&nbsp;Diff</a>
+ }
+ if( diffType!=0 ){
+ if( *zW ){
+ @ %z(chref("button","%R?diff=%d",diffType))\
+ @ Show&nbsp;Whitespace&nbsp;Changes</a>
+ }else{
+ @ %z(chref("button","%R?diff=%d&w",diffType))Ignore&nbsp;Whitespace</a>
+ }
+ }
+ @ </div>
+ while( db_step(&q)==SQLITE_ROW ){
+ const char *zTreename = db_column_text(&q,0);
+ int isDeleted = db_column_int(&q, 1);
+ int isChnged = db_column_int(&q,2);
+ int isNew = db_column_int(&q,3);
+ int srcid = db_column_int(&q, 4);
+ int isLink = db_column_int(&q, 5);
+ const char *zUuid = db_column_text(&q, 6);
+ int showDiff = 1;
+
+ DCfg.diffFlags &= (~DIFF_FILE_MASK);
+ @ <div class='file-change-line'><span>
+ if( isDeleted ){
+ @ DELETED %h(zTreename)
+ DCfg.diffFlags |= DIFF_FILE_DELETED;
+ showDiff = 0;
+ }else if( file_access(zTreename, F_OK) ){
+ @ MISSING %h(zTreename)
+ showDiff = 0;
+ }else if( isNew ){
+ @ ADDED %h(zTreename)
+ DCfg.diffFlags |= DIFF_FILE_ADDED;
+ srcid = 0;
+ showDiff = 0;
+ }else if( isChnged==3 ){
+ @ ADDED_BY_MERGE %h(zTreename)
+ DCfg.diffFlags |= DIFF_FILE_ADDED;
+ srcid = 0;
+ showDiff = 0;
+ }else if( isChnged==5 ){
+ @ ADDED_BY_INTEGRATE %h(zTreename)
+ DCfg.diffFlags |= DIFF_FILE_ADDED;
+ srcid = 0;
+ showDiff = 0;
+ }else{
+ @ CHANGED %h(zTreename)
+ }
+ @ </span></div>
+ if( showDiff && pCfg ){
+ Blob old, new;
+ if( !isLink != !file_islink(zTreename) ){
+ @ %s(DIFF_CANNOT_COMPUTE_SYMLINK)
+ continue;
+ }
+ if( srcid>0 ){
+ content_get(srcid, &old);
+ pCfg->zLeftHash = zUuid;
+ }else{
+ blob_zero(&old);
+ pCfg->zLeftHash = 0;
+ }
+ blob_read_from_file(&new, zTreename, ExtFILE);
+ text_diff(&old, &new, cgi_output_blob(), pCfg);
+ blob_reset(&old);
+ blob_reset(&new);
+ }
+ }
+ db_finalize(&q);
+ adjust_changes_section_title(__LINE__, 0);
+ append_diff_javascript(diffType);
+}
+
+/*
+** Render a web-page diff of the changes in the working check-out to
+** an external reference.
+*/
+static void ckout_external_base_diff(int vid, const char *zExBase){
+ int diffType; /* 0: no diff, 1: unified, 2: side-by-side */
+ DiffConfig DCfg,*pCfg; /* Diff details */
+ const char *zW; /* The "w" query parameter */
+ Stmt q;
+
+ diffType = preferred_diff_type();
+ pCfg = construct_diff_flags(diffType, &DCfg);
+ db_prepare(&q,
+ "SELECT pathname FROM vfile WHERE vid=%d ORDER BY pathname", vid
+ );
+ if( DCfg.diffFlags & DIFF_SIDEBYSIDE ){
+ DCfg.diffFlags |= DIFF_HTML | DIFF_NOTTOOBIG;
+ }else{
+ DCfg.diffFlags |= DIFF_LINENO | DIFF_HTML | DIFF_NOTTOOBIG;
+ }
+ @ <div class="section" id="changes_section">Changes</div>
+ DCfg.diffFlags |= DIFF_NUMSTAT; /* Show stats in the 'Changes' section */
+ @ <div class="sectionmenu info-changes-menu">
+ zW = (DCfg.diffFlags&DIFF_IGNORE_ALLWS)?"&w":"";
+ if( diffType!=1 ){
+ @ %z(chref("button","%R?diff=1&exbase=%h%s",zExBase,zW))\
+ @ Unified&nbsp;Diff</a>
+ }
+ if( diffType!=2 ){
+ @ %z(chref("button","%R?diff=2&exbase=%h%s",zExBase,zW))\
+ @ Side-by-Side&nbsp;Diff</a>
+ }
+ if( diffType!=0 ){
+ if( *zW ){
+ @ %z(chref("button","%R?diff=%d&exbase=%h",diffType,zExBase))\
+ @ Show&nbsp;Whitespace&nbsp;Changes</a>
+ }else{
+ @ %z(chref("button","%R?diff=%d&exbase=%h&w",diffType,zExBase))\
+ @ Ignore&nbsp;Whitespace</a>
+ }
+ }
+ @ </div>
+ while( db_step(&q)==SQLITE_ROW ){
+ const char *zFile; /* Name of file in the repository */
+ char *zLhs; /* Full name of left-hand side file */
+ char *zRhs; /* Full name of right-hand side file */
+ Blob rhs; /* Full text of RHS */
+ Blob lhs; /* Full text of LHS */
+
+ zFile = db_column_text(&q,0);
+ zLhs = mprintf("%s/%s", zExBase, zFile);
+ zRhs = mprintf("%s%s", g.zLocalRoot, zFile);
+ if( file_size(zLhs, ExtFILE)<0 ){
+ @ <div class='file-change-line'><span>
+ @ Missing from external baseline: %h(zFile)
+ @ </span></div>
+ }else{
+ blob_read_from_file(&lhs, zLhs, ExtFILE);
+ blob_read_from_file(&rhs, zRhs, ExtFILE);
+ if( blob_size(&lhs)!=blob_size(&rhs)
+ || memcmp(blob_buffer(&lhs), blob_buffer(&rhs), blob_size(&lhs))!=0
+ ){
+ @ <div class='file-change-line'><span>
+ @ Changes to %h(zFile)
+ @ </span></div>
+ if( pCfg ){
+ char *zFullFN;
+ char *zHexFN;
+ zFullFN = file_canonical_name_dup(zLhs);
+ zHexFN = mprintf("x%H", zFullFN);
+ fossil_free(zFullFN);
+ pCfg->zLeftHash = zHexFN;
+ text_diff(&lhs, &rhs, cgi_output_blob(), pCfg);
+ pCfg->zLeftHash = 0;
+ fossil_free(zHexFN);
+ }
+ }
+ blob_reset(&lhs);
+ blob_reset(&rhs);
+ }
+ fossil_free(zLhs);
+ fossil_free(zRhs);
+ }
+ db_finalize(&q);
+ adjust_changes_section_title(__LINE__, 0);
+ append_diff_javascript(diffType);
+}
+
+/*
+** WEBPAGE: ckout
+**
+** Show information about the current checkout. This page only functions
+** if the web server is run on a loopback interface (in other words, was
+** started using "fossil ui" or similar) from within an open check-out.
+**
+** If the "exbase=PATH" query parameter is provided, then the diff shown
+** uses the files in PATH as the baseline. This is the same as using
+** the "--from PATH" argument to the "fossil diff" command-line. In fact,
+** when using "fossil ui --from PATH", the --from argument becomes the value
+** of the exbase query parameter for the start page. Note that if PATH
+** is a pure hexadecimal string, it is decoded first before being used as
+** the pathname. Real pathnames should contain at least one directory
+** separator character.
+**
+** Other query parameters related to diffs are also accepted.
+*/
+void ckout_page(void){
+ int vid;
+ const char *zHome; /* Home directory */
+ int nHome;
+ const char *zExBase;
+ char *zHostname;
+ char *zCwd;
+
+ if( !cgi_is_loopback(g.zIpAddr) || !db_open_local(0) ){
+ cgi_redirectf("%R/home");
+ return;
+ }
+ file_chdir(g.zLocalRoot, 0);
+ vid = db_lget_int("checkout", 0);
+ db_unprotect(PROTECT_ALL);
+ vfile_check_signature(vid, CKSIG_ENOTFILE);
+ db_protect_pop();
+ style_set_current_feature("vinfo");
+ zHostname = fossil_hostname();
+ zCwd = file_getcwd(0,0);
+ zHome = fossil_getenv("HOME");
+ if( zHome ){
+ nHome = (int)strlen(zHome);
+ if( strncmp(zCwd, zHome, nHome)==0 && zCwd[nHome]=='/' ){
+ zCwd = mprintf("~%s", zCwd+nHome);
+ }
+ }else{
+ nHome = 0;
+ }
+ if( zHostname ){
+ style_header("Checkout Status: %h on %h", zCwd, zHostname);
+ }else{
+ style_header("Checkout Status: %h", zCwd);
+ }
+ render_checkin_context(vid, 0, 2, 0);
+ @ <hr>
+ zExBase = P("exbase");
+ if( zExBase && zExBase[0] ){
+ char *zPath = decode16_dup(zExBase);
+ char *zCBase = file_canonical_name_dup(zPath?zPath:zExBase);
+ if( nHome && strncmp(zCBase, zHome, nHome)==0 && zCBase[nHome]=='/' ){
+ @ <p>Using external baseline: ~%h(zCBase+nHome)</p>
+ }else{
+ @ <p>Using external baseline: %h(zCBase)</p>
+ }
+ ckout_external_base_diff(vid, zCBase);
+ fossil_free(zCBase);
+ fossil_free(zPath);
+ }else{
+ ckout_normal_diff(vid);
+ }
+ style_finish_page();
+}
+
+/*
+** SETTING: path-to-tag width=40
+**
+** If the path-to-tag setting is defined and is not an empty string
+** then the /info page for checkin HASH will display a link to
+** a timeline showing an abbreviated path from the HASH checkin to
+** the next checkin that has a tag which is the value of this setting.
+**
+** A typical value for this setting is "release", in which case a
+** "path-to-release" link is shown on /info pages of checkins.
+*/
+
+/*
+** WEBPAGE: vinfo
+** WEBPAGE: ci
+** URL: /ci/ARTIFACTID
+** OR: /ci?name=ARTIFACTID
+**
+** Display information about a particular check-in. The exact
+** same information is shown on the /info page if the name query
+** parameter to /info describes a check-in.
+**
+** The ARTIFACTID can be a unique prefix for the HASH of the check-in,
+** or a tag or branch name that identifies the check-in.
+*/
+void ci_page(void){
+ Stmt q1, q2, q3;
+ int rid;
+ int isLeaf;
+ int diffType; /* 0: no diff, 1: unified, 2: side-by-side */
+ const char *zName; /* Name of the check-in to be displayed */
+ const char *zUuid; /* Hash of zName, found via blob.uuid */
+ const char *zParent; /* Hash of the parent check-in (if any) */
+ const char *zRe; /* regex parameter */
+ ReCompiled *pRe = 0; /* regex */
+ const char *zW; /* URL param for ignoring whitespace */
+ const char *zPage = "vinfo"; /* Page that shows diffs */
+ const char *zBrName; /* Branch name */
+ DiffConfig DCfg,*pCfg; /* Type of diff */
+ int aChng[N_FCHNG]; /* Counts of change types */
+
+ memset(aChng, 0, sizeof(aChng));
+ login_check_credentials();
+ if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
+ style_set_current_feature("vinfo");
+ zName = P("name");
+ rid = name_to_rid_www("name");
+ if( rid==0 ){
+ style_header("Check-in Information Error");
+ @ No such object: %h(zName)
+ style_finish_page();
+ return;
+ }
+ zRe = P("regex");
+ if( zRe ) fossil_re_compile(&pRe, zRe, 0);
+ zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
+ zParent = db_text(0,
+ "SELECT uuid FROM plink, blob"
+ " WHERE plink.cid=%d AND blob.rid=plink.pid AND plink.isprim",
+ rid
+ );
+ isLeaf = !db_exists("SELECT 1 FROM plink WHERE pid=%d", rid);
+ db_prepare(&q1,
+ "SELECT uuid," /* 0 */
+ " datetime(mtime,toLocal(),'subsec')," /* 1 */
+ " user," /* 2 */
+ " comment," /* 3 */
+ " datetime(omtime,toLocal(),'subsec')," /* 4 */
+ " mtime," /* 5 */
+ " strftime('%%Y-%%m-%%dT%%H:%%M:%%fZ',mtime)" /* 6 */
+ " FROM blob, event"
+ " WHERE blob.rid=%d"
+ " AND event.objid=%d",
+ rid, rid
+ );
+ zBrName = branch_of_rid(rid);
+
+ diffType = preferred_diff_type();
+ cgi_check_for_malice();
+ if( db_step(&q1)==SQLITE_ROW ){
+ const char *zUuid = db_column_text(&q1, 0);
+ int nUuid = db_column_bytes(&q1, 0);
+ char *zEUser, *zEComment;
+ const char *zUser;
+ const char *zOrigUser;
+ const char *zComment;
+ const char *zDate, *zZulu;
+ const char *zOrigDate;
+ int okWiki = 0;
+ Blob wiki_read_links = BLOB_INITIALIZER;
+ Blob wiki_add_links = BLOB_INITIALIZER;
+
+ Th_StoreUnsafe("current_checkin", zName);
+ style_header("Check-in [%S]", zUuid);
+ login_anonymous_available();
+ zEUser = db_text(0,
+ "SELECT value FROM tagxref"
+ " WHERE tagid=%d AND rid=%d AND tagtype>0",
+ TAG_USER, rid);
+ zEComment = db_text(0,
+ "SELECT value FROM tagxref WHERE tagid=%d AND rid=%d",
+ TAG_COMMENT, rid);
+ zOrigUser = db_column_text(&q1, 2);
+ zUser = zEUser ? zEUser : zOrigUser;
+ zComment = db_column_text(&q1, 3);
+ zDate = db_column_text(&q1,1);
+ zOrigDate = db_column_text(&q1, 4);
+ if( zOrigDate==0 ) zOrigDate = zDate;
+ zZulu = db_column_text(&q1, 6);
+ @ <div class="section accordion">Overview</div>
+ @ <div class="accordion_panel">
+ @ <table class="label-value">
+ @ <tr><th>Comment:</th><td class="infoComment">\
+ @ %!W(zEComment?zEComment:zComment)</td></tr>
+
+ /* The Download: line */
+ if( g.perm.Zip ){
+ @ <tr><th>Downloads:</th><td>
+ if( robot_would_be_restricted("download") ){
+ @ See separate %z(href("%R/rchvdwnld/%!S",zUuid))download page</a>
+ }else{
+ char *zBase = archive_base_name(rid);
+ @ %z(href("%R/tarball/%s.tar.gz",zBase))Tarball</a>
+ @ | %z(href("%R/zip/%s.zip",zBase))ZIP archive</a>
+ if( g.zLogin!=0 ){
+ @ | %z(href("%R/sqlar/%s.sqlar",zBase))\
+ @ SQL archive</a></td></tr>
+ }
+ fossil_free(zBase);
+ }
+ }
+
+ @ <tr><th>Timelines:</th><td>
+ @ %z(href("%R/timeline?f=%!S&unhide",zUuid))family</a>
+ if( zParent ){
+ @ | %z(href("%R/timeline?p=%!S&unhide",zUuid))ancestors</a>
+ }
+ if( !isLeaf ){
+ char *zPathTo = db_get("path-to-tag",0);
+ @ | %z(href("%R/timeline?d=%!S&unhide",zUuid))descendants</a>
+ if( zPathTo!=0 && zPathTo[0]!=0 ){
+ @ | %z(href("%R/timeline?from=%!S&ft=%t&min",zUuid,zPathTo))\
+ @ path-to-%h(zPathTo)</a>
+ }
+ }
+ if( zParent && !isLeaf ){
+ @ | %z(href("%R/timeline?dp=%!S&unhide",zUuid))both</a>
+ }
+ db_prepare(&q2,"SELECT substr(tag.tagname,5) FROM tagxref, tag "
+ " WHERE rid=%d AND tagtype>0 "
+ " AND tag.tagid=tagxref.tagid "
+ " AND +tag.tagname GLOB 'sym-*'", rid);
+ while( db_step(&q2)==SQLITE_ROW ){
+ const char *zTagName = db_column_text(&q2, 0);
+ if( fossil_strcmp(zTagName,zBrName)==0 ){
+ cgi_printf(" | ");
+ style_copy_button(1, "name-br", 0, 0, 0, "%z%h</a>",
+ href("%R/timeline?r=%T&unhide",zTagName), zTagName);
+ cgi_printf("\n");
+ if( wiki_tagid2("branch",zTagName)!=0 ){
+ blob_appendf(&wiki_read_links, " | %z%h</a>",
+ href("%R/%s?name=branch/%h",
+ (g.perm.Write && g.perm.WrWiki)
+ ? "wikiedit" : "wiki",
+ zTagName), zTagName);
+ }else if( g.perm.Write && g.perm.WrWiki ){
+ blob_appendf(&wiki_add_links, " | %z%h</a>",
+ href("%R/wikiedit?name=branch/%h",zTagName), zTagName);
+ }
+ }else{
+ @ | %z(href("%R/timeline?t=%T&unhide",zTagName))%h(zTagName)</a>
+ if( wiki_tagid2("tag",zTagName)!=0 ){
+ blob_appendf(&wiki_read_links, " | %z%h</a>",
+ href("%R/wiki?name=tag/%h",zTagName), zTagName);
+ }else if( g.perm.Write && g.perm.WrWiki ){
+ blob_appendf(&wiki_add_links, " | %z%h</a>",
+ href("%R/wikiedit?name=tag/%h",zTagName), zTagName);
+ }
+ }
+ }
+ db_finalize(&q2);
+ @ </td></tr>
+
+ @ <tr><th>Files:</th>
+ @ <td>
+ @ %z(href("%R/tree?ci=%!S",zUuid))files</a>
+ @ | %z(href("%R/fileage?name=%!S",zUuid))file ages</a>
+ @ | %z(href("%R/tree?nofiles&type=tree&ci=%!S",zUuid))folders</a>
+ @ </td>
+ @ </tr>
+
+ @ <tr><th>%s(hname_alg(nUuid)):</th><td>
+ style_copy_button(1, "hash-ci", 0, 2, 0, "%.32s<wbr>%s", zUuid, zUuid+32);
+ if( g.perm.Setup ){
+ @ (Record ID: %d(rid))
+ }
+ @ </td></tr>
+ @ <tr><th>User&nbsp;&amp;&nbsp;Date:</th><td>
+ hyperlink_to_user(zUser,zDate," on ");
+ style_copy_button(1, 0, 0, 0, zZulu,
+ "%z%h</a>", href("%R/timeline?c=%T",zDate), zDate);
+ @ </td></tr>
+ // hyperlink_to_date(zDate, "</td></tr>");
+ if( zEComment ){
+ @ <tr><th>Original&nbsp;Comment:</th>
+ @ <td class="infoComment">%!W(zComment)</td></tr>
+ }
+ if( fossil_strcmp(zDate, zOrigDate)!=0
+ || fossil_strcmp(zOrigUser, zUser)!=0
+ ){
+ @ <tr><th>Original&nbsp;User&nbsp;&amp;&nbsp;Date:</th><td>
+ hyperlink_to_user(zOrigUser,zOrigDate," on ");
+ hyperlink_to_date(zOrigDate, "</td></tr>");
+ }
+ if( g.perm.Admin ){
+ db_prepare(&q2,
+ "SELECT rcvfrom.ipaddr, user.login, datetime(rcvfrom.mtime),"
+ " blob.rcvid"
+ " FROM blob JOIN rcvfrom USING(rcvid) LEFT JOIN user USING(uid)"
+ " WHERE blob.rid=%d",
+ rid
+ );
+ if( db_step(&q2)==SQLITE_ROW ){
+ const char *zIpAddr = db_column_text(&q2, 0);
+ const char *zUser = db_column_text(&q2, 1);
+ const char *zDate = db_column_text(&q2, 2);
+ int rcvid = db_column_int(&q2,3);
+ if( zUser==0 || zUser[0]==0 ) zUser = "unknown";
+ @ <tr><th>Received&nbsp;From:</th>
+ @ <td>%h(zUser) @ %h(zIpAddr) on %s(zDate) \
+ @ (<a href="%R/rcvfrom?rcvid=%d(rcvid)">Rcvid %d(rcvid)</a>)</td></tr>
+ }
+ db_finalize(&q2);
+ }
+
+ /* Only show links to edit wiki pages if the users can read wiki
+ ** and if the wiki pages already exist */
+ if( g.perm.WrWiki
+ && g.perm.RdWiki
+ && g.perm.Write
+ && ((okWiki = wiki_tagid2("checkin",zUuid))!=0 ||
+ blob_size(&wiki_read_links)>0)
+ && db_get_boolean("wiki-about",1)
+ ){
+ const char *zLinks = blob_str(&wiki_read_links);
+ @ <tr><th>Edit&nbsp;Wiki:</th><td>\
+ if( okWiki ){
+ @ %z(href("%R/wikiedit?name=checkin/%s",zUuid))this check-in</a>\
+ }else if( zLinks[0] ){
+ zLinks += 3;
+ }
+ @ %s(zLinks)</td></tr>
+ }
+
+ /* Only show links to create new wiki pages if the users can write wiki
+ ** and if the wiki pages do not already exist */
+ if( g.perm.WrWiki
+ && g.perm.RdWiki
+ && g.perm.Write
+ && (blob_size(&wiki_add_links)>0 || !okWiki)
+ && db_get_boolean("wiki-about",1)
+ ){
+ const char *zLinks = blob_str(&wiki_add_links);
+ @ <tr><th>Add&nbsp;Wiki:</th><td>\
+ if( !okWiki ){
+ @ %z(href("%R/wikiedit?name=checkin/%s",zUuid))this check-in</a>\
+ }else if( zLinks[0] ){
+ zLinks += 3;
+ }
+ @ %s(zLinks)</td></tr>
+ }
+
+ if( g.perm.Hyperlink ){
+ const char *zMainBranch = db_main_branch();
+ @ <tr><th>Other&nbsp;Links:</th>
+ @ <td>
+ if( fossil_strcmp(zBrName, zMainBranch)!=0 ){
+ @ %z(href("%R/vdiff?branch=%!S", zUuid))branch diff</a> |
+ }
+ @ %z(href("%R/artifact/%!S",zUuid))manifest</a>
+ @ | %z(href("%R/ci_tags/%!S",zUuid))tags</a>
+ if( g.perm.Admin ){
+ @ | %z(href("%R/mlink?ci=%!S",zUuid))mlink table</a>
+ }
+ if( g.anon.Write ){
+ @ | %z(href("%R/ci_edit?r=%!S",zUuid))edit</a>
+ }
+ @ </td>
+ @ </tr>
+ }
+ @ </table>
+ blob_reset(&wiki_read_links);
+ blob_reset(&wiki_add_links);
+ }else{
+ style_header("Check-in Information");
+ login_anonymous_available();
+ }
+ db_finalize(&q1);
+ @ </div>
+ builtin_request_js("accordion.js");
+ if( !PB("nowiki") ){
+ wiki_render_associated("checkin", zUuid, 0);
+ }
+ render_backlink_graph(zUuid,
+ "<div class=\"section accordion\">References</div>\n");
+ @ <div class="section accordion">Context</div><div class="accordion_panel">
+ render_checkin_context(rid, 0, 2, 0);
+ @ </div><div class="section accordion" id="changes_section">Changes</div>
+ @ <div class="accordion_panel">
+ @ <div class="sectionmenu info-changes-menu">
+ /* ^^^ .info-changes-menu is used by diff scroll sync */
+ pCfg = construct_diff_flags(diffType, &DCfg);
+ DCfg.diffFlags |= DIFF_NUMSTAT; /* Show stats in the 'Changes' section */
+ DCfg.pRe = pRe;
+ zW = (DCfg.diffFlags&DIFF_IGNORE_ALLWS)?"&w":"";
+ if( diffType!=1 ){
+ @ %z(chref("button","%R/%s/%T?diff=1%s",zPage,zName,zW))\
+ @ Unified&nbsp;Diff</a>
+ }
+ if( diffType!=2 ){
+ @ %z(chref("button","%R/%s/%T?diff=2%s",zPage,zName,zW))\
+ @ Side-by-Side&nbsp;Diff</a>
+ }
+ if( diffType!=0 ){
+ if( *zW ){
+ @ %z(chref("button","%R/%s/%T?diff=%d",zPage,zName,diffType))
+ @ Show&nbsp;Whitespace&nbsp;Changes</a>
+ }else{
+ @ %z(chref("button","%R/%s/%T?diff=%d&w",zPage,zName,diffType))
+ @ Ignore&nbsp;Whitespace</a>
+ }
+ }
+ if( zParent ){
+ @ %z(chref("button","%R/vpatch?from=%!S&to=%!S",zParent,zUuid))
+ @ Patch</a>
+ }
+ if( g.perm.Admin ){
+ @ %z(chref("button","%R/mlink?ci=%!S",zUuid))MLink Table</a>
+ }
+ @ </div>
+ if( pRe ){
+ @ <p><b>Only differences that match regular expression "%h(zRe)"
+ @ are shown.</b></p>
+ }
+ db_prepare(&q3,
+ "SELECT name,"
+ " mperm,"
+ " (SELECT uuid FROM blob WHERE rid=mlink.pid),"
+ " (SELECT uuid FROM blob WHERE rid=mlink.fid),"
+ " (SELECT name FROM filename WHERE filename.fnid=mlink.pfnid)"
+ " FROM mlink JOIN filename ON filename.fnid=mlink.fnid"
+ " WHERE mlink.mid=%d AND NOT mlink.isaux"
+ " AND (mlink.fid>0"
+ " OR mlink.fnid NOT IN (SELECT pfnid FROM mlink WHERE mid=%d))"
+ " ORDER BY name /*sort*/",
+ rid, rid
+ );
+ while( db_step(&q3)==SQLITE_ROW ){
+ const char *zName = db_column_text(&q3,0);
+ int mperm = db_column_int(&q3, 1);
+ const char *zOld = db_column_text(&q3,2);
+ const char *zNew = db_column_text(&q3,3);
+ const char *zOldName = db_column_text(&q3, 4);
+ append_file_change_line(zUuid, zName, zOld, zNew, zOldName,
+ pCfg,mperm,aChng);
+ }
+ db_finalize(&q3);
+ @ </div>
+ if( diffType!=0 ){
+ adjust_changes_section_title(__LINE__, aChng);
+ }
+ append_diff_javascript(diffType);
+ style_finish_page();
+}
+
+/*
+** WEBPAGE: winfo
+** URL: /winfo?name=HASH
+**
+** Display information about a wiki page.
+*/
+void winfo_page(void){
+ int rid;
+ Manifest *pWiki;
+ char *zUuid;
+ char *zDate;
+ Blob wiki;
+ int modPending;
+ const char *zModAction;
+ int tagid;
+ int ridNext;
+
+ login_check_credentials();
+ if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; }
+ style_set_current_feature("winfo");
+ rid = name_to_rid_www("name");
+ if( rid==0 || (pWiki = manifest_get(rid, CFTYPE_WIKI, 0))==0 ){
+ style_header("Wiki Page Information Error");
+ @ No such object: %h(P("name"))
+ style_finish_page();
+ return;
+ }
+ if( g.perm.ModWiki && (zModAction = P("modaction"))!=0 ){
+ if( strcmp(zModAction,"delete")==0 ){
+ moderation_disapprove(rid);
+ /*
+ ** Next, check if the wiki page still exists; if not, we cannot
+ ** redirect to it.
+ */
+ if( db_exists("SELECT 1 FROM tagxref JOIN tag USING(tagid)"
+ " WHERE rid=%d AND tagname LIKE 'wiki-%%'", rid) ){
+ cgi_redirectf("%R/wiki?name=%T", pWiki->zWikiTitle);
+ /*NOTREACHED*/
+ }else{
+ cgi_redirectf("%R/modreq");
+ /*NOTREACHED*/
+ }
+ }
+ if( strcmp(zModAction,"approve")==0 ){
+ moderation_approve('w', rid);
+ }
+ }
+ cgi_check_for_malice();
+ style_header("Update of \"%h\"", pWiki->zWikiTitle);
+ zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
+ zDate = db_text(0, "SELECT datetime(%.17g,toLocal())", pWiki->rDate);
+ style_submenu_element("Raw", "%R/artifact/%s", zUuid);
+ style_submenu_element("History", "%R/whistory?name=%t", pWiki->zWikiTitle);
+ style_submenu_element("Page", "%R/wiki?name=%t", pWiki->zWikiTitle);
+ login_anonymous_available();
+ @ <div class="section">Overview</div>
+ @ <p><table class="label-value">
+ @ <tr><th>Artifact&nbsp;ID:</th>
+ @ <td>%z(href("%R/artifact/%!S",zUuid))%s(zUuid)</a>
+ if( g.perm.Setup ){
+ @ (%d(rid))
+ }
+ modPending = moderation_pending_www(rid);
+ @ </td></tr>
+ @ <tr><th>Page&nbsp;Name:</th>\
+ @ <td>%z(href("%R/whistory?name=%h",pWiki->zWikiTitle))\
+ @ %h(pWiki->zWikiTitle)</a></td></tr>
+ @ <tr><th>Date:</th><td>
+ hyperlink_to_date(zDate, "</td></tr>");
+ @ <tr><th>Original&nbsp;User:</th><td>
+ hyperlink_to_user(pWiki->zUser, zDate, "</td></tr>");
+ if( pWiki->zMimetype ){
+ @ <tr><th>Mimetype:</th><td>%h(pWiki->zMimetype)</td></tr>
+ }
+ if( pWiki->nParent>0 ){
+ int i;
+ @ <tr><th>Parent%s(pWiki->nParent==1?"":"s"):</th><td>
+ for(i=0; i<pWiki->nParent; i++){
+ char *zParent = pWiki->azParent[i];
+ @ %z(href("%R/info/%!S",zParent))%s(zParent)</a>
+ @ %z(href("%R/wdiff?id=%!S&pid=%!S",zUuid,zParent))(diff)</a>
+ }
+ @ </td></tr>
+ }
+ tagid = wiki_tagid(pWiki->zWikiTitle);
+ if( tagid>0 && (ridNext = wiki_next(tagid, pWiki->rDate))>0 ){
+ char *zId = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", ridNext);
+ @ <tr><th>Next</th>
+ @ <td>%z(href("%R/info/%!S",zId))%s(zId)</a></td>
+ }
+ @ </table>
+
+ if( g.perm.ModWiki && modPending ){
+ @ <div class="section">Moderation</div>
+ @ <blockquote>
+ @ <form method="POST" action="%R/winfo/%s(zUuid)">
+ @ <label><input type="radio" name="modaction" value="delete">
+ @ Delete this change</label><br>
+ @ <label><input type="radio" name="modaction" value="approve">
+ @ Approve this change</label><br>
+ @ <input type="submit" value="Submit">
+ @ </form>
+ @ </blockquote>
+ }
+
+
+ @ <div class="section">Content</div>
+ blob_init(&wiki, pWiki->zWiki, -1);
+ safe_html_context(DOCSRC_WIKI);
+ wiki_render_by_mimetype(&wiki, pWiki->zMimetype);
+ blob_reset(&wiki);
+ manifest_destroy(pWiki);
+ document_emit_js();
+ style_finish_page();
+}
+
+/*
+** Find an check-in based on query parameter zParam and parse its
+** manifest. Return the number of errors.
+*/
+static Manifest *vdiff_parse_manifest(const char *zParam, int *pRid){
+ int rid;
+
+ *pRid = rid = name_to_rid_www(zParam);
+ if( rid==0 ){
+ const char *z = P(zParam);
+ if( z==0 || z[0]==0 ){
+ webpage_error("Missing \"%s\" query parameter.", zParam);
+ }else{
+ webpage_error("No such artifact: \"%s\"", z);
+ }
+ return 0;
+ }
+ if( !is_a_version(rid) ){
+ webpage_error("Artifact %s is not a check-in.", P(zParam));
+ return 0;
+ }
+ return manifest_get(rid, CFTYPE_MANIFEST, 0);
+}
+
+/*
+** WEBPAGE: vdiff
+** URL: /vdiff?from=TAG&to=TAG
+**
+** Show the difference between two check-ins identified by the from= and
+** to= query parameters.
+**
+** Query parameters:
+**
+** from=TAG Left side of the comparison
+** to=TAG Right side of the comparison
+** branch=TAG Show all changes on a particular branch
+** diff=INTEGER 0: none, 1: unified, 2: side-by-side
+** glob=STRING only diff files matching this glob
+** dc=N show N lines of context around each diff
+** w=BOOLEAN ignore whitespace when computing diffs
+** nohdr omit the description at the top of the page
+** nc omit branch coloration from the header graph
+** inv "Invert". Exchange the roles of from= and to=
+**
+** Show all differences between two check-ins.
+*/
+void vdiff_page(void){
+ int ridFrom, ridTo;
+ int diffType = 0; /* 0: none, 1: unified, 2: side-by-side */
+ Manifest *pFrom, *pTo;
+ ManifestFile *pFileFrom, *pFileTo;
+ const char *zBranch;
+ const char *zFrom;
+ const char *zTo;
+ const char *zRe;
+ const char *zGlob;
+ Glob * pGlob = 0;
+ char *zMergeOrigin = 0;
+ ReCompiled *pRe = 0;
+ DiffConfig DCfg, *pCfg = 0;
+ int graphFlags = 0;
+ Blob qp; /* non-glob= query parameters for generated links */
+ Blob qpGlob; /* glob= query parameter for generated links */
+ int bInvert = PB("inv");
+ int aChng[N_FCHNG]; /* Count of versiou change types */
+
+ memset(aChng, 0, sizeof(aChng));
+ login_check_credentials();
+ if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
+ if( robot_restrict("diff") ) return;
+ login_anonymous_available();
+ fossil_nice_default();
+ blob_init(&qp, 0, 0);
+ blob_init(&qpGlob, 0, 0);
+ diffType = preferred_diff_type();
+ zRe = P("regex");
+ if( zRe ) fossil_re_compile(&pRe, zRe, 0);
+ zBranch = P("branch");
+ if( zBranch && zBranch[0]==0 ) zBranch = 0;
+ if( zBranch ){
+ blob_appendf(&qp, "branch=%T", zBranch);
+ zMergeOrigin = mprintf("merge-in:%s", zBranch);
+ cgi_replace_parameter("from", zMergeOrigin);
+ cgi_replace_parameter("to", zBranch);
+ }else{
+ if( bInvert ){
+ blob_appendf(&qp, "to=%T&from=%T",PD("from",""),PD("to",""));
+ }else{
+ blob_appendf(&qp, "from=%T&to=%T",PD("from",""),PD("to",""));
+ }
+ }
+ pTo = vdiff_parse_manifest("to", &ridTo);
+ if( pTo==0 ) return;
+ pFrom = vdiff_parse_manifest("from", &ridFrom);
+ if( pFrom==0 ) return;
+ zGlob = P("glob");
+ /*
+ ** Maintenace reminder: we explicitly do _not_ use P_NoBot()
+ ** for "from" and "to" because those args can contain legitimate
+ ** strings which may trigger the looks-like SQL checks, e.g.
+ ** from=merge-in:OR-clause-improvement
+ ** to=OR-clause-improvement
+ */
+ zFrom = P("from");
+ zTo = P("to");
+ if( bInvert ){
+ Manifest *pTemp = pTo;
+ const char *zTemp = zTo;
+ pTo = pFrom;
+ pFrom = pTemp;
+ zTo = zFrom;
+ zFrom = zTemp;
+ }
+ if( zGlob ){
+ if( !*zGlob ){
+ zGlob = NULL;
+ }else{
+ blob_appendf(&qpGlob, "&glob=%T", zGlob);
+ pGlob = glob_create(zGlob);
+ }
+ }
+ if( PB("nc") ){
+ graphFlags |= TIMELINE_NOCOLOR;
+ blob_appendf(&qp, "&nc");
+ }
+ pCfg = construct_diff_flags(diffType, &DCfg);
+ if( DCfg.diffFlags & DIFF_IGNORE_ALLWS ){
+ blob_appendf(&qp, "&w");
+ }
+ cgi_check_for_malice();
+ style_set_current_feature("vdiff");
+ if( zBranch==0 ){
+ style_submenu_element("Path", "%R/timeline?me=%T&you=%T", zFrom, zTo);
+ }
+ if( diffType!=2 ){
+ style_submenu_element("Side-by-Side Diff", "%R/vdiff?diff=2&%b%b", &qp,
+ &qpGlob);
+ }
+ if( diffType!=1 ) {
+ style_submenu_element("Unified Diff", "%R/vdiff?diff=1&%b%b", &qp, &qpGlob);
+ }
+ if( zBranch==0 ){
+ style_submenu_element("Invert","%R/vdiff?diff=%d&inv&%b%b", diffType,
+ &qp, &qpGlob);
+ }
+ if( zGlob ){
+ style_submenu_element("Clear glob", "%R/vdiff?diff=%d&%b", diffType, &qp);
+ }else{
+ style_submenu_element("Patch", "%R/vpatch?from=%T&to=%T%s", zFrom, zTo,
+ (DCfg.diffFlags & DIFF_IGNORE_ALLWS)?"&w":"");
+ }
+ if( diffType!=0 ){
+ style_submenu_checkbox("w", "Ignore Whitespace", 0, 0);
+ }
+ if( zBranch ){
+ style_header("Changes On Branch %h", zBranch);
+ }else{
+ style_header("Check-in Differences");
+ }
+ if( P("nohdr")==0 ){
+ if( zBranch ){
+ char *zRealBranch = branch_of_rid(ridTo);
+ char *zToUuid = rid_to_uuid(ridTo);
+ char *zFromUuid = rid_to_uuid(ridFrom);
+ @ <h2>Changes In Branch \
+ @ %z(href("%R/timeline?r=%T",zRealBranch))%h(zRealBranch)</a>
+ if( ridTo != symbolic_name_to_rid(zRealBranch,"ci") ){
+ @ Through %z(href("%R/info/%!S",zToUuid))[%S(zToUuid)]</a>
+ }
+ @ Excluding Merge-Ins</h2>
+ @ <p>This is equivalent to a diff from
+ @ <span class='timelineSelected'>\
+ @ %z(href("%R/info/%!S",zFromUuid))%S(zFromUuid)</a></span>
+ @ to <span class='timelineSelected timelineSecondary'>\
+ @ %z(href("%R/info/%!S",zToUuid))%S(zToUuid)</a></span></p>
+ }else{
+ @ <h2>Difference From <span class='timelineSelected'>\
+ @ %z(href("%R/info/%h",zFrom))%h(zFrom)</a></span>
+ @ To <span class='timelineSelected timelineSecondary'>\
+ @ %z(href("%R/info/%h",zTo))%h(zTo)</a></span></h2>
+ }
+ render_checkin_context(ridFrom, ridTo, 0, graphFlags);
+ if( pRe ){
+ @ <p><b>Only differences that match regular expression "%h(zRe)"
+ @ are shown.</b></p>
+ }
+ if( zGlob ){
+ @ <p><b>Only files matching the glob "%h(zGlob)" are shown.</b></p>
+ }
+ @<hr><p>
+ }
+ blob_reset(&qp);
+ blob_reset(&qpGlob);
+
+ manifest_file_rewind(pFrom);
+ pFileFrom = manifest_file_next(pFrom, 0);
+ manifest_file_rewind(pTo);
+ pFileTo = manifest_file_next(pTo, 0);
+ DCfg.pRe = pRe;
+ while( pFileFrom || pFileTo ){
+ int cmp;
+ if( pFileFrom==0 ){
+ cmp = +1;
+ }else if( pFileTo==0 ){
+ cmp = -1;
+ }else{
+ cmp = fossil_strcmp(pFileFrom->zName, pFileTo->zName);
+ }
+ if( cmp<0 ){
+ if( !pGlob || glob_match(pGlob, pFileFrom->zName) ){
+ append_file_change_line(zFrom, pFileFrom->zName,
+ pFileFrom->zUuid, 0, 0, pCfg, 0,aChng);
+ }
+ pFileFrom = manifest_file_next(pFrom, 0);
+ }else if( cmp>0 ){
+ if( !pGlob || glob_match(pGlob, pFileTo->zName) ){
+ append_file_change_line(zTo, pFileTo->zName,
+ 0, pFileTo->zUuid, 0, pCfg,
+ manifest_file_mperm(pFileTo),aChng);
+ }
+ pFileTo = manifest_file_next(pTo, 0);
+ }else if( fossil_strcmp(pFileFrom->zUuid, pFileTo->zUuid)==0 ){
+ pFileFrom = manifest_file_next(pFrom, 0);
+ pFileTo = manifest_file_next(pTo, 0);
+ }else{
+ if(!pGlob || (glob_match(pGlob, pFileFrom->zName)
+ || glob_match(pGlob, pFileTo->zName)) ){
+ append_file_change_line(zFrom, pFileFrom->zName,
+ pFileFrom->zUuid,
+ pFileTo->zUuid, 0, pCfg,
+ manifest_file_mperm(pFileTo),aChng);
+ }
+ pFileFrom = manifest_file_next(pFrom, 0);
+ pFileTo = manifest_file_next(pTo, 0);
+ }
+ }
+ glob_free(pGlob);
+ manifest_destroy(pFrom);
+ manifest_destroy(pTo);
+ append_diff_javascript(diffType);
+ style_finish_page();
+}
+
+#if INTERFACE
+/*
+** Possible return values from object_description()
+*/
+#define OBJTYPE_CHECKIN 0x0001
+#define OBJTYPE_CONTENT 0x0002
+#define OBJTYPE_WIKI 0x0004
+#define OBJTYPE_TICKET 0x0008
+#define OBJTYPE_ATTACHMENT 0x0010
+#define OBJTYPE_EVENT 0x0020
+#define OBJTYPE_TAG 0x0040
+#define OBJTYPE_SYMLINK 0x0080
+#define OBJTYPE_EXE 0x0100
+#define OBJTYPE_FORUM 0x0200
+
+/*
+** Possible flags for the second parameter to
+** object_description()
+*/
+#define OBJDESC_DETAIL 0x0001 /* Show more detail */
+#define OBJDESC_BASE 0x0002 /* Set <base> using this object */
+#endif
+
+/*
+** Write a description of an object to the www reply.
+*/
+int object_description(
+ int rid, /* The artifact ID for the object to describe */
+ u32 objdescFlags, /* Flags to control display */
+ const char *zFileName, /* For file objects, use this name. Can be NULL */
+ Blob *pDownloadName /* Fill with a good download name. Can be NULL */
+){
+ Stmt q;
+ int cnt = 0;
+ int nWiki = 0;
+ int objType = 0;
+ char *zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
+ int showDetail = (objdescFlags & OBJDESC_DETAIL)!=0;
+ char *prevName = 0;
+ int bNeedBase = (objdescFlags & OBJDESC_BASE)!=0;
+
+ db_prepare(&q,
+ "SELECT filename.name, datetime(event.mtime,toLocal()),"
+ " coalesce(event.ecomment,event.comment),"
+ " coalesce(event.euser,event.user),"
+ " b.uuid, mlink.mperm,"
+ " coalesce((SELECT value FROM tagxref"
+ " WHERE tagid=%d AND tagtype>0 AND rid=mlink.mid),'trunk'),"
+ " a.size"
+ " FROM mlink, filename, event, blob a, blob b"
+ " WHERE filename.fnid=mlink.fnid"
+ " AND event.objid=mlink.mid"
+ " AND a.rid=mlink.fid"
+ " AND b.rid=mlink.mid"
+ " AND mlink.fid=%d"
+ " ORDER BY filename.name, event.mtime /*sort*/",
+ TAG_BRANCH, rid
+ );
+ @ <ul>
+ while( db_step(&q)==SQLITE_ROW ){
+ const char *zName = db_column_text(&q, 0);
+ const char *zDate = db_column_text(&q, 1);
+ const char *zCom = db_column_text(&q, 2);
+ const char *zUser = db_column_text(&q, 3);
+ const char *zVers = db_column_text(&q, 4);
+ int mPerm = db_column_int(&q, 5);
+ const char *zBr = db_column_text(&q, 6);
+ int szFile = db_column_int(&q,7);
+ int sameFilename = prevName!=0 && fossil_strcmp(zName,prevName)==0;
+ if( zFileName && fossil_strcmp(zName,zFileName)!=0 ) continue;
+ if( sameFilename && !showDetail ){
+ if( cnt==1 ){
+ @ %z(href("%R/whatis/%!S",zUuid))[more...]</a>
+ }
+ cnt++;
+ continue;
+ }
+ if( !sameFilename ){
+ if( prevName && showDetail ) {
+ @ </ul>
+ }
+ if( mPerm==PERM_LNK ){
+ @ <li>Symbolic link
+ objType |= OBJTYPE_SYMLINK;
+ }else if( mPerm==PERM_EXE ){
+ @ <li>Executable file
+ objType |= OBJTYPE_EXE;
+ }else{
+ @ <li>File
+ if( bNeedBase ){
+ bNeedBase = 0;
+ style_set_current_page("doc/%S/%s",zVers,zName);
+ }
+ }
+ objType |= OBJTYPE_CONTENT;
+ @ %z(href("%R/finfo?name=%T&ci=%!S&m=%!S",zName,zVers,zUuid))\
+ @ %h(zName)</a>
+ tag_private_status(rid);
+ if( showDetail ){
+ @ <ul>
+ }
+ prevName = fossil_strdup(zName);
+ }
+ if( showDetail ){
+ @ <li>
+ hyperlink_to_date(zDate,"");
+ @ &mdash; part of check-in
+ hyperlink_to_version(zVers);
+ }else{
+ @ &mdash; part of check-in
+ hyperlink_to_version(zVers);
+ @ at
+ hyperlink_to_date(zDate,"");
+ }
+ if( zBr && zBr[0] ){
+ @ on branch %z(href("%R/timeline?r=%T",zBr))%h(zBr)</a>
+ }
+ @ &mdash; %!W(zCom) (user:
+ hyperlink_to_user(zUser,zDate,",");
+ @ size: %d(szFile))
+ if( g.perm.Hyperlink ){
+ @ %z(href("%R/annotate?filename=%T&checkin=%!S",zName,zVers))
+ @ [annotate]</a>
+ @ %z(href("%R/blame?filename=%T&checkin=%!S",zName,zVers))
+ @ [blame]</a>
+ @ %z(href("%R/timeline?uf=%!S",zUuid))[check-ins&nbsp;using]</a>
+ if( fileedit_is_editable(zName) ){
+ @ %z(href("%R/fileedit?filename=%T&checkin=%!S",zName,zVers))[edit]</a>
+ }
+ }
+ cnt++;
+ if( pDownloadName && blob_size(pDownloadName)==0 ){
+ blob_append(pDownloadName, zName, -1);
+ }
+ }
+ if( prevName && showDetail ){
+ @ </ul>
+ }
+ @ </ul>
+ free(prevName);
+ db_finalize(&q);
+ db_prepare(&q,
+ "SELECT substr(tagname, 6, 10000), datetime(event.mtime, toLocal()),"
+ " coalesce(event.euser, event.user)"
+ " FROM tagxref, tag, event"
+ " WHERE tagxref.rid=%d"
+ " AND tag.tagid=tagxref.tagid"
+ " AND tag.tagname LIKE 'wiki-%%'"
+ " AND event.objid=tagxref.rid",
+ rid
+ );
+ while( db_step(&q)==SQLITE_ROW ){
+ const char *zPagename = db_column_text(&q, 0);
+ const char *zDate = db_column_text(&q, 1);
+ const char *zUser = db_column_text(&q, 2);
+ if( cnt>0 ){
+ @ Also wiki page
+ }else{
+ @ Wiki page
+ }
+ objType |= OBJTYPE_WIKI;
+ @ [%z(href("%R/wiki?name=%t",zPagename))%h(zPagename)</a>] by
+ hyperlink_to_user(zUser,zDate," on");
+ hyperlink_to_date(zDate,".");
+ nWiki++;
+ cnt++;
+ if( pDownloadName && blob_size(pDownloadName)==0 ){
+ blob_appendf(pDownloadName, "%s.txt", zPagename);
+ }
+ }
+ db_finalize(&q);
+ if( nWiki==0 ){
+ db_prepare(&q,
+ "SELECT datetime(mtime, toLocal()), user, comment, type, uuid, tagid"
+ " FROM event, blob"
+ " WHERE event.objid=%d"
+ " AND blob.rid=%d",
+ rid, rid
+ );
+ while( db_step(&q)==SQLITE_ROW ){
+ const char *zDate = db_column_text(&q, 0);
+ const char *zUser = db_column_text(&q, 1);
+ const char *zCom = db_column_text(&q, 2);
+ const char *zType = db_column_text(&q, 3);
+ const char *zUuid = db_column_text(&q, 4);
+ int eventTagId = db_column_int(&q, 5);
+ if( cnt>0 ){
+ @ Also
+ }
+ if( zType[0]=='w' ){
+ @ Wiki edit
+ objType |= OBJTYPE_WIKI;
+ }else if( zType[0]=='t' ){
+ @ Ticket change
+ objType |= OBJTYPE_TICKET;
+ }else if( zType[0]=='c' ){
+ @ Manifest of check-in
+ objType |= OBJTYPE_CHECKIN;
+ }else if( zType[0]=='e' ){
+ if( eventTagId != 0) {
+ @ Instance of technote
+ objType |= OBJTYPE_EVENT;
+ hyperlink_to_event_tagid(db_column_int(&q, 5));
+ }else{
+ @ Attachment to technote
+ }
+ }else if( zType[0]=='f' ){
+ objType |= OBJTYPE_FORUM;
+ @ Forum post
+ }else{
+ @ Tag referencing
+ }
+ if( zType[0]!='e' || eventTagId == 0){
+ hyperlink_to_version(zUuid);
+ }
+ @ - %!W(zCom) by
+ hyperlink_to_user(zUser,zDate," on");
+ hyperlink_to_date(zDate, ".");
+ if( pDownloadName && blob_size(pDownloadName)==0 ){
+ blob_appendf(pDownloadName, "%S.txt", zUuid);
+ }
+ tag_private_status(rid);
+ cnt++;
+ }
+ db_finalize(&q);
+ }
+ db_prepare(&q,
+ "SELECT target, filename, datetime(mtime, toLocal()), user, src"
+ " FROM attachment"
+ " WHERE src=(SELECT uuid FROM blob WHERE rid=%d)"
+ " ORDER BY mtime DESC /*sort*/",
+ rid
+ );
+ while( db_step(&q)==SQLITE_ROW ){
+ const char *zTarget = db_column_text(&q, 0);
+ const char *zFilename = db_column_text(&q, 1);
+ const char *zDate = db_column_text(&q, 2);
+ const char *zUser = db_column_text(&q, 3);
+ /* const char *zSrc = db_column_text(&q, 4); */
+ if( cnt>0 ){
+ @ Also attachment "%h(zFilename)" to
+ }else{
+ @ Attachment "%h(zFilename)" to
+ }
+ objType |= OBJTYPE_ATTACHMENT;
+ switch( attachment_target_type(zTarget, 1) ){
+ case CFTYPE_FORUM:
+ if( g.perm.Hyperlink && g.anon.RdForum ){
+ @ forum post [%z(href("%R/forumpost/%!S",zTarget))%S(zTarget)</a>]
+ }else{
+ @ forum post [%S(zTarget)]
+ }
+ break;
+ case CFTYPE_TICKET:
+ if( g.perm.Hyperlink && g.anon.RdTkt ){
+ @ ticket [%z(href("%R/tktview?name=%!S",zTarget))%S(zTarget)</a>]
+ }else{
+ @ ticket [%S(zTarget)]
+ }
+ break;
+ case CFTYPE_EVENT:
+ if( g.perm.Hyperlink && g.anon.RdWiki ){
+ @ tech note [%z(href("%R/technote/%h",zTarget))%S(zTarget)</a>]
+ }else{
+ @ tech note [%S(zTarget)]
+ }
+ break;
+ case CFTYPE_WIKI:
+ if( g.perm.Hyperlink && g.anon.RdWiki ){
+ @ wiki page [%z(href("%R/wiki?name=%t",zTarget))%h(zTarget)</a>]
+ }else{
+ @ wiki page [%h(zTarget)]
+ }
+ break;
+ default:
+ /* historical behavior is to assume wiki, but we can end up
+ ** showing bogus links that way to stale attachments. */
+ @ unknown artifact %h(zTarget)
+ }
+ @ added by
+ hyperlink_to_user(zUser,zDate," on");
+ hyperlink_to_date(zDate,".");
+ cnt++;
+ if( pDownloadName && blob_size(pDownloadName)==0 ){
+ blob_append(pDownloadName, zFilename, -1);
+ }
+ tag_private_status(rid);
+ }
+ db_finalize(&q);
+ if( db_exists("SELECT 1 FROM tagxref WHERE rid=%d AND tagid=%d",
+ rid, TAG_CLUSTER) ){
+ @ Cluster %z(href("%R/info/%S",zUuid))%S(zUuid)</a>.
+ cnt++;
+ }
+ if( cnt==0 ){
+ @ Unrecognized artifact
+ if( pDownloadName && blob_size(pDownloadName)==0 ){
+ blob_appendf(pDownloadName, "%S.txt", zUuid);
+ }
+ tag_private_status(rid);
+ }
+ return objType;
+}
+
+/*
+** SETTING: preferred-diff-type width=16 default=0
+**
+** The preferred-diff-type setting determines the preferred diff format
+** for web pages if the format is not otherwise specified, for example
+** by a query parameter or cookie. Allowed values:
+**
+** 1 Unified diff
+** 2 Side-by-side diff
+**
+** If this setting is omitted or has a value of 0 or less, then it
+** is ignored.
+*/
+/*
+** Return the preferred diff type.
+**
+** 0 = No diff at all.
+** 1 = unified diff
+** 2 = side-by-side diff
+**
+** To determine the preferred diff type, the following values are
+** consulted in the order shown. The first available source wins.
+**
+** * The "diff" query parameter
+** * The "diff" field of the user display cookie
+** * The "preferred-diff-type" setting
+** * 1 for mobile and 2 for desktop, based on the UserAgent
+*/
+int preferred_diff_type(void){
+ int dflt;
+ int res;
+ int isBot;
+ static char zDflt[2]
+ /*static b/c cookie_link_parameter() does not copy it!*/;
+ if( robot_would_be_restricted("diff") ){
+ dflt = 0;
+ isBot = 1;
+ }else{
+ dflt = db_get_int("preferred-diff-type",-99);
+ if( dflt<=0 ) dflt = user_agent_is_likely_mobile() ? 1 : 2;
+ isBot = 0;
+ }
+ zDflt[0] = dflt + '0';
+ zDflt[1] = 0;
+ cookie_link_parameter("diff","diff", zDflt);
+ res = atoi(PD_NoBot("diff",zDflt));
+ if( isBot && res>0 && robot_restrict("diff") ){
+ cgi_reply();
+ fossil_exit(0);
+ }
+ return res;
+}
+
+
+/*
+** WEBPAGE: fdiff
+** URL: fdiff?v1=HASH&v2=HASH
+**
+** Two arguments, v1 and v2, identify the artifacts to be diffed.
+** Show diff side by side unless sbs is 0. Generate plain text if
+** "patch" is present, otherwise generate "pretty" HTML.
+**
+** Alternative URL: fdiff?from=filename1&to=filename2&ci=checkin
+**
+** If the "from" and "to" query parameters are both present, then they are
+** the names of two files within the check-in "ci" that are diffed. If the
+** "ci" parameter is omitted, then the most recent check-in ("tip") is
+** used.
+**
+** Additional parameters:
+**
+** dc=N Show N lines of context around each diff
+** patch Use the patch diff format
+** regex=REGEX Only show differences that match REGEX
+** sbs=BOOLEAN Turn side-by-side diffs on and off (default: on)
+** verbose=BOOLEAN Show more detail when describing artifacts
+** w=BOOLEAN Ignore whitespace
+*/
+void diff_page(void){
+ int v1, v2;
+ int isPatch = P("patch")!=0;
+ int diffType; /* 0: none, 1: unified, 2: side-by-side */
+ char *zV1;
+ char *zV2;
+ const char *zRe;
+ ReCompiled *pRe = 0;
+ u32 objdescFlags = 0;
+ int verbose = PB("verbose");
+ DiffConfig DCfg;
+
+ login_check_credentials();
+ if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
+ if( robot_restrict("diff") ) return;
+ diff_config_init(&DCfg, 0);
+ diffType = preferred_diff_type();
+ if( P("from") && P("to") ){
+ v1 = artifact_from_ci_and_filename("from");
+ v2 = artifact_from_ci_and_filename("to");
+ if( v1==0 || v2==0 ) fossil_redirect_home();
+ }else{
+ Stmt q;
+ v1 = name_to_rid_www("v1");
+ v2 = name_to_rid_www("v2");
+ if( v1==0 || v2==0 ) fossil_redirect_home();
+
+ /* If the two file versions being compared both have the same
+ ** filename, then offer an "Annotate" link that constructs an
+ ** annotation between those version. */
+ db_prepare(&q,
+ "SELECT (SELECT substr(uuid,1,20) FROM blob WHERE rid=a.mid),"
+ " (SELECT substr(uuid,1,20) FROM blob WHERE rid=b.mid),"
+ " (SELECT name FROM filename WHERE filename.fnid=a.fnid)"
+ " FROM mlink a, event ea, mlink b, event eb"
+ " WHERE a.fid=%d"
+ " AND b.fid=%d"
+ " AND a.fnid=b.fnid"
+ " AND a.fid!=a.pid"
+ " AND b.fid!=b.pid"
+ " AND ea.objid=a.mid"
+ " AND eb.objid=b.mid"
+ " ORDER BY ea.mtime ASC, eb.mtime ASC",
+ v1, v2
+ );
+ if( db_step(&q)==SQLITE_ROW ){
+ const char *zCkin = db_column_text(&q, 0);
+ const char *zOrig = db_column_text(&q, 1);
+ const char *zFN = db_column_text(&q, 2);
+ style_submenu_element("Annotate",
+ "%R/annotate?origin=%s&checkin=%s&filename=%T",
+ zOrig, zCkin, zFN);
+ }
+ db_finalize(&q);
+ }
+ zRe = P("regex");
+ cgi_check_for_malice();
+ if( zRe ) fossil_re_compile(&pRe, zRe, 0);
+ if( verbose ) objdescFlags |= OBJDESC_DETAIL;
+ if( isPatch ){
+ Blob c1, c2, *pOut;
+ DiffConfig DCfg;
+ pOut = cgi_output_blob();
+ cgi_set_content_type("text/plain");
+ DCfg.diffFlags = DIFF_VERBOSE;
+ content_get(v1, &c1);
+ content_get(v2, &c2);
+ DCfg.pRe = pRe;
+ text_diff(&c1, &c2, pOut, &DCfg);
+ blob_reset(&c1);
+ blob_reset(&c2);
+ return;
+ }
+
+ zV1 = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", v1);
+ zV2 = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", v2);
+ construct_diff_flags(diffType, &DCfg);
+ DCfg.diffFlags |= DIFF_HTML;
+
+ style_set_current_feature("fdiff");
+ style_header("Diff");
+ style_submenu_checkbox("w", "Ignore Whitespace", 0, 0);
+ if( diffType==2 ){
+ style_submenu_element("Unified Diff", "%R/fdiff?v1=%T&v2=%T&diff=1",
+ P("v1"), P("v2"));
+ }else{
+ style_submenu_element("Side-by-side Diff", "%R/fdiff?v1=%T&v2=%T&diff=2",
+ P("v1"), P("v2"));
+ }
+ style_submenu_checkbox("verbose", "Verbose", 0, 0);
+ style_submenu_element("Patch", "%R/fdiff?v1=%T&v2=%T&patch",
+ P("v1"), P("v2"));
+
+ if( P("smhdr")!=0 ){
+ @ <h2>Differences From Artifact
+ @ %z(href("%R/artifact/%!S",zV1))[%S(zV1)]</a> To
+ @ %z(href("%R/artifact/%!S",zV2))[%S(zV2)]</a>.</h2>
+ }else{
+ @ <h2>Differences From
+ @ Artifact %z(href("%R/artifact/%!S",zV1))[%S(zV1)]</a>:</h2>
+ object_description(v1, objdescFlags,0, 0);
+ @ <h2>To Artifact %z(href("%R/artifact/%!S",zV2))[%S(zV2)]</a>:</h2>
+ object_description(v2, objdescFlags,0, 0);
+ }
+ if( pRe ){
+ @ <b>Only differences that match regular expression "%h(zRe)"
+ @ are shown.</b>
+ DCfg.pRe = pRe;
+ }
+ @ <hr>
+ append_diff(zV1, zV2, &DCfg);
+ append_diff_javascript(diffType);
+ style_finish_page();
+}
+
+/*
+** WEBPAGE: raw
+** URL: /raw/ARTIFACTID
+** URL: /raw?ci=BRANCH&filename=NAME
+**
+** Additional query parameters:
+**
+** m=MIMETYPE The mimetype is MIMETYPE
+** at=FILENAME Content-disposition; attachment; filename=FILENAME;
+**
+** Return the uninterpreted content of an artifact. Used primarily
+** to view artifacts that are images.
+*/
+void rawartifact_page(void){
+ int rid = 0;
+ char *zUuid;
+
+ (void)P("at")/*for cgi_check_for_malice()*/;
+ (void)P("m");
+ if( P("ci") ){
+ rid = artifact_from_ci_and_filename(0);
+ }
+ if( rid==0 ){
+ rid = name_to_rid_www("name");
+ }
+ login_check_credentials();
+ if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
+ cgi_check_for_malice();
+ if( rid==0 ) fossil_redirect_home();
+ zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
+ etag_check(ETAG_HASH, zUuid);
+ if( fossil_strcmp(P("name"), zUuid)==0 && login_is_nobody() ){
+ g.isConst = 1;
+ }
+ free(zUuid);
+ deliver_artifact(rid, P("m"));
+}
+
+
+/*
+** WEBPAGE: secureraw
+** URL: /secureraw/HASH?m=TYPE
+**
+** Return the uninterpreted content of an artifact. This is similar
+** to /raw except in this case the only way to specify the artifact
+** is by the full-length SHA1 or SHA3 hash. Abbreviations are not
+** accepted.
+*/
+void secure_rawartifact_page(void){
+ int rid = 0;
+ const char *zName = PD("name", "");
+
+ (void)P("at")/*for cgi_check_for_malice()*/;
+ (void)P("m");
+ cgi_check_for_malice();
+ login_check_credentials();
+ if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
+ rid = db_int(0, "SELECT rid FROM blob WHERE uuid=%Q", zName);
+ if( rid==0 ){
+ cgi_set_status(404, "Not Found");
+ @ Unknown artifact: "%h(zName)"
+ return;
+ }
+ g.isConst = 1;
+ deliver_artifact(rid, P("m"));
+}
+
+
+/*
+** WEBPAGE: jchunk hidden
+** URL: /jchunk/HASH?from=N&to=M
+**
+** Return lines of text from a file as a JSON array - one entry in the
+** array for each line of text.
+**
+** The HASH is normally a sha1 or sha3 hash that identifies an artifact
+** in the BLOB table of the database. However, if HASH starts with an "x"
+** and is followed by valid hexadecimal, and if we are running in a
+** "fossil ui" situation (locally and with privilege), then decode the hex
+** into a filename and read the file content from that name.
+**
+** **Warning:** This is an internal-use-only interface that is subject to
+** change at any moment. External application should not use this interface
+** since the application will break when this interface changes, and this
+** interface will undoubtedly change.
+**
+** This page is intended to be used in an XHR from javascript on a
+** diff page, to return unseen context to fill in additional context
+** when the user clicks on the appropriate button. The response is
+** always in JSON form and errors are reported as documented for
+** ajax_route_error().
+*/
+void jchunk_page(void){
+ int rid = 0;
+ const char *zName = PD("name", "");
+ int nName = (int)(strlen(zName)&0x7fffffff);
+ int iFrom = atoi(PD("from","0"));
+ int iTo = atoi(PD("to","0"));
+ int ln;
+ int go = 1;
+ const char *zSep;
+ Blob content;
+ Blob line;
+ Blob *pOut;
+
+ if(0){
+ ajax_route_error(400, "Just testing client-side error handling.");
+ return;
+ }
+
+ login_check_credentials();
+ cgi_check_for_malice();
+ if( !g.perm.Read ){
+ ajax_route_error(403, "Access requires Read permissions.");
+ return;
+ }
+ if( iFrom<1 || iTo<iFrom ){
+ ajax_route_error(500, "Invalid line range from=%d, to=%d.",
+ iFrom, iTo);
+ return;
+ }
+ if( zName[0]=='x'
+ && ((nName-1)&1)==0
+ && validate16(&zName[1],nName-1)
+ && g.perm.Admin
+ && cgi_is_loopback(g.zIpAddr)
+ && db_open_local(0)
+ ){
+ /* Treat the HASH as a hex-encoded filename */
+ int n = (nName-1)/2;
+ char *zFN = fossil_malloc(n+1);
+ decode16((const u8*)&zName[1], (u8*)zFN, nName-1);
+ zFN[n] = 0;
+ if( file_size(zFN, ExtFILE)<0 ){
+ blob_zero(&content);
+ }else{
+ blob_read_from_file(&content, zFN, ExtFILE);
+ }
+ fossil_free(zFN);
+ }else{
+ /* Treat the HASH as an artifact hash matching BLOB.UUID */
+#if 1
+ /* Re-enable this block once this code is integrated somewhere into
+ the UI. */
+ rid = db_int(0, "SELECT rid FROM blob WHERE uuid=%Q", zName);
+ if( rid==0 ){
+ ajax_route_error(404, "Unknown artifact: %h", zName);
+ return;
+ }
+#else
+ /* This impl is only to simplify "manual" testing via the JS
+ console. */
+ rid = symbolic_name_to_rid(zName, "*");
+ if( rid==0 ){
+ ajax_route_error(404, "Unknown artifact: %h", zName);
+ return;
+ }else if( rid<0 ){
+ ajax_route_error(418, "Ambiguous artifact name: %h", zName);
+ return;
+ }
+#endif
+ content_get(rid, &content);
+ }
+ g.isConst = 1;
+ cgi_set_content_type("application/json");
+ ln = 0;
+ while( go && ln<iFrom ){
+ go = blob_line(&content, &line);
+ ln++;
+ }
+ pOut = cgi_output_blob();
+ blob_append(pOut, "[\n", 2);
+ zSep = 0;
+ while( go && ln<=iTo ){
+ if( zSep ) blob_append(pOut, zSep, 2);
+ blob_trim(&line);
+ blob_append_json_literal(pOut, blob_buffer(&line), blob_size(&line));
+ zSep = ",\n";
+ go = blob_line(&content, &line);
+ ln++;
+ }
+ blob_appendf(pOut,"]\n");
+ blob_reset(&content);
+}
+
+/*
+** Generate a verbatim artifact as the result of an HTTP request.
+** If zMime is not NULL, use it as the mimetype. If zMime is
+** NULL, guess at the mimetype based on the filename
+** associated with the artifact.
+*/
+void deliver_artifact(int rid, const char *zMime){
+ Blob content;
+ const char *zAttachName = P("at");
+ if( zMime==0 ){
+ char *zFN = (char*)zAttachName;
+ if( zFN==0 ){
+ zFN = db_text(0, "SELECT filename.name FROM mlink, filename"
+ " WHERE mlink.fid=%d"
+ " AND filename.fnid=mlink.fnid", rid);
+ }
+ if( zFN==0 ){
+ /* Look also at the attachment table */
+ zFN = db_text(0, "SELECT attachment.filename FROM attachment, blob"
+ " WHERE blob.rid=%d"
+ " AND attachment.src=blob.uuid", rid);
+ }
+ if( zFN ){
+ zMime = mimetype_from_name(zFN);
+ }
+ if( zMime==0 ){
+ zMime = "application/x-fossil-artifact";
+ }
+ }
+ content_get(rid, &content);
+ fossil_free(style_csp(1));
+ cgi_set_content_type(zMime);
+ if( zAttachName ){
+ cgi_content_disposition_filename(zAttachName);
+ }
+ cgi_set_content(&content);
+}
+
+/*
+** Render a hex dump of a file.
+*/
+static void hexdump(Blob *pBlob){
+ const unsigned char *x;
+ int n, i, j, k;
+ char zLine[100];
+ static const char zHex[] = "0123456789abcdef";
+
+ x = (const unsigned char*)blob_buffer(pBlob);
+ n = blob_size(pBlob);
+ for(i=0; i<n; i+=16){
+ j = 0;
+ zLine[0] = zHex[(i>>24)&0xf];
+ zLine[1] = zHex[(i>>16)&0xf];
+ zLine[2] = zHex[(i>>8)&0xf];
+ zLine[3] = zHex[i&0xf];
+ zLine[4] = ':';
+ sqlite3_snprintf(sizeof(zLine), zLine, "%04x: ", i);
+ for(j=0; j<16; j++){
+ k = 5+j*3;
+ zLine[k] = ' ';
+ if( i+j<n ){
+ unsigned char c = x[i+j];
+ zLine[k+1] = zHex[c>>4];
+ zLine[k+2] = zHex[c&0xf];
+ }else{
+ zLine[k+1] = ' ';
+ zLine[k+2] = ' ';
+ }
+ }
+ zLine[53] = ' ';
+ zLine[54] = ' ';
+ cgi_append_content(zLine, 55);
+ for(j=k=0; j<16; j++){
+ if( i+j<n ){
+ unsigned char c = x[i+j];
+ if( c>'>' && c<=0x7e ){
+ zLine[k++] = c;
+ }else if( c=='>' ){
+ zLine[k++] = '&';
+ zLine[k++] = 'g';
+ zLine[k++] = 't';
+ zLine[k++] = ';';
+ }else if( c=='<' ){
+ zLine[k++] = '&';
+ zLine[k++] = 'l';
+ zLine[k++] = 't';
+ zLine[k++] = ';';
+ }else if( c=='&' ){
+ zLine[k++] = '&';
+ zLine[k++] = 'a';
+ zLine[k++] = 'm';
+ zLine[k++] = 'p';
+ zLine[k++] = ';';
+ }else if( c>=' ' ){
+ zLine[k++] = c;
+ }else{
+ zLine[k++] = '.';
+ }
+ }else{
+ break;
+ }
+ }
+ zLine[k++] = '\n';
+ cgi_append_content(zLine, k);
+ }
+}
+
+/*
+** WEBPAGE: hexdump
+** URL: /hexdump?name=ARTIFACTID
+**
+** Show the complete content of a file identified by ARTIFACTID
+** as preformatted text.
+**
+** Other parameters:
+**
+** verbose Show more detail when describing the object
+*/
+void hexdump_page(void){
+ int rid;
+ Blob content;
+ Blob downloadName;
+ char *zUuid;
+ u32 objdescFlags = 0;
+
+ rid = name_to_rid_www("name");
+ login_check_credentials();
+ if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
+ if( rid==0 ) fossil_redirect_home();
+ cgi_check_for_malice();
+ if( g.perm.Admin ){
+ const char *zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid);
+ if( db_exists("SELECT 1 FROM shun WHERE uuid=%Q", zUuid) ){
+ style_submenu_element("Unshun", "%R/shun?accept=%s&sub=1#delshun", zUuid);
+ }else{
+ style_submenu_element("Shun", "%R/shun?shun=%s#addshun", zUuid);
+ }
+ }
+ style_header("Hex Artifact Content");
+ zUuid = db_text("?","SELECT uuid FROM blob WHERE rid=%d", rid);
+ etag_check(ETAG_HASH, zUuid);
+ @ <h2>Artifact
+ style_copy_button(1, "hash-ar", 0, 2, 0, "%s", zUuid);
+ if( g.perm.Setup ){
+ @ (%d(rid)):</h2>
+ }else{
+ @ :</h2>
+ }
+ blob_zero(&downloadName);
+ if( P("verbose")!=0 ) objdescFlags |= OBJDESC_DETAIL;
+ object_description(rid, objdescFlags, 0, &downloadName);
+ style_submenu_element("Download", "%R/raw/%s?at=%T",
+ zUuid, file_tail(blob_str(&downloadName)));
+ @ <hr>
+ content_get(rid, &content);
+ if( blob_size(&content)>100000 ){
+ /* Prevent robots from running hexdump on megabyte-sized source files
+ ** and there by eating up lots of CPU time and bandwidth. There is
+ ** no good reason for a robot to need a hexdump. */
+ @ <p>A hex dump of this file is not available because it is too large.
+ @ Please download the raw binary file and generate a hex dump yourself.</p>
+ }else{
+ @ <blockquote><pre>
+ hexdump(&content);
+ @ </pre></blockquote>
+ }
+ style_finish_page();
+}
+
+/*
+** Look for "ci" and "filename" query parameters. If found, try to
+** use them to extract the record ID of an artifact for the file.
+**
+** Also look for "fn" and "name" as an aliases for "filename". If any
+** "filename" or "fn" or "name" are present but "ci" is missing, then
+** use "tip" as the default value for "ci".
+**
+** If zNameParam is not NULL, then use that parameter as the filename
+** rather than "fn" or "filename" or "name". the zNameParam is used
+** for the from= and to= query parameters of /fdiff.
+*/
+int artifact_from_ci_and_filename(const char *zNameParam){
+ const char *zFilename;
+ const char *zCI;
+ int cirid;
+ Manifest *pManifest;
+ ManifestFile *pFile;
+ int rid = 0;
+
+ if( zNameParam ){
+ zFilename = P(zNameParam);
+ }else{
+ zFilename = P("filename");
+ if( zFilename==0 ){
+ zFilename = P("fn");
+ }
+ if( zFilename==0 ){
+ zFilename = P("name");
+ }
+ }
+ if( zFilename==0 ) return 0;
+
+ zCI = PD("ci", "tip");
+ cirid = name_to_typed_rid(zCI, "ci");
+ if( cirid<=0 ) return 0;
+ pManifest = manifest_get(cirid, CFTYPE_MANIFEST, 0);
+ if( pManifest==0 ) return 0;
+ manifest_file_rewind(pManifest);
+ while( (pFile = manifest_file_next(pManifest,0))!=0 ){
+ if( fossil_strcmp(zFilename, pFile->zName)==0 ){
+ rid = db_int(0, "SELECT rid FROM blob WHERE uuid=%Q", pFile->zUuid);
+ break;
+ }
+ }
+ manifest_destroy(pManifest);
+ return rid;
+}
+
+/*
+** The "z" argument is a string that contains the text of a source
+** code file and nZ is its length in bytes. This routine appends that
+** text to the HTTP reply with line numbering.
+**
+** zName is the content's file name, if any (it may be NULL). If that
+** name contains a '.' then the part after the final '.' is used as
+** the X part of a "language-X" CSS class on the generated CODE block.
+**
+** zLn is the ?ln= parameter for the HTTP query. If there is an argument,
+** then highlight that line number and scroll to it once the page loads.
+** If there are two line numbers, highlight the range of lines.
+** Multiple ranges can be highlighed by adding additional line numbers
+** separated by a non-digit character (also not one of [-,.]).
+**
+** If includeJS is true then the JS code associated with line
+** numbering is also emitted, else it is not. If this routine is
+** called multiple times in a single app run, the JS is emitted only
+** once. Note that when using this routine to emit Ajax responses, the
+** JS should be not be included, as it will not get imported properly
+** into the response's rendering.
+*/
+void output_text_with_line_numbers(
+ const char *z,
+ int nZ,
+ const char *zName,
+ const char *zLn,
+ int includeJS
+){
+ int iStart, iEnd; /* Start and end of region to highlight */
+ int n = 0; /* Current line number */
+ int i = 0; /* Loop index */
+ int iTop = 0; /* Scroll so that this line is on top of screen. */
+ int nLine = 0; /* content line count */
+ int nSpans = 0; /* number of distinct zLn spans */
+ const char *zExt = file_extension(zName);
+ static int emittedJS = 0; /* emitted shared JS yet? */
+ Stmt q;
+
+ iStart = iEnd = atoi(zLn);
+ db_multi_exec(
+ "CREATE TEMP TABLE lnos(iStart INTEGER PRIMARY KEY, iEnd INTEGER)");
+ if( iStart>0 ){
+ do{
+ while( fossil_isdigit(zLn[i]) ) i++;
+ if( zLn[i]==',' || zLn[i]=='-' || zLn[i]=='.' ){
+ i++;
+ while( zLn[i]=='.' ){ i++; }
+ iEnd = atoi(&zLn[i]);
+ while( fossil_isdigit(zLn[i]) ) i++;
+ }
+ while( fossil_isdigit(zLn[i]) ) i++;
+ if( iEnd<iStart ) iEnd = iStart;
+ db_multi_exec(
+ "INSERT OR REPLACE INTO lnos VALUES(%d,%d)", iStart, iEnd
+ );
+ ++nSpans;
+ iStart = iEnd = atoi(&zLn[i++]);
+ }while( zLn[i] && iStart && iEnd );
+ }
+ /*cgi_printf("<!-- ln span count=%d -->", nSpans);*/
+ cgi_append_content("<table class='numbered-lines'><tbody>"
+ "<tr><td class='line-numbers'><pre>", -1);
+ iStart = iEnd = 0;
+ count_lines(z, nZ, &nLine);
+ for( n=1 ; n<=nLine; ++n ){
+ const char * zAttr = "";
+ const char * zId = "";
+ if(nSpans>0 && iEnd==0){/*Grab the next range of zLn marking*/
+ db_prepare(&q, "SELECT iStart, iEnd FROM lnos "
+ "WHERE iStart >= %d ORDER BY iStart", n);
+ if( db_step(&q)==SQLITE_ROW ){
+ iStart = db_column_int(&q, 0);
+ iEnd = db_column_int(&q, 1);
+ if(!iTop){
+ iTop = iStart - 15 + (iEnd-iStart)/4;
+ if( iTop>iStart - 2 ) iTop = iStart-2;
+ }
+ }else{
+ /* Note that overlapping multi-spans, e.g. 10-15+12-20,
+ can cause us to miss a row. */
+ iStart = iEnd = 0;
+ }
+ db_finalize(&q);
+ --nSpans;
+ /*cgi_printf("<!-- iStart=%d, iEnd=%d -->", iStart, iEnd);*/
+ }
+ if(n==iTop) {
+ zId = " id='scrollToMe'";
+ }
+ if(n==iStart){/*Figure out which CSS class(es) this line needs...*/
+ if(n==iEnd){
+ zAttr = " class='selected-line start end'";
+ iEnd = 0;
+ }else{
+ zAttr = " class='selected-line start'";
+ }
+ iStart = 0;
+ }else if(n==iEnd){
+ zAttr = " class='selected-line end'";
+ iEnd = 0;
+ }else if( n>iStart && n<iEnd ){
+ zAttr = " class='selected-line'";
+ }
+ cgi_printf("<span%s%s>%6d</span>\n", zId, zAttr, n)
+ /* ^^^ explicit \n is necessary for text-mode browsers. */;
+ }
+ cgi_append_content("</pre></td><td class='file-content'><pre>",-1);
+ if(zExt && *zExt){
+ cgi_printf("<code class='language-%h'>",zExt);
+ }else{
+ cgi_append_content("<code>", -1);
+ }
+ cgi_printf("%z", htmlize(z, nZ));
+ CX("</code></pre></td></tr></tbody></table>\n");
+ if(includeJS && !emittedJS){
+ emittedJS = 1;
+ if( db_int(0, "SELECT EXISTS(SELECT 1 FROM lnos)") ){
+ builtin_request_js("scroll.js");
+ }
+ builtin_fossil_js_bundle_or("numbered-lines", NULL);
+ }
+}
+
+/*
+** COMMAND: test-line-numbers
+**
+** Usage: %fossil test-line-numbers FILE ?LN-SPEC?
+**
+*/
+void cmd_test_line_numbers(void){
+ Blob content = empty_blob;
+ const char * zLn = "";
+ const char * zFilename = 0;
+
+ if(g.argc < 3){
+ usage("FILE");
+ }else if(g.argc>3){
+ zLn = g.argv[3];
+ }
+ db_find_and_open_repository(0,0);
+ zFilename = g.argv[2];
+ fossil_print("%s %s\n", zFilename, zLn);
+
+ blob_read_from_file(&content, zFilename, ExtFILE);
+ output_text_with_line_numbers(blob_str(&content), blob_size(&content),
+ zFilename, zLn, 0);
+ blob_reset(&content);
+ fossil_print("%b\n", cgi_output_blob());
+}
+
+/*
+** WEBPAGE: artifact
+** WEBPAGE: file
+** WEBPAGE: whatis
+** WEBPAGE: docfile
+**
+** Typical usage:
+**
+** /artifact/HASH
+** /whatis/HASH
+** /file/NAME
+** /docfile/NAME
+**
+** Additional query parameters:
+**
+** ln - show line numbers
+** ln=N - highlight line number N
+** ln=M-N - highlight lines M through N inclusive
+** ln=M-N+Y-Z - highlight lines M through N and Y through Z (inclusive)
+** verbose - show more detail in the description
+** brief - show just the document, not the metadata. The
+** /docfile page is an alias for /file?brief
+** download - redirect to the download (artifact page only)
+** name=NAME - filename or hash as a query parameter
+** filename=NAME - alternative spelling for "name="
+** fn=NAME - alternative spelling for "name="
+** ci=VERSION - The specific check-in to use with "name=" to
+** identify the file.
+** txt - Force display of unformatted source text
+** hash - Output only the hash of the artifact
+**
+** The /artifact page show the complete content of a file
+** identified by HASH. The /whatis page shows only a description
+** of how the artifact is used. The /file page shows the most recent
+** version of the file or directory called NAME, or a list of the
+** top-level directory if NAME is omitted.
+**
+** For /artifact and /whatis, the name= query parameter can refer to
+** either the name of a file, or an artifact hash. If the ci= query
+** parameter is also present, then name= must refer to a file name.
+** If ci= is omitted, then the hash interpretation is preferred but
+** if name= cannot be understood as a hash, a default "tip" value is
+** used for ci=.
+**
+** For /file, name= can only be interpreted as a filename. As before,
+** a default value of "tip" is used for ci= if ci= is omitted.
+*/
+void artifact_page(void){
+ int rid = 0;
+ Blob content;
+ const char *zMime;
+ Blob downloadName;
+ Blob uuid;
+ int renderAsWiki = 0;
+ int renderAsOrg = 0;
+ int renderAsHtml = 0;
+ int renderAsSvg = 0;
+ int objType;
+ int asText;
+ const char *zUuid = 0;
+ u32 objdescFlags = OBJDESC_BASE;
+ int descOnly = fossil_strcmp(g.zPath,"whatis")==0;
+ int hashOnly = P("hash")!=0;
+ int docOnly = P("brief")!=0;
+ int isFile = fossil_strcmp(g.zPath,"file")==0;
+ const char *zLn = P("ln");
+ const char *zName = P("name");
+ const char *zCI = P("ci");
+ HQuery url;
+ char *zCIUuid = 0;
+ int isSymbolicCI = 0; /* ci= exists and is a symbolic name, not a hash */
+ int isBranchCI = 0; /* ci= refers to a branch name */
+ char *zHeader = 0;
+
+ login_check_credentials();
+ if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
+ cgi_check_for_malice();
+ style_set_current_feature("artifact");
+ if( fossil_strcmp(g.zPath, "docfile")==0 ){
+ isFile = 1;
+ docOnly = 1;
+ }
+
+ /* Capture and normalize the name= and ci= query parameters */
+ if( zName==0 ){
+ zName = P("filename");
+ if( zName==0 ){
+ zName = P("fn");
+ }
+ }
+ if( zCI && strlen(zCI)==0 ){ zCI = 0; }
+ if( zCI
+ && name_to_uuid2(zCI, "ci", &zCIUuid)
+ && sqlite3_strnicmp(zCIUuid, zCI, strlen(zCI))!=0
+ ){
+ isSymbolicCI = 1;
+ isBranchCI = branch_includes_uuid(zCI, zCIUuid);
+ }
+
+ /* The name= query parameter (or at least one of its alternative
+ ** spellings) is required. Except for /file, show a top-level
+ ** directory listing if name= is omitted.
+ */
+ if( zName==0 ){
+ if( isFile ){
+ if( P("ci")==0 ) cgi_set_query_parameter("ci","tip");
+ page_tree();
+ return;
+ }
+ style_header("Missing name= query parameter");
+ @ The name= query parameter is missing
+ style_finish_page();
+ return;
+ }
+
+ url_initialize(&url, g.zPath);
+ url_add_parameter(&url, "name", zName);
+ url_add_parameter(&url, "ci", zCI); /* no-op if zCI is NULL */
+
+ if( zCI==0 && !isFile ){
+ /* If there is no ci= query parameter, then prefer to interpret
+ ** name= as a hash for /artifact and /whatis. But not for /file.
+ ** For /file, a name= without a ci= will prefer to use the default
+ ** "tip" value for ci=. */
+ rid = name_to_rid(zName);
+ }
+ if( rid==0 ){
+ rid = artifact_from_ci_and_filename(0);
+ }
+
+ if( rid==0 ){ /* Artifact not found */
+ if( isFile ){
+ Stmt q;
+ /* For /file, also check to see if name= refers to a directory,
+ ** and if so, do a listing for that directory */
+ int nName = (int)strlen(zName);
+ if( nName && zName[nName-1]=='/' ) nName--;
+ if( db_exists(
+ "SELECT 1 FROM filename"
+ " WHERE name GLOB '%.*q/*' AND substr(name,1,%d)=='%.*q/';",
+ nName, zName, nName+1, nName, zName
+ ) ){
+ if( P("ci")==0 ) cgi_set_query_parameter("ci","tip");
+ page_tree();
+ return;
+ }
+ /* No directory found, look for an historic version of the file
+ ** that was subsequently deleted. */
+ db_prepare(&q,
+ "SELECT fid, uuid FROM mlink, filename, event, blob"
+ " WHERE filename.name=%Q"
+ " AND mlink.fnid=filename.fnid AND mlink.fid>0"
+ " AND event.objid=mlink.mid"
+ " AND blob.rid=mlink.mid"
+ " ORDER BY event.mtime DESC",
+ zName
+ );
+ if( db_step(&q)==SQLITE_ROW ){
+ rid = db_column_int(&q, 0);
+ zCI = fossil_strdup(db_column_text(&q, 1));
+ zCIUuid = fossil_strdup(zCI);
+ url_add_parameter(&url, "ci", zCI);
+ }
+ db_finalize(&q);
+ if( rid==0 ){
+ style_header("No such file");
+ @ File '%h(zName)' does not exist in this repository.
+ }
+ }else{
+ style_header("No such artifact");
+ @ Artifact '%h(zName)' does not exist in this repository.
+ }
+ if( rid==0 ){
+ style_finish_page();
+ return;
+ }
+ }
+
+ if( descOnly || P("verbose")!=0 ){
+ url_add_parameter(&url, "verbose", "1");
+ objdescFlags |= OBJDESC_DETAIL;
+ }
+ zUuid = db_text("?", "SELECT uuid FROM blob WHERE rid=%d", rid);
+ etag_check(ETAG_HASH, zUuid);
+
+ if( descOnly && hashOnly ){
+ blob_set(&uuid, zUuid);
+ cgi_set_content_type("text/plain");
+ cgi_set_content(&uuid);
+ return;
+ }
+
+ asText = P("txt")!=0;
+ if( isFile ){
+ if( docOnly ){
+ /* No header */
+ }else if( zCI==0 || fossil_strcmp(zCI,"tip")==0 ){
+ zCI = "tip";
+ @ <h2>File %z(href("%R/finfo?name=%T&m&ci=tip",zName))%h(zName)</a>
+ @ from the %z(href("%R/info/tip"))latest check-in</a></h2>
+ }else{
+ const char *zPath;
+ Blob path;
+ blob_zero(&path);
+ hyperlinked_path(zName, &path, zCI, "dir", "", LINKPATH_FINFO);
+ zPath = blob_str(&path);
+ @ <h2>File %s(zPath) artifact \
+ style_copy_button(1,"hash-fid",0,0,0,"%z%S</a> ",
+ href("%R/info/%s",zUuid),zUuid);
+ if( isBranchCI ){
+ @ on branch %z(href("%R/timeline?r=%T",zCI))%h(zCI)</a></h2>
+ }else if( isSymbolicCI ){
+ @ part of check-in %z(href("%R/info/%!S",zCIUuid))%s(zCI)</a></h2>
+ }else{
+ @ part of check-in %z(href("%R/info/%!S",zCIUuid))%S(zCIUuid)</a></h2>
+ }
+ blob_reset(&path);
+ }
+ zMime = mimetype_from_name(zName);
+ if( !docOnly ){
+ style_submenu_element("Artifact", "%R/artifact/%S", zUuid);
+ style_submenu_element("Annotate", "%R/annotate?filename=%T&checkin=%T",
+ zName, zCI);
+ style_submenu_element("Blame", "%R/blame?filename=%T&checkin=%T",
+ zName, zCI);
+ style_submenu_element("Doc", "%R/doc/%T/%T", zCI, zName);
+ }
+ blob_init(&downloadName, zName, -1);
+ objType = OBJTYPE_CONTENT;
+ }else{
+ @ <h2>Artifact
+ style_copy_button(1, "hash-ar", 0, 2, 0, "%s", zUuid);
+ if( g.perm.Setup ){
+ @ (%d(rid)):</h2>
+ }else{
+ @ :</h2>
+ }
+ blob_zero(&downloadName);
+ if( asText ) objdescFlags &= ~OBJDESC_BASE;
+ objType = object_description(rid, objdescFlags,
+ (isFile?zName:0), &downloadName);
+ zMime = mimetype_from_name(blob_str(&downloadName));
+ }
+ if( !descOnly && P("download")!=0 ){
+ cgi_redirectf("%R/raw/%s?at=%T",
+ db_text("x", "SELECT uuid FROM blob WHERE rid=%d", rid),
+ file_tail(blob_str(&downloadName)));
+ /*NOTREACHED*/
+ }
+ if( g.perm.Admin && !docOnly ){
+ const char *zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid);
+ if( db_exists("SELECT 1 FROM shun WHERE uuid=%Q", zUuid) ){
+ style_submenu_element("Unshun", "%R/shun?accept=%s&sub=1#accshun", zUuid);
+ }else{
+ style_submenu_element("Shun", "%R/shun?shun=%s#addshun",zUuid);
+ }
+ }
+
+ if( isFile ){
+ if( isSymbolicCI ){
+ zHeader = mprintf("%s at %s", file_tail(zName), zCI);
+ style_set_current_page("doc/%t/%T", zCI, zName);
+ }else if( zCIUuid && zCIUuid[0] ){
+ zHeader = mprintf("%s at [%S]", file_tail(zName), zCIUuid);
+ style_set_current_page("doc/%S/%T", zCIUuid, zName);
+ }else{
+ zHeader = fossil_strdup(file_tail(zName));
+ style_set_current_page("doc/tip/%T", zName);
+ }
+ }else if( descOnly ){
+ zHeader = mprintf("Artifact Description [%S]", zUuid);
+ }else{
+ zHeader = mprintf("Artifact [%S]", zUuid);
+ }
+ style_header("%s", zHeader);
+ fossil_free(zCIUuid);
+ fossil_free(zHeader);
+ if( !isFile && g.perm.Admin ){
+ Stmt q;
+ db_prepare(&q,
+ "SELECT coalesce(user.login,rcvfrom.uid),"
+ " datetime(rcvfrom.mtime,toLocal()),"
+ " coalesce(rcvfrom.ipaddr,'unknown'),"
+ " rcvfrom.rcvid"
+ " FROM blob, rcvfrom LEFT JOIN user ON user.uid=rcvfrom.uid"
+ " WHERE blob.rid=%d"
+ " AND rcvfrom.rcvid=blob.rcvid;", rid);
+ while( db_step(&q)==SQLITE_ROW ){
+ const char *zUser = db_column_text(&q,0);
+ const char *zDate = db_column_text(&q,1);
+ const char *zIp = db_column_text(&q,2);
+ int rcvid = db_column_int(&q,3);
+ @ <p>Received on %s(zDate) from %h(zUser) at %h(zIp).
+ @ (<a href="%R/rcvfrom?rcvid=%d(rcvid)">rcvid&nbsp;%d(rcvid)</a>)</p>
+ }
+ db_finalize(&q);
+ }
+ if( !docOnly ){
+ style_submenu_element("Download", "%R/raw/%s?at=%T",
+ zUuid, file_tail(blob_str(&downloadName)));
+ if( db_exists("SELECT 1 FROM mlink WHERE fid=%d", rid) ){
+ style_submenu_element("Check-ins Using", "%R/timeline?uf=%s", zUuid);
+ }
+ }
+ if( zMime ){
+ if( fossil_strcmp(zMime, "text/html")==0 ){
+ if( asText ){
+ style_submenu_element("Html", "%s", url_render(&url, "txt", 0, 0, 0));
+ }else{
+ renderAsHtml = 1;
+ if( !docOnly ){
+ style_submenu_element("Text", "%s", url_render(&url, "txt","1",0,0));
+ }
+ }
+ }else if( fossil_strcmp(zMime, "text/x-fossil-wiki")==0
+ || fossil_strcmp(zMime, "text/x-markdown")==0
+ || fossil_strcmp(zMime, "text/x-pikchr")==0
+ || fossil_strcmp(zMime, "text/x-org")==0 ){
+ if( asText ){
+ style_submenu_element(zMime[7]=='p' ? "Pikchr" : "Wiki",
+ "%s", url_render(&url, "txt", 0, 0, 0));
+ }else{
+ if( fossil_strcmp(zMime,"text/x-org")==0 ) renderAsOrg = 1;
+ else renderAsWiki = 1;
+ if( !docOnly ){
+ style_submenu_element("Text", "%s", url_render(&url, "txt","1",0,0));
+ }
+ }
+ }else if( fossil_strcmp(zMime, "image/svg+xml")==0 ){
+ if( asText ){
+ style_submenu_element("Svg", "%s", url_render(&url, "txt", 0, 0, 0));
+ }else{
+ renderAsSvg = 1;
+ if( !docOnly ){
+ style_submenu_element("Text", "%s", url_render(&url, "txt","1",0,0));
+ }
+ }
+ }
+ if( !docOnly && fileedit_is_editable(zName) ){
+ style_submenu_element("Edit",
+ "%R/fileedit?filename=%T&checkin=%!S",
+ zName, zCI);
+ }
+ }
+ if( (objType & (OBJTYPE_WIKI|OBJTYPE_TICKET))!=0 ){
+ style_submenu_element("Parsed", "%R/info/%s", zUuid);
+ }
+ if( descOnly ){
+ style_submenu_element("Content", "%R/artifact/%s", zUuid);
+ }else{
+ if( !docOnly || !isFile ){
+ @ <hr>
+ }
+ content_get(rid, &content);
+ if( renderAsOrg ){
+ Blob orgHtml = BLOB_INITIALIZER;
+ org_to_html(&content,0,&orgHtml);
+ @ %s(blob_str(&orgHtml))
+ blob_reset(&orgHtml);
+ }else if( renderAsWiki ){
+ safe_html_context(DOCSRC_FILE);
+ wiki_render_by_mimetype(&content, zMime);
+ document_emit_js();
+ }else if( renderAsHtml ){
+ @ <iframe src="%R/raw/%s(zUuid)"
+ @ width="100%%" frameborder="0" marginwidth="0" marginheight="0"
+ @ sandbox="allow-same-origin" id="ifm1">
+ @ </iframe>
+ @ <script nonce="%h(style_nonce())">/* info.c:%d(__LINE__) */
+ @ document.getElementById("ifm1").addEventListener("load",
+ @ function(){
+ @ this.height=this.contentDocument.documentElement.scrollHeight + 75;
+ @ }
+ @ );
+ @ </script>
+ }else if( renderAsSvg ){
+ @ <object type="image/svg+xml" data="%R/raw/%s(zUuid)"></object>
+ }else{
+ const char *zContentMime;
+ style_submenu_element("Hex", "%R/hexdump?name=%s", zUuid);
+ if( zLn==0 || atoi(zLn)==0 ){
+ style_submenu_checkbox("ln", "Line Numbers", 0, 0);
+ }
+ blob_to_utf8_no_bom(&content, 0);
+ zContentMime = mimetype_from_content(&content);
+ if( zMime==0 ) zMime = zContentMime;
+ @ <blockquote class="file-content">
+ if( zContentMime==0 ){
+ const char *z, *zFileName, *zExt;
+ z = blob_str(&content);
+ zFileName = db_text(0,
+ "SELECT name FROM mlink, filename"
+ " WHERE filename.fnid=mlink.fnid"
+ " AND mlink.fid=%d",
+ rid);
+ zExt = zFileName ? file_extension(zFileName) : 0;
+ if( zLn ){
+ output_text_with_line_numbers(z, blob_size(&content),
+ zFileName, zLn, 1);
+ }else if( zExt && zExt[0] ){
+ @ <pre>
+ @ <code class="language-%s(zExt)">%h(z)</code>
+ @ </pre>
+ }else{
+ @ <pre>
+ @ %h(z)
+ @ </pre>
+ }
+ }else if( strncmp(zMime, "image/", 6)==0 ){
+ @ <p>(file is %d(blob_size(&content)) bytes of image data)</i></p>
+ @ <p><img src="%R/raw/%s(zUuid)?m=%s(zMime)"></p>
+ style_submenu_element("Image", "%R/raw/%s?m=%s", zUuid, zMime);
+ }else if( strncmp(zMime, "audio/", 6)==0 ){
+ @ <p>(file is %d(blob_size(&content)) bytes of sound data)</i></p>
+ @ <audio controls src="%R/raw/%s(zUuid)?m=%s(zMime)">
+ @ (Not supported by this browser)
+ @ </audio>
+ }else{
+ @ <i>(file is %d(blob_size(&content)) bytes of binary data)</i>
+ }
+ @ </blockquote>
+ }
+ }
+ style_finish_page();
+}
+
+/*
+** WEBPAGE: tinfo
+** URL: /tinfo?name=ARTIFACTID
+**
+** Show the details of a ticket change control artifact.
+*/
+void tinfo_page(void){
+ int rid;
+ char *zDate;
+ const char *zUuid;
+ char zTktName[HNAME_MAX+1];
+ Manifest *pTktChng;
+ int modPending;
+ const char *zModAction;
+ char *zTktTitle;
+ login_check_credentials();
+ if( !g.perm.RdTkt ){ login_needed(g.anon.RdTkt); return; }
+ rid = name_to_rid_www("name");
+ if( rid==0 ){ fossil_redirect_home(); }
+ cgi_check_for_malice();
+ zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid);
+ if( g.perm.Admin ){
+ if( db_exists("SELECT 1 FROM shun WHERE uuid=%Q", zUuid) ){
+ style_submenu_element("Unshun", "%R/shun?accept=%s&sub=1#accshun", zUuid);
+ }else{
+ style_submenu_element("Shun", "%R/shun?shun=%s#addshun", zUuid);
+ }
+ }
+ pTktChng = manifest_get(rid, CFTYPE_TICKET, 0);
+ if( pTktChng==0 ) fossil_redirect_home();
+ zDate = db_text(0, "SELECT datetime(%.12f,toLocal())", pTktChng->rDate);
+ sqlite3_snprintf(sizeof(zTktName), zTktName, "%s", pTktChng->zTicketUuid);
+ if( g.perm.ModTkt && (zModAction = P("modaction"))!=0 ){
+ if( strcmp(zModAction,"delete")==0 ){
+ moderation_disapprove(rid);
+ /*
+ ** Next, check if the ticket still exists; if not, we cannot
+ ** redirect to it.
+ */
+ if( db_exists("SELECT 1 FROM ticket WHERE tkt_uuid GLOB '%q*'",
+ zTktName) ){
+ cgi_redirectf("%R/tktview/%s", zTktName);
+ /*NOTREACHED*/
+ }else{
+ cgi_redirectf("%R/modreq");
+ /*NOTREACHED*/
+ }
+ }
+ if( strcmp(zModAction,"approve")==0 ){
+ moderation_approve('t', rid);
+ }
+ }
+ zTktTitle = db_table_has_column("repository", "ticket", "title" )
+ ? db_text("(No title)",
+ "SELECT title FROM ticket WHERE tkt_uuid=%Q", zTktName)
+ : 0;
+ style_set_current_feature("tinfo");
+ style_header("Ticket Change Details");
+ style_submenu_element("Raw", "%R/artifact/%s", zUuid);
+ style_submenu_element("History", "%R/tkthistory/%s#%S", zTktName,zUuid);
+ style_submenu_element("Page", "%R/tktview/%t", zTktName);
+ style_submenu_element("Timeline", "%R/tkttimeline/%t", zTktName);
+ if( P("plaintext") ){
+ style_submenu_element("Formatted", "%R/info/%s", zUuid);
+ }else{
+ style_submenu_element("Plaintext", "%R/info/%s?plaintext", zUuid);
+ }
+
+ @ <div class="section">Overview</div>
+ @ <p><table class="label-value">
+ @ <tr><th>Artifact&nbsp;ID:</th>
+ @ <td>%z(href("%R/artifact/%!S",zUuid))%s(zUuid)</a>
+ if( g.perm.Setup ){
+ @ (%d(rid))
+ }
+ modPending = moderation_pending_www(rid);
+ @ <tr><th>Ticket:</th>
+ @ <td>%z(href("%R/tktview/%s",zTktName))%s(zTktName)</a>
+ if( zTktTitle ){
+ @<br>%h(zTktTitle)
+ }
+ @</td></tr>
+ @ <tr><th>User&nbsp;&amp;&nbsp;Date:</th><td>
+ hyperlink_to_user(pTktChng->zUser, zDate, " on ");
+ hyperlink_to_date(zDate, "</td></tr>");
+ @ </table>
+ free(zDate);
+ free(zTktTitle);
+
+ if( g.perm.ModTkt && modPending ){
+ @ <div class="section">Moderation</div>
+ @ <blockquote>
+ @ <form method="POST" action="%R/tinfo/%s(zUuid)">
+ @ <label><input type="radio" name="modaction" value="delete">
+ @ Delete this change</label><br>
+ @ <label><input type="radio" name="modaction" value="approve">
+ @ Approve this change</label><br>
+ @ <input type="submit" value="Submit">
+ @ </form>
+ @ </blockquote>
+ }
+
+ @ <div class="section">Changes</div>
+ @ <p>
+ ticket_output_change_artifact(pTktChng, 0, 1, 0);
+ manifest_destroy(pTktChng);
+ style_finish_page();
+}
+
+/*
+** rid is a cluster. Paint a page that contains detailed information
+** about that cluster.
+*/
+static void cluster_info(int rid, const char *zName){
+ Manifest *pCluster;
+ int i;
+ Blob where = BLOB_INITIALIZER;
+ Blob unks = BLOB_INITIALIZER;
+ Stmt q;
+ char *zSha1Bg;
+ char *zSha3Bg;
+ int badRid = 0;
+ int rcvid;
+ int hashClr = PB("hclr");
+ const char *zDate;
+
+ pCluster = manifest_get(rid, CFTYPE_CLUSTER, 0);
+ if( pCluster==0 ){
+ artifact_page();
+ return;
+ }
+ style_header("Cluster %S", zName);
+ rcvid = db_int(0, "SELECT rcvid FROM blob WHERE rid=%d", rid);
+ if( rcvid==0 ){
+ zDate = 0;
+ }else{
+ zDate = db_text(0, "SELECT datetime(mtime) FROM rcvfrom WHERE rcvid=%d",
+ rcvid);
+ }
+ @ <p>Artifact %z(href("%R/artifact/%h",zName))%S(zName)</a> is a cluster
+ @ with %d(pCluster->nCChild) entries
+ if( g.perm.Admin ){
+ @ received <a href="%R/rcvfrom?rcvid=%d(rcvid)">%h(zDate)</a>:
+ }else{
+ @ received %h(zDate):
+ }
+ blob_appendf(&where,"IN(0");
+ for(i=0; i<pCluster->nCChild; i++){
+ int rid = fast_uuid_to_rid(pCluster->azCChild[i]);
+ if( rid ){
+ blob_appendf(&where,",%d", rid);
+ }else{
+ if( blob_size(&unks)>0 ) blob_append_char(&unks, ',');
+ badRid++;
+ blob_append_sql(&unks,"(%d,%Q)",-badRid,pCluster->azCChild[i]);
+ }
+ }
+ blob_append_char(&where,')');
+ describe_artifacts(blob_str(&where));
+ blob_reset(&where);
+ if( badRid>0 ){
+ db_multi_exec(
+ "WITH unks(rx,hx) AS (VALUES %s)\n"
+ "INSERT INTO description(rid,uuid,type,summary) "
+ " SELECT rx, hx, 'phantom', '' FROM unks;",
+ blob_sql_text(&unks)
+ );
+ }
+ blob_reset(&unks);
+ db_prepare(&q,
+ "SELECT rid, uuid, summary, isPrivate, type='phantom', rcvid, ref"
+ " FROM description ORDER BY uuid"
+ );
+ if( skin_detail_boolean("white-foreground") ){
+ zSha1Bg = "#714417";
+ zSha3Bg = "#177117";
+ }else{
+ zSha1Bg = "#ebffb0";
+ zSha3Bg = "#b0ffb0";
+ }
+ @ <table cellpadding="2" cellspacing="0" border="1">
+ if( g.perm.Admin ){
+ @ <tr><th>RID<th>Hash<th>Rcvid<th>Description<th>Ref<th>Remarks
+ }else{
+ @ <tr><th>RID<th>Hash<th>Description<th>Ref<th>Remarks
+ }
+ while( db_step(&q)==SQLITE_ROW ){
+ int rid = db_column_int(&q,0);
+ const char *zUuid = db_column_text(&q, 1);
+ const char *zDesc = db_column_text(&q, 2);
+ int isPriv = db_column_int(&q,3);
+ int isPhantom = db_column_int(&q,4);
+ const char *zRef = db_column_text(&q,6);
+ if( isPriv && !isPhantom && !g.perm.Private && !g.perm.Admin ){
+ /* Don't show private artifacts to users without Private (x) permission */
+ continue;
+ }
+ if( rid<=0 ){
+ @ <tr><td>&nbsp;</td>
+ }else if( hashClr ){
+ const char *zClr = db_column_bytes(&q,1)>40 ? zSha3Bg : zSha1Bg;
+ @ <tr style='background-color:%s(zClr);'><td align="right">%d(rid)</td>
+ }else{
+ @ <tr><td align="right">%d(rid)</td>
+ }
+ if( rid<=0 ){
+ @ <td>&nbsp;%S(zUuid)&nbsp;</td>
+ }else{
+ @ <td>&nbsp;%z(href("%R/info/%!S",zUuid))%S(zUuid)</a>&nbsp;</td>
+ }
+ if( g.perm.Admin ){
+ int rcvid = db_column_int(&q,5);
+ if( rcvid<=0 ){
+ @ <td>&nbsp;
+ }else{
+ @ <td><a href='%R/rcvfrom?rcvid=%d(rcvid)'>%d(rcvid)</a>
+ }
+ }
+ @ <td align="left">%h(zDesc)</td>
+ if( zRef && zRef[0] ){
+ @ <td>%z(href("%R/info/%!S",zRef))%S(zRef)</a>
+ }else{
+ @ <td>&nbsp;
+ }
+ if( isPriv || isPhantom ){
+ if( isPriv==0 ){
+ @ <td>phantom</td>
+ }else if( isPhantom==0 ){
+ @ <td>private</td>
+ }else{
+ @ <td>private,phantom</td>
+ }
+ }else{
+ @ <td>&nbsp;
+ }
+ @ </tr>
+ }
+ @ </table>
+ db_finalize(&q);
+ style_finish_page();
+}
+
+
+/*
+** WEBPAGE: info
+** URL: info/NAME
+**
+** The NAME argument is any valid artifact name: an artifact hash,
+** a timestamp, a tag name, etc.
+**
+** Because NAME can match so many different things (commit artifacts,
+** wiki pages, ticket comments, forum posts...) the format of the output
+** page depends on the type of artifact that NAME matches.
+*/
+void info_page(void){
+ const char *zName;
+ Blob uuid;
+ int rid;
+ int rc;
+ int nLen;
+
+ zName = P("name");
+ if( zName==0 ) fossil_redirect_home();
+ cgi_check_for_malice();
+ nLen = strlen(zName);
+ blob_set(&uuid, zName);
+ if( name_collisions(zName) ){
+ cgi_set_parameter("src","info");
+ ambiguous_page();
+ return;
+ }
+ rc = name_to_uuid(&uuid, -1, "*");
+ if( rc==1 ){
+ if( validate16(zName, nLen) ){
+ if( db_exists("SELECT 1 FROM ticket WHERE tkt_uuid GLOB '%q*'", zName) ){
+ cgi_set_parameter_nocopy("tl","1",0);
+ tktview_page();
+ return;
+ }
+ if( db_exists("SELECT 1 FROM tag"
+ " WHERE tagname GLOB 'event-%q*'", zName) ){
+ event_page();
+ return;
+ }
+ }
+ style_header("No Such Object");
+ @ <p>No such object: %h(zName)</p>
+ if( nLen<4 ){
+ @ <p>Object name should be no less than 4 characters. Ten or more
+ @ characters are recommended.</p>
+ }
+ style_finish_page();
+ return;
+ }else if( rc==2 ){
+ cgi_set_parameter("src","info");
+ ambiguous_page();
+ return;
+ }
+ zName = blob_str(&uuid);
+ rid = db_int(0, "SELECT rid FROM blob WHERE uuid=%Q", zName);
+ if( rid==0 ){
+ style_header("Broken Link");
+ @ <p>No such object: %h(zName)</p>
+ style_finish_page();
+ return;
+ }
+ if( db_exists("SELECT 1 FROM mlink WHERE mid=%d", rid) ){
+ ci_page();
+ }else
+ if( db_exists("SELECT 1 FROM tagxref JOIN tag USING(tagid)"
+ " WHERE rid=%d AND tagname LIKE 'wiki-%%'", rid) ){
+ winfo_page();
+ }else
+ if( db_exists("SELECT 1 FROM tagxref JOIN tag USING(tagid)"
+ " WHERE rid=%d AND tagname LIKE 'tkt-%%'", rid) ){
+ tinfo_page();
+ }else
+ if( db_table_exists("repository","forumpost")
+ && db_exists("SELECT 1 FROM forumpost WHERE fpid=%d", rid)
+ ){
+ forumthread_page();
+ }else
+ if( db_exists("SELECT 1 FROM plink WHERE cid=%d", rid) ){
+ ci_page();
+ }else
+ if( db_exists("SELECT 1 FROM plink WHERE pid=%d", rid) ){
+ ci_page();
+ }else
+ if( db_exists("SELECT 1 FROM attachment WHERE attachid=%d", rid) ){
+ ainfo_page();
+ }else
+ if( db_exists("SELECT 1 FROM tagxref WHERE rid=%d AND tagid=%d",
+ rid, TAG_CLUSTER) ){
+ cluster_info(rid, zName);
+ }else
+ {
+ artifact_page();
+ }
+}
+
+/*
+** Do a comment comparison.
+**
+** + Leading and trailing whitespace are ignored.
+** + \r\n characters compare equal to \n
+**
+** Return true if equal and false if not equal.
+*/
+static int comment_compare(const char *zA, const char *zB){
+ if( zA==0 ) zA = "";
+ if( zB==0 ) zB = "";
+ while( fossil_isspace(zA[0]) ) zA++;
+ while( fossil_isspace(zB[0]) ) zB++;
+ while( zA[0] && zB[0] ){
+ if( zA[0]==zB[0] ){ zA++; zB++; continue; }
+ if( zA[0]=='\r' && zA[1]=='\n' && zB[0]=='\n' ){
+ zA += 2;
+ zB++;
+ continue;
+ }
+ if( zB[0]=='\r' && zB[1]=='\n' && zA[0]=='\n' ){
+ zB += 2;
+ zA++;
+ continue;
+ }
+ return 0;
+ }
+ while( fossil_isspace(zB[0]) ) zB++;
+ while( fossil_isspace(zA[0]) ) zA++;
+ return zA[0]==0 && zB[0]==0;
+}
+
+/*
+*****************************************************************************
+** The following methods operate on the "newtags" temporary table. This
+** table collects changes for a control artifact that will implement an
+** edit to a check-in.
+**
+** Initialize the newtags table
+*/
+static void init_newtags(void){
+ db_multi_exec("CREATE TEMP TABLE newtags(tag UNIQUE, prefix, value)");
+}
+
+/* Add a new changes to the newtags table */
+static void change_special(
+ const char *zName, /* Name of the special tag */
+ const char *zOp, /* Operation prefix (e.g. +,-,*) */
+ const char *zValue /* Value of the tag */
+){
+ db_multi_exec("REPLACE INTO newtags VALUES(%Q,'%q',%Q)", zName, zOp, zValue);
+}
+
+/* Change a symbolic tag */
+static void change_sym_tag(const char *zTag, const char *zOp){
+ db_multi_exec("REPLACE INTO newtags VALUES('sym-%q',%Q,NULL)", zTag, zOp);
+}
+
+/* Cancel a tag */
+static void cancel_special(const char *zTag){
+ change_special(zTag,"-",0);
+}
+
+/* Add a background color tag. This will be propagating tag
+** if fPropagateColor is true. */
+static void add_color(const char *zNewColor, int fPropagateColor){
+ change_special("bgcolor",fPropagateColor ? "*" : "+", zNewColor);
+}
+
+/* Cancel a background color tag */
+static void cancel_color(void){
+ change_special("bgcolor","-",0);
+}
+
+/* Add a comment-change tag */
+static void add_comment(const char *zNewComment){
+ change_special("comment","+",zNewComment);
+}
+
+/* Add a date-change tag */
+static void add_date(const char *zNewDate){
+ change_special("date","+",zNewDate);
+}
+
+/* Add a change-user tag */
+static void add_user(const char *zNewUser){
+ change_special("user","+",zNewUser);
+}
+
+/* Add a generic symbolic tag (one that has no value) */
+static void add_tag(const char *zNewTag){
+ change_sym_tag(zNewTag,"+");
+}
+
+/* Cancel an existing symbolic tag associated with check-in rid */
+static void cancel_tag(int rid, const char *zCancelTag){
+ if( db_exists("SELECT 1 FROM tagxref, tag"
+ " WHERE tagxref.rid=%d AND tagtype>0"
+ " AND tagxref.tagid=tag.tagid AND tagname='sym-%q'",
+ rid, zCancelTag)
+ ){
+ change_sym_tag(zCancelTag,"-");
+ }
+}
+
+/* Add a hidden tag that propagates to all ancestors */
+static void hide_branch(void){
+ change_special("hidden","*",0);
+}
+
+/* Close the branch */
+static void close_leaf(int rid){
+ change_special("closed",is_a_leaf(rid)?"+":"*",0);
+}
+
+/* Move the check-in to a different branch */
+static void change_branch(int rid, const char *zNewBranch){
+ db_multi_exec(
+ "REPLACE INTO newtags "
+ " SELECT tagname, '-', NULL FROM tagxref, tag"
+ " WHERE tagxref.rid=%d AND tagtype==2"
+ " AND tagname GLOB 'sym-*'"
+ " AND tag.tagid=tagxref.tagid",
+ rid
+ );
+ change_special("branch","*",zNewBranch);
+ change_sym_tag(zNewBranch,"*");
+}
+
+/*
+** Construct a control artifact.
+**
+** Preconditions:
+**
+** (1) "ctrl" contains the beginning of a control artifact with
+** just the D-card showing the timestamp on the control artifact
+** itself.
+**
+** (2) The newtags temporary table has been constructed.
+**
+** (3) Zero or more methods may have been called to populate the
+** newtags table. Or the newtags table might be empty.
+**
+** Construct the complete control artifact. Or, if there are not
+** changes, just zero out the ctrl blob.
+*/
+static void construct_newtags_artifact(
+ Blob *ctrl, /* The control artifact text */
+ int rid, /* rid of the check-in that the artifact applies to */
+ const char *zUuid, /* UUID of the check-in to which the artifact applies*/
+ const char *zUserOvrd /* The user name on the control artifact */
+){
+ Stmt q;
+ int nChng = 0;
+
+ db_prepare(&q, "SELECT tag, prefix, value FROM newtags"
+ " ORDER BY prefix || tag");
+ while( db_step(&q)==SQLITE_ROW ){
+ const char *zTag = db_column_text(&q, 0);
+ const char *zPrefix = db_column_text(&q, 1);
+ const char *zValue = db_column_text(&q, 2);
+ nChng++;
+ if( zValue ){
+ blob_appendf(ctrl, "T %s%F %s %F\n", zPrefix, zTag, zUuid, zValue);
+ }else{
+ blob_appendf(ctrl, "T %s%F %s\n", zPrefix, zTag, zUuid);
+ }
+ }
+ db_finalize(&q);
+ if( nChng>0 ){
+ Blob cksum;
+ if( zUserOvrd && zUserOvrd[0] ){
+ blob_appendf(ctrl, "U %F\n", zUserOvrd);
+ }else{
+ blob_appendf(ctrl, "U %F\n", login_name());
+ }
+ md5sum_blob(ctrl, &cksum);
+ blob_appendf(ctrl, "Z %b\n", &cksum);
+ blob_reset(&cksum);
+ }else{
+ blob_reset(ctrl);
+ }
+}
+
+/*
+** Return true if the artifact is complete. We assume that any non-empty
+** artifact is complete.
+*/
+static int artifact_is_complete(Blob *ctrl){
+ return blob_size(ctrl)>0;
+}
+
+/*
+** Apply a control artifact to the repository database. Except, if the
+** artifact is empty, this routine is a no-op.
+*/
+static void publish_newtags_artifact(Blob *ctrl, int rid){
+ if( artifact_is_complete(ctrl) ){
+ int nrid;
+ g.markPrivate = content_is_private(rid);
+ nrid = content_put(ctrl);
+ manifest_crosslink(nrid, ctrl, MC_PERMIT_HOOKS);
+ }
+}
+/* End of the newtags subsystem
+******************************************************************************/
+
+/*
+** This method checks that the date can be parsed.
+** Returns 1 if datetime() can validate, 0 otherwise.
+*/
+int is_datetime(const char* zDate){
+ return db_int(0, "SELECT datetime(%Q) NOT NULL", zDate);
+}
+
+/*
+** WEBPAGE: ci_edit
+**
+** Edit a check-in. (Check-ins are immutable and do not really change.
+** This page really creates supplemental tags that affect the display
+** of the check-in.)
+**
+** Query parameters:
+**
+** rid=INTEGER Record ID of the check-in to edit (REQUIRED)
+**
+** POST parameters after pressing "Preview", "Cancel", or "Apply":
+**
+** c=TEXT New check-in comment
+** u=TEXT New user name
+** newclr Apply a background color
+** clr=TEXT New background color (only if newclr)
+** pclr Propagate new background color (only if newclr)
+** dt=TEXT New check-in date/time (ISO8610 format)
+** newtag Add a new tag to the check-in
+** tagname=TEXT Name of the new tag to be added (only if newtag)
+** newbr Put the check-in on a new branch
+** brname=TEXT Name of the new branch (only if newbr)
+** close Close this check-in
+** hide Hide this check-in
+** cNNN Cancel tag with tagid=NNN
+**
+** cancel Cancel the edit. Return to the check-in view
+** preview Show a preview of the edited check-in comment
+** apply Apply changes
+*/
+void ci_edit_page(void){
+ int rid;
+ const char *zComment; /* Current comment on the check-in */
+ const char *zNewComment; /* Revised check-in comment */
+ const char *zUser; /* Current user for the check-in */
+ const char *zNewUser; /* Revised user */
+ const char *zDate; /* Current date of the check-in */
+ const char *zNewDate; /* Revised check-in date */
+ const char *zNewColorFlag; /* "checked" if "Change color" is checked */
+ const char *zColor; /* Current background color */
+ const char *zNewColor; /* Revised background color */
+ const char *zNewTagFlag; /* "checked" if "Add tag" is checked */
+ const char *zNewTag; /* Name of the new tag */
+ const char *zNewBrFlag; /* "checked" if "New branch" is checked */
+ const char *zNewBranch; /* Name of the new branch */
+ const char *zCloseFlag; /* "checked" if "Close" is checked */
+ const char *zHideFlag; /* "checked" if "Hide" is checked */
+ int fPropagateColor; /* True if color propagates before edit */
+ int fNewPropagateColor; /* True if color propagates after edit */
+ int fHasHidden = 0; /* True if hidden tag already set */
+ int fHasClosed = 0; /* True if closed tag already set */
+ const char *zChngTime = 0; /* Value of chngtime= query param, if any */
+ int bApply = P("apply")!=0;
+ int bPreview = P("preview")!=0;
+ char *zUuid;
+ Blob comment;
+ char *zBranchName = 0;
+ Stmt q;
+ Blob ctrl; /* The generated control artifact */
+
+ login_check_credentials();
+ if( !g.perm.Write ){ login_needed(g.anon.Write); return; }
+ rid = name_to_typed_rid(P("r"), "ci");
+ zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
+ zComment = db_text(0, "SELECT coalesce(ecomment,comment)"
+ " FROM event WHERE objid=%d", rid);
+ if( zComment==0 ) fossil_redirect_home();
+ if( P("cancel") ){
+ cgi_redirectf("%R/ci/%S", zUuid);
+ }
+ if( g.perm.Setup ) zChngTime = P("chngtime");
+ zNewComment = PD("c",zComment);
+ zUser = db_text(0, "SELECT coalesce(euser,user)"
+ " FROM event WHERE objid=%d", rid);
+ if( zUser==0 ) fossil_redirect_home();
+ zNewUser = PDT("u",zUser);
+ zDate = db_text(0, "SELECT datetime(mtime)"
+ " FROM event WHERE objid=%d", rid);
+ if( zDate==0 ) fossil_redirect_home();
+ zNewDate = PDT("dt",zDate);
+ zColor = db_text("", "SELECT bgcolor"
+ " FROM event WHERE objid=%d", rid);
+ zNewColor = PDT("clr",zColor);
+ fPropagateColor = db_int(0, "SELECT tagtype FROM tagxref"
+ " WHERE rid=%d AND tagid=%d",
+ rid, TAG_BGCOLOR)==2;
+ fNewPropagateColor = P("clr")!=0 ? P("pclr")!=0 : fPropagateColor;
+ zNewColorFlag = P("newclr") ? " checked" : "";
+ zNewTagFlag = P("newtag") ? " checked" : "";
+ zNewTag = PDT("tagname","");
+ zNewBrFlag = P("newbr") ? " checked" : "";
+ zNewBranch = PDT("brname","");
+ zBranchName = branch_of_rid(rid);
+ zCloseFlag = P("close") ? " checked" : "";
+ zHideFlag = P("hide") ? " checked" : "";
+ blob_zero(&ctrl);
+ if( bApply && !cgi_csrf_safe(2) ) bApply = 0;
+ if( bApply || bPreview ){
+ char *zNow = date_in_standard_format(zChngTime ? zChngTime : "now");
+ blob_appendf(&ctrl, "D %s\n", zNow);
+ init_newtags();
+ if( zNewColorFlag[0]
+ && zNewColor[0]
+ && (fPropagateColor!=fNewPropagateColor
+ || fossil_strcmp(zColor,zNewColor)!=0)
+ ){
+ add_color(zNewColor,fNewPropagateColor);
+ }
+ if( comment_compare(zComment,zNewComment)==0 ) add_comment(zNewComment);
+ if( fossil_strcmp(zDate,zNewDate)!=0 ) add_date(zNewDate);
+ if( fossil_strcmp(zUser,zNewUser)!=0 ) add_user(zNewUser);
+ db_prepare(&q,
+ "SELECT tag.tagid, tagname FROM tagxref, tag"
+ " WHERE tagxref.rid=%d AND tagtype>0 AND tagxref.tagid=tag.tagid",
+ rid
+ );
+ while( db_step(&q)==SQLITE_ROW ){
+ int tagid = db_column_int(&q, 0);
+ const char *zTag = db_column_text(&q, 1);
+ char zLabel[30];
+ sqlite3_snprintf(sizeof(zLabel), zLabel, "c%d", tagid);
+ if( P(zLabel) ) cancel_special(zTag);
+ }
+ db_finalize(&q);
+ if( zHideFlag[0] ) hide_branch();
+ if( zCloseFlag[0] ) close_leaf(rid);
+ if( zNewTagFlag[0] && zNewTag[0] ) add_tag(zNewTag);
+ if( zNewBrFlag[0] && zNewBranch[0] ) change_branch(rid,zNewBranch);
+ construct_newtags_artifact(&ctrl, rid, zUuid, 0);
+ if( bApply ){
+ if( artifact_is_complete(&ctrl) ){
+ db_begin_transaction();
+ publish_newtags_artifact(&ctrl, rid);
+ db_end_transaction(0);
+ blob_reset(&ctrl);
+ }
+ cgi_redirectf("%R/ci/%S", zUuid);
+ }
+ }
+ blob_zero(&comment);
+ blob_append(&comment, zNewComment, -1);
+ style_header("Edit Check-in %S", zUuid);
+ @ <div class="section accordion">Original Context Around \
+ @ Check-in %S(zUuid) on %s(zDate)</div>
+ @ <div class="accordion_panel">
+ render_checkin_context(rid, 0, 2, 0);
+ @ </div>
+ if( bPreview ){
+ @ <div class="section accordion">After The Proposed Changes</div>
+ @ <div class="accordion_panel">
+ db_begin_transaction();
+ publish_newtags_artifact(&ctrl, rid);
+ render_checkin_context(rid, 0, 2, 0);
+ db_end_transaction(1);
+ @ </div>
+ }
+ @ <div class="section accordion">Proposed Changes For \
+ @ Check-In %S(zUuid):</div>
+ @ <div class="accordion_panel">
+ form_begin(0, "%R/ci_edit");
+ @ <div><input type="hidden" name="r" value="%s(zUuid)">
+ @ <table border="0" cellspacing="10">
+
+ @ <tr><th align="right" valign="top">User:</th>
+ @ <td valign="top">
+ @ <input type="text" name="u" size="20" value="%h(zNewUser)">
+ @ </td></tr>
+
+ @ <tr><th align="right" valign="top">Comment:</th>
+ @ <td valign="top">
+ @ <textarea name="c" rows="10" cols="80">%h(zNewComment)</textarea>
+ @ </td></tr>
+
+ @ <tr><th align="right" valign="top">Check-in Time:</th>
+ @ <td valign="top">
+ @ <input type="text" name="dt" size="20" value="%h(zNewDate)">
+ @ </td></tr>
+
+ if( zChngTime ){
+ @ <tr><th align="right" valign="top">Timestamp of this change:</th>
+ @ <td valign="top">
+ @ <input type="text" name="chngtime" size="20" value="%h(zChngTime)">
+ @ </td></tr>
+ }
+
+ @ <tr><th align="right" valign="top">Background&nbsp;Color:</th>
+ @ <td valign="top">
+ @ <div><label><input type='checkbox' name='newclr'%s(zNewColorFlag)>
+ @ Change background color: \
+ @ <input type='color' name='clr'\
+ @ value='%s(zNewColor[0]?zNewColor:"#808080")'></label></div>
+ @ <div><label>
+ if( fNewPropagateColor ){
+ @ <input type="checkbox" name="pclr" checked="checked">
+ }else{
+ @ <input type="checkbox" name="pclr">
+ }
+ @ Propagate color to descendants</label></div>
+ @ <div class='font-size-80'>Be aware that fixed background
+ @ colors will not interact well with all available skins.
+ @ It is recommended that Fossil be allowed to select these
+ @ colors automatically so that it can take the skin's
+ @ preferences into account.</div>
+ @ </td></tr>
+
+ @ <tr><th align="right" valign="top">Tags:</th>
+ @ <td valign="top">
+ @ <label><input type="checkbox" id="newtag" name="newtag"%s(zNewTagFlag)>
+ @ Add the following new tag name to this check-in:</label>
+ @ <input size="15" name="tagname" id="tagname" value="%h(zNewTag)">
+ db_prepare(&q,
+ "SELECT tag.tagid, tagname, tagxref.value FROM tagxref, tag"
+ " WHERE tagxref.rid=%d AND tagtype>0 AND tagxref.tagid=tag.tagid"
+ " ORDER BY CASE WHEN tagname GLOB 'sym-*' THEN substr(tagname,5)"
+ " ELSE tagname END /*sort*/",
+ rid
+ );
+ while( db_step(&q)==SQLITE_ROW ){
+ int tagid = db_column_int(&q, 0);
+ const char *zTagName = db_column_text(&q, 1);
+ int isSpecialTag = fossil_strncmp(zTagName, "sym-", 4)!=0;
+ char zLabel[30];
+
+ if( tagid == TAG_CLOSED ){
+ fHasClosed = 1;
+ }else if( (tagid == TAG_COMMENT) || (tagid == TAG_BRANCH) ){
+ continue;
+ }else if( tagid==TAG_HIDDEN ){
+ fHasHidden = 1;
+ }else if( !isSpecialTag && zTagName &&
+ fossil_strcmp(&zTagName[4], zBranchName)==0){
+ continue;
+ }
+ sqlite3_snprintf(sizeof(zLabel), zLabel, "c%d", tagid);
+ @ <br><label>
+ if( P(zLabel) ){
+ @ <input type="checkbox" name="c%d(tagid)" checked="checked">
+ }else{
+ @ <input type="checkbox" name="c%d(tagid)">
+ }
+ if( isSpecialTag ){
+ @ Cancel special tag <b>%h(zTagName)</b></label>
+ }else{
+ @ Cancel tag <b>%h(&zTagName[4])</b></label>
+ }
+ }
+ db_finalize(&q);
+ @ </td></tr>
+
+ if( !zBranchName ){
+ zBranchName = fossil_strdup(db_main_branch());
+ }
+ if( !zNewBranch || !zNewBranch[0]){
+ zNewBranch = zBranchName;
+ }
+ @ <tr><th align="right" valign="top">Branching:</th>
+ @ <td valign="top">
+ @ <label><input id="newbr" type="checkbox" name="newbr" \
+ @ data-branch='%h(zBranchName)'%s(zNewBrFlag)>
+ @ Starting from this check-in, rename the branch to:</label>
+ @ <input id="brname" type="text" style="width:15;" name="brname" \
+ @ value="%h(zNewBranch)"></td></tr>
+ if( !fHasHidden ){
+ @ <tr><th align="right" valign="top">Branch Hiding:</th>
+ @ <td valign="top">
+ @ <label><input type="checkbox" id="hidebr" name="hide"%s(zHideFlag)>
+ @ Hide branch
+ @ <span style="font-weight:bold" id="hbranch">%h(zBranchName)</span>
+ @ from the timeline starting from this check-in</label>
+ @ </td></tr>
+ }
+ if( !fHasClosed ){
+ if( is_a_leaf(rid) ){
+ @ <tr><th align="right" valign="top">Leaf Closure:</th>
+ @ <td valign="top">
+ @ <label><input type="checkbox" name="close"%s(zCloseFlag)>
+ @ Mark this leaf as "closed" so that it no longer appears on the
+ @ "leaves" page and is no longer labeled as a "<b>Leaf</b>"</label>
+ @ </td></tr>
+ }else if( zBranchName ){
+ @ <tr><th align="right" valign="top">Branch Closure:</th>
+ @ <td valign="top">
+ @ <label><input type="checkbox" name="close"%s(zCloseFlag)>
+ @ Mark branch
+ @ <span style="font-weight:bold" id="cbranch">%h(zBranchName)</span>
+ @ as "closed".</label>
+ @ </td></tr>
+ }
+ }
+ if( zBranchName ) fossil_free(zBranchName);
+
+
+ @ <tr><td colspan="2">
+ @ <input type="submit" name="cancel" value="Cancel">
+ @ <input type="submit" name="preview" value="Preview">
+ if( P("preview") ){
+ @ <input type="submit" name="apply" value="Apply Changes">
+ }
+ @ </td></tr>
+ @ </table>
+ login_insert_csrf_secret();
+ @ </div></form>
+ builtin_request_js("ci_edit.js");
+ style_finish_page();
+}
+
+/*
+** Prepare an ammended commit comment. Let the user modify it using the
+** editor specified in the global_config table or either
+** the VISUAL or EDITOR environment variable.
+**
+** Store the final commit comment in pComment. pComment is assumed
+** to be uninitialized - any prior content is overwritten.
+**
+** Use zInit to initialize the check-in comment so that the user does
+** not have to retype.
+*/
+static void prepare_amend_comment(
+ Blob *pComment,
+ const char *zInit,
+ const char *zUuid
+){
+ Blob prompt;
+#if defined(_WIN32) || defined(__CYGWIN__)
+ int bomSize;
+ const unsigned char *bom = get_utf8_bom(&bomSize);
+ blob_init(&prompt, (const char *) bom, bomSize);
+ if( zInit && zInit[0]){
+ blob_append(&prompt, zInit, -1);
+ }
+#else
+ blob_init(&prompt, zInit, -1);
+#endif
+ blob_append(&prompt, "\n# Enter a new comment for check-in ", -1);
+ if( zUuid && zUuid[0] ){
+ blob_append(&prompt, zUuid, -1);
+ }
+ blob_append(&prompt, ".\n# Lines beginning with a # are ignored.\n", -1);
+ prompt_for_user_comment(pComment, &prompt);
+ blob_reset(&prompt);
+}
+
+#define AMEND_USAGE_STMT "HASH OPTION ?OPTION ...?"
+/*
+** COMMAND: amend
+**
+** Usage: %fossil amend HASH OPTION ?OPTION ...?
+**
+** Amend the tags on check-in HASH to change how it displays in the timeline.
+**
+** Options:
+** --author USER Make USER the author for check-in
+** --bgcolor COLOR Apply COLOR to this check-in
+** --branch NAME Rename branch of check-in to NAME
+** --branchcolor COLOR Apply and propagate COLOR to the branch
+** --cancel TAG Cancel TAG from this check-in
+** --close Mark this "leaf" as closed
+** --date DATETIME Make DATETIME the check-in time
+** --date-override DATETIME Set the change time on the control artifact
+** -e|--edit-comment Launch editor to revise comment
+** --editor NAME Text editor to use for check-in comment
+** --hide Hide branch starting from this check-in
+** -m|--comment COMMENT Make COMMENT the check-in comment
+** -M|--message-file FILE Read the amended comment from FILE
+** -n|--dry-run Print control artifact, but make no changes
+** --no-verify-comment Do not validate the check-in comment
+** --tag TAG Add new TAG to this check-in
+** --user-override USER Set the user name on the control artifact
+**
+** DATETIME may be "now" or "YYYY-MM-DDTHH:MM:SS.SSS". If in
+** year-month-day form, it may be truncated, the "T" may be replaced by
+** a space, and it may also name a timezone offset from UTC as "-HH:MM"
+** (westward) or "+HH:MM" (eastward). Either no timezone suffix or "Z"
+** means UTC.
+*/
+void ci_amend_cmd(void){
+ int rid;
+ const char *zComment; /* Current comment on the check-in */
+ const char *zNewComment; /* Revised check-in comment */
+ const char *zComFile; /* Filename from which to read comment */
+ const char *zUser; /* Current user for the check-in */
+ const char *zNewUser; /* Revised user */
+ const char *zDate; /* Current date of the check-in */
+ const char *zNewDate; /* Revised check-in date */
+ const char *zColor;
+ const char *zNewColor;
+ const char *zNewBrColor;
+ const char *zNewBranch;
+ const char **pzNewTags = 0;
+ const char **pzCancelTags = 0;
+ int fClose; /* True if leaf should be closed */
+ int fHide; /* True if branch should be hidden */
+ int fPropagateColor; /* True if color propagates before amend */
+ int fNewPropagateColor = 0; /* True if color propagates after amend */
+ int fHasHidden = 0; /* True if hidden tag already set */
+ int fHasClosed = 0; /* True if closed tag already set */
+ int fEditComment; /* True if editor to be used for comment */
+ int fDryRun; /* Print control artifact, make no changes */
+ int noVerifyCom = 0; /* Allow suspicious check-in comments */
+ const char *zChngTime; /* The change time on the control artifact */
+ const char *zUserOvrd; /* The user name on the control artifact */
+ const char *zUuid;
+ Blob ctrl;
+ Blob comment;
+ char *zNow;
+ int nTags, nCancels;
+ int i;
+ Stmt q;
+ int ckComFlgs; /* Flags passed to verify_comment() */
+
+
+ fEditComment = find_option("edit-comment","e",0)!=0;
+ zNewComment = find_option("comment","m",1);
+ zComFile = find_option("message-file","M",1);
+ zNewBranch = find_option("branch",0,1);
+ zNewColor = find_option("bgcolor",0,1);
+ zNewBrColor = find_option("branchcolor",0,1);
+ if( zNewBrColor ){
+ zNewColor = zNewBrColor;
+ fNewPropagateColor = 1;
+ }
+ zNewDate = find_option("date",0,1);
+ zNewUser = find_option("author",0,1);
+ pzNewTags = find_repeatable_option("tag",0,&nTags);
+ pzCancelTags = find_repeatable_option("cancel",0,&nCancels);
+ fClose = find_option("close",0,0)!=0;
+ fHide = find_option("hide",0,0)!=0;
+ fDryRun = find_option("dry-run","n",0)!=0;
+ zChngTime = find_option("date-override",0,1);
+ if( zChngTime==0 ) zChngTime = find_option("chngtime",0,1);
+ zUserOvrd = find_option("user-override",0,1);
+ noVerifyCom = find_option("no-verify-comment",0,0)!=0;
+ db_find_and_open_repository(0,0);
+ user_select();
+ (void)fossil_text_editor();
+ verify_all_options();
+ if( g.argc<3 || g.argc>=4 ) usage(AMEND_USAGE_STMT);
+ rid = name_to_typed_rid(g.argv[2], "ci");
+ if( rid==0 && !is_a_version(rid) ) fossil_fatal("no such check-in");
+ zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
+ if( zUuid==0 ) fossil_fatal("Unable to find artifact hash");
+ zComment = db_text(0, "SELECT coalesce(ecomment,comment)"
+ " FROM event WHERE objid=%d", rid);
+ zUser = db_text(0, "SELECT coalesce(euser,user)"
+ " FROM event WHERE objid=%d", rid);
+ zDate = db_text(0, "SELECT datetime(mtime)"
+ " FROM event WHERE objid=%d", rid);
+ zColor = db_text("", "SELECT bgcolor"
+ " FROM event WHERE objid=%d", rid);
+ fPropagateColor = db_int(0, "SELECT tagtype FROM tagxref"
+ " WHERE rid=%d AND tagid=%d",
+ rid, TAG_BGCOLOR)==2;
+ fNewPropagateColor = zNewColor && zNewColor[0]
+ ? fNewPropagateColor : fPropagateColor;
+ db_prepare(&q,
+ "SELECT tag.tagid FROM tagxref, tag"
+ " WHERE tagxref.rid=%d AND tagtype>0 AND tagxref.tagid=tag.tagid",
+ rid
+ );
+ while( db_step(&q)==SQLITE_ROW ){
+ int tagid = db_column_int(&q, 0);
+
+ if( tagid == TAG_CLOSED ){
+ fHasClosed = 1;
+ }else if( tagid==TAG_HIDDEN ){
+ fHasHidden = 1;
+ }else{
+ continue;
+ }
+ }
+ db_finalize(&q);
+ blob_zero(&ctrl);
+ zNow = date_in_standard_format(zChngTime && zChngTime[0] ? zChngTime : "now");
+ blob_appendf(&ctrl, "D %s\n", zNow);
+ init_newtags();
+ if( zNewColor && zNewColor[0]
+ && (fPropagateColor!=fNewPropagateColor
+ || fossil_strcmp(zColor,zNewColor)!=0)
+ ){
+ add_color(
+ mprintf("%s%s", (zNewColor[0]!='#' &&
+ validate16(zNewColor,strlen(zNewColor)) &&
+ (strlen(zNewColor)==6 || strlen(zNewColor)==3)) ? "#" : "",
+ zNewColor
+ ),
+ fNewPropagateColor
+ );
+ }
+ if( (zNewColor!=0 && zNewColor[0]==0) && (zColor && zColor[0] ) ){
+ cancel_color();
+ }
+ if( fEditComment || zNewComment || zComFile ){
+ blob_init(&comment, 0, 0);
+
+ /* Figure out how much comment verification is requested */
+ if( noVerifyCom ){
+ ckComFlgs = 0;
+ }else{
+ const char *zVerComs = db_get("verify-comments","on");
+ if( is_false(zVerComs) ){
+ ckComFlgs = 0;
+ }else if( strcmp(zVerComs,"preview")==0 ){
+ ckComFlgs = COMCK_PREVIEW | COMCK_MARKUP;
+ }else{
+ ckComFlgs = COMCK_MARKUP;
+ }
+ }
+ if( fEditComment ){
+ prepare_amend_comment(&comment, zComment, zUuid);
+ }else if( zComFile ){
+ blob_read_from_file(&comment, zComFile, ExtFILE);
+ blob_to_utf8_no_bom(&comment, 1);
+ }else if( zNewComment ){
+ blob_init(&comment, zNewComment, -1);
+ }
+ if( blob_size(&comment)>0
+ && comment_compare(zComment, blob_str(&comment))==0
+ ){
+ int rc;
+ while( (rc = verify_comment(&comment, ckComFlgs))!=0 ){
+ char cReply;
+ Blob ans;
+ if( !fEditComment ){
+ fossil_fatal("Amend aborted; "
+ "use --no-verify-comment to override");
+ }
+ if( rc==COMCK_PREVIEW ){
+ prompt_user("Continue, abort, or edit (C/a/e)? ", &ans);
+ }else{
+ prompt_user("Edit, abort, or continue (E/a/c)? ", &ans);
+ }
+ cReply = blob_str(&ans)[0];
+ cReply = fossil_tolower(cReply);
+ blob_reset(&ans);
+ if( cReply=='a' ){
+ fossil_fatal("Amend aborted.");
+ }
+ if( cReply=='e' || (cReply!='c' && rc!=COMCK_PREVIEW) ){
+ char *zPrior = blob_materialize(&comment);
+ blob_init(&comment, 0, 0);
+ prepare_amend_comment(&comment, zPrior, zUuid);
+ fossil_free(zPrior);
+ continue;
+ }else{
+ break;
+ }
+ }
+ }
+ add_comment(blob_str(&comment));
+ }
+ if( zNewDate && zNewDate[0] && fossil_strcmp(zDate,zNewDate)!=0 ){
+ if( is_datetime(zNewDate) ){
+ add_date(zNewDate);
+ }else{
+ fossil_fatal("Unsupported date format, use YYYY-MM-DD HH:MM:SS");
+ }
+ }
+ if( zNewUser && zNewUser[0] && fossil_strcmp(zUser,zNewUser)!=0 ){
+ add_user(zNewUser);
+ }
+ if( pzNewTags!=0 ){
+ for(i=0; i<nTags; i++){
+ if( pzNewTags[i] && pzNewTags[i][0] ) add_tag(pzNewTags[i]);
+ }
+ fossil_free((void *)pzNewTags);
+ }
+ if( pzCancelTags!=0 ){
+ for(i=0; i<nCancels; i++){
+ if( pzCancelTags[i] && pzCancelTags[i][0] )
+ cancel_tag(rid,pzCancelTags[i]);
+ }
+ fossil_free((void *)pzCancelTags);
+ }
+ if( fHide && !fHasHidden ) hide_branch();
+ if( fClose && !fHasClosed ) close_leaf(rid);
+ if( zNewBranch && zNewBranch[0] ) change_branch(rid,zNewBranch);
+ construct_newtags_artifact(&ctrl, rid, zUuid, zUserOvrd);
+ if( fDryRun ){
+ fossil_print("%s", blob_str(&ctrl));
+ blob_reset(&ctrl);
+ }else{
+ db_begin_transaction();
+ publish_newtags_artifact(&ctrl, rid);
+ show_common_info(rid, "hash:", 1, 0);
+ db_end_transaction(0);
+ }
+ if( g.localOpen ){
+ manifest_to_disk(rid);
+ }
+}
+
+
+/*
+** COMMAND: test-symlink-list
+**
+** Show all symlinks that have been checked into a Fossil repository.
+**
+** This command does a linear scan through all check-ins and so might take
+** several seconds on a large repository.
+*/
+void test_symlink_list_cmd(void){
+ Stmt q;
+ db_find_and_open_repository(0,0);
+ add_content_sql_commands(g.db);
+ db_prepare(&q,
+ "SELECT min(date(e.mtime)),"
+ " b.uuid,"
+ " f.filename,"
+ " content(f.uuid)"
+ " FROM event AS e, blob AS b, files_of_checkin(b.uuid) AS f"
+ " WHERE e.type='ci'"
+ " AND b.rid=e.objid"
+ " AND f.perm LIKE '%%l%%'"
+ " GROUP BY 3, 4"
+ " ORDER BY 1 DESC"
+ );
+ while( db_step(&q)==SQLITE_ROW ){
+ fossil_print("%s %.16s %s -> %s\n",
+ db_column_text(&q,0),
+ db_column_text(&q,1),
+ db_column_text(&q,2),
+ db_column_text(&q,3));
+ }
+ db_finalize(&q);
+}
+
+#if INTERFACE
+/*
+** Description of a check-in relative to an earlier, tagged check-in.
+*/
+typedef struct CommitDescr {
+ char *zRelTagname; /* Tag name on the relative check-in */
+ int nCommitsSince; /* Number of commits since then */
+ char *zCommitHash; /* Hash of the described check-in */
+ int isDirty; /* Working directory has uncommitted changes */
+} CommitDescr;
+#endif
+
+/*
+** Describe the check-in given by 'zName', and possibly matching 'matchGlob',
+** relative to an earlier, tagged check-in. Use 'descr' for the output.
+**
+** Finds the closest ancestor (ignoring merge-ins) that has a non-propagating
+** label tag and the number of steps backwards that we had to search in
+** order to find that tag. Tags applied to more than one check-in are ignored.
+**
+** Return values:
+** 0: ok
+** -1: zName does not resolve to a commit
+** -2: zName resolves to more than a commit
+** -3: no ancestor commit with a fitting non-propagating tag found
+*/
+int describe_commit(
+ const char *zName, /* Name of the commit to be described */
+ const char *matchGlob, /* Glob pattern for the tag */
+ CommitDescr *descr /* Write the description here */
+){
+ int rid; /* rid for zName */
+ const char *zUuid; /* Hash of rid */
+ int nRet = 0; /* Value to be returned */
+ Stmt q; /* Query for tagged ancestors */
+
+ rid = symbolic_name_to_rid(zName, "ci"); /* only commits */
+
+ if( rid<=0 ){
+ /* Commit does not exist or is ambiguous */
+ descr->zRelTagname = mprintf("");
+ descr->nCommitsSince = -1;
+ descr->zCommitHash = mprintf("");
+ descr->isDirty = -1;
+ return (rid-1);
+ }
+
+ zUuid = rid_to_uuid(rid);
+ descr->zCommitHash = fossil_strdup(zUuid);
+ descr->isDirty = unsaved_changes(0);
+
+ db_multi_exec(
+ "DROP TABLE IF EXISTS temp.singletonTag;"
+ "CREATE TEMP TABLE singletonTag("
+ " rid INT,"
+ " tagname TEXT,"
+ " PRIMARY KEY (rid,tagname)"
+ ") WITHOUT ROWID;"
+ "INSERT OR IGNORE INTO singletonTag(rid, tagname)"
+ " SELECT min(rid),"
+ " substr(tagname,5)"
+ " FROM tag, tagxref"
+ " WHERE tag.tagid=tagxref.tagid"
+ " AND tagxref.tagtype=1"
+ " AND tagname GLOB 'sym-%q'"
+ " GROUP BY tagname"
+ " HAVING count(*)==1;",
+ (matchGlob ? matchGlob : "*")
+ );
+
+ db_prepare(&q,
+ "WITH RECURSIVE"
+ " ancestor(rid,mtime,tagname,n) AS ("
+ " SELECT %d, event.mtime, singletonTag.tagname, 0 "
+ " FROM event"
+ " LEFT JOIN singletonTag ON singletonTag.rid=event.objid"
+ " WHERE event.objid=%d"
+ " UNION ALL"
+ " SELECT plink.pid, event.mtime, singletonTag.tagname, n+1"
+ " FROM ancestor, plink, event"
+ " LEFT JOIN singletonTag ON singletonTag.rid=plink.pid"
+ " WHERE plink.cid=ancestor.rid"
+ " AND plink.isprim=1"
+ " AND event.objid=plink.pid"
+ " AND ancestor.tagname IS NULL"
+ " ORDER BY mtime DESC"
+ " LIMIT 100000"
+ " )"
+ "SELECT tagname, n"
+ " FROM ancestor"
+ " WHERE tagname IS NOT NULL"
+ " ORDER BY n LIMIT 1;",
+ rid, rid
+ );
+
+ if( db_step(&q)==SQLITE_ROW ){
+ const char *lastTag = db_column_text(&q, 0);
+ descr->zRelTagname = fossil_strdup(lastTag);
+ descr->nCommitsSince = db_column_int(&q, 1);
+ nRet = 0;
+ }else{
+ /* no ancestor commit with a fitting singleton tag found */
+ descr->zRelTagname = mprintf("");
+ descr->nCommitsSince = -1;
+ nRet = -3;
+ }
+
+ db_finalize(&q);
+ return nRet;
+}
+
+/*
+** COMMAND: describe
+**
+** Usage: %fossil describe ?VERSION? ?OPTIONS?
+**
+** Provide a description of the given VERSION by showing a non-propagating
+** tag of the youngest tagged ancestor, followed by the number of commits
+** since that, and the short hash of VERSION. Only tags applied to a single
+** check-in are considered.
+**
+** If no VERSION is provided, describe the currently checked-out version.
+**
+** If VERSION and the found ancestor refer to the same commit, the last two
+** components are omitted, unless --long is provided. When no fitting tagged
+** ancestor is found, show only the short hash of VERSION.
+**
+** Options:
+** --digits Display so many hex digits of the hash
+** (default: the larger of 6 and the 'hash-digit' setting)
+** -d|--dirty Show whether there are changes to be committed
+** --long Always show all three components
+** --match GLOB Consider only non-propagating tags matching GLOB
+*/
+void describe_cmd(void){
+ const char *zName;
+ const char *zMatchGlob;
+ const char *zDigits;
+ int nDigits;
+ int bDirtyFlag = 0;
+ int bLongFlag = 0;
+ CommitDescr descr;
+
+ db_find_and_open_repository(0,0);
+ bDirtyFlag = find_option("dirty","d",0)!=0;
+ bLongFlag = find_option("long","",0)!=0;
+ zMatchGlob = find_option("match", 0, 1);
+ zDigits = find_option("digits", 0, 1);
+
+ if ( !zDigits || ((nDigits=atoi(zDigits))==0) ){
+ nDigits = hash_digits(0);
+ }
+
+ /* We should be done with options.. */
+ verify_all_options();
+ if( g.argc<3 ){
+ zName = "current";
+ }else{
+ zName = g.argv[2];
+ }
+
+ if( bDirtyFlag ){
+ if ( g.argc>=3 ) fossil_fatal("cannot use --dirty with specific check-in");
+ }
+
+ switch( describe_commit(zName, zMatchGlob, &descr) ){
+ case -1:
+ fossil_fatal("commit %s does not exist", zName);
+ break;
+ case -2:
+ fossil_fatal("commit %s is ambiguous", zName);
+ break;
+ case -3:
+ fossil_print("%.*s%s\n", nDigits, descr.zCommitHash,
+ bDirtyFlag ? (descr.isDirty ? "-dirty" : "") : "");
+ break;
+ case 0:
+ if( descr.nCommitsSince==0 && !bLongFlag ){
+ fossil_print("%s%s\n", descr.zRelTagname,
+ bDirtyFlag ? (descr.isDirty ? "-dirty" : "") : "");
+ }else{
+ fossil_print("%s-%d-%.*s%s\n", descr.zRelTagname,
+ descr.nCommitsSince, nDigits, descr.zCommitHash,
+ bDirtyFlag ? (descr.isDirty ? "-dirty" : "") : "");
+ }
+ break;
+ default:
+ fossil_fatal("cannot describe commit");
+ break;
+ }
+}