/*
** 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
/*
** 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 UUID
** * 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 *zUuidName, /* Name of the UUID */
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 %s %s\n", zUuidName, zUuid, zDate ? zDate : "");
free(zUuid);
free(zDate);
}
if( zUuid && showComment ){
zComment = db_text(0,
"SELECT coalesce(ecomment,comment) || "
" ' (user: ' || coalesce(euser,user,'?') || ')' "
" FROM event WHERE objid=%d",
rid
);
}
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 %s %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 %s %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, 14, 79);
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 name");
while( db_step(&s)==SQLITE_ROW ){
const char *zName;
const char *zCkout = db_column_text(&s, 0);
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 name");
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);
}
/*
** 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.
**
** Use the "finfo" command to get information about a specific
** file in a checkout.
**
** Options:
**
** -R|--repository FILE Extract info from repository FILE
** -l|--detail Show extra information
**
** See also: annotate, artifact, finfo, timeline
*/
void info_cmd(void){
i64 fsize;
int bDetail = find_option("detail","l",0)!=0;
if( g.argc==3 && (fsize = file_size(g.argv[2]))>0 && (fsize&0x1ff)==0 ){
db_open_config(0);
db_record_repository_filename(g.argv[2]);
db_open_repository(g.argv[2]);
fossil_print("project-name: %s\n", db_get("project-name", ""));
fossil_print("project-code: %s\n", db_get("project-code", ""));
extraRepoInfo();
return;
}
db_find_and_open_repository(0,0);
if( g.argc==2 ){
int vid;
/* 012345678901234 */
db_record_repository_filename(0);
fossil_print("project-name: %s\n", db_get("project-name", ""));
if( g.localOpen ){
fossil_print("repository: %s\n", db_repository_filename());
fossil_print("local-root: %s\n", g.zLocalRoot);
}
if( bDetail ) extraRepoInfo();
if( g.zConfigDbName ){
fossil_print("config-db: %s\n", g.zConfigDbName);
}
fossil_print("project-code: %s\n", db_get("project-code", ""));
vid = g.localOpen ? db_lget_int("checkout", 0) : 0;
if( vid ){
show_common_info(vid, "checkout:", 1, 1);
}
fossil_print("checkins: %d\n",
db_int(-1, "SELECT count(*) FROM event WHERE type='ci' /*scan*/"));
}else{
int rid;
rid = name_to_rid(g.argv[2]);
if( rid==0 ){
fossil_panic("no such object: %s\n", g.argv[2]);
}
show_common_info(rid, "uuid:", 1, 1);
}
}
/*
** Show information about all tags on a given node.
*/
static void showTags(int rid, const char *zNotGlob){
Stmt q;
int cnt = 0;
db_prepare(&q,
"SELECT tag.tagid, tagname, "
" (SELECT uuid FROM blob WHERE rid=tagxref.srcid AND rid!=%d),"
" value, datetime(tagxref.mtime,'localtime'), 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 AND tagname NOT GLOB '%q'"
" ORDER BY tagname /*sort*/", rid, rid, rid, zNotGlob
);
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 ){
@
}
blob_reset(&from);
blob_reset(&to);
blob_reset(&out);
}
/*
** 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 *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. */
u64 diffFlags, /* Flags for text_diff(). Zero to omit diffs */
ReCompiled *pRe, /* Only show diffs that match this regex, if not NULL */
int mperm /* executable or symlink permission for zNew */
){
if( !g.perm.Hyperlink ){
if( zNew==0 ){
@
}
}
/*
** Construct an appropriate diffFlag for text_diff() based on query
** parameters and the to boolean arguments.
*/
u64 construct_diff_flags(int showDiff, int sideBySide){
u64 diffFlags;
if( showDiff==0 ){
diffFlags = 0; /* Zero means do not show any diff */
}else{
int x;
if( sideBySide ){
diffFlags = DIFF_SIDEBYSIDE | DIFF_IGNORE_EOLWS;
/* "dw" query parameter determines width of each column */
x = atoi(PD("dw","80"))*(DIFF_CONTEXT_MASK+1);
if( x<0 || x>DIFF_WIDTH_MASK ) x = DIFF_WIDTH_MASK;
diffFlags += x;
}else{
diffFlags = DIFF_INLINE | DIFF_IGNORE_EOLWS;
}
/* "dc" query parameter determines lines of context */
x = atoi(PD("dc","7"));
if( x<0 || x>DIFF_CONTEXT_MASK ) x = DIFF_CONTEXT_MASK;
diffFlags += x;
/* The "noopt" parameter disables diff optimization */
if( PD("noopt",0)!=0 ) diffFlags |= DIFF_NOOPT;
}
return diffFlags;
}
/*
** WEBPAGE: vinfo
** WEBPAGE: ci
** URL: /ci?name=RID|ARTIFACTID
**
** Display information about a particular check-in.
**
** We also jump here from /info if the name is a version.
**
** If the /ci page is used (instead of /vinfo or /info) then the
** default behavior is to show unified diffs of all file changes.
** With /vinfo and /info, only a list of the changed files are
** shown, without diffs. This behavior is inverted if the
** "show-version-diffs" setting is turned on.
*/
void ci_page(void){
Stmt q;
int rid;
int isLeaf;
int showDiff; /* True to show diffs */
int sideBySide; /* True for side-by-side diffs */
u64 diffFlags; /* Flag parameter for text_diff() */
const char *zName; /* Name of the checkin to be displayed */
const char *zUuid; /* UUID of zName */
const char *zParent; /* UUID of the parent checkin (if any) */
const char *zRe; /* regex parameter */
ReCompiled *pRe = 0; /* regex */
login_check_credentials();
if( !g.perm.Read ){ login_needed(); return; }
zName = P("name");
rid = name_to_rid_www("name");
if( rid==0 ){
style_header("Check-in Information Error");
@ No such object: %h(g.argv[2])
style_footer();
return;
}
zRe = P("regex");
if( zRe ) 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 = is_a_leaf(rid);
db_prepare(&q,
"SELECT uuid, datetime(mtime, 'localtime'), user, comment,"
" datetime(omtime, 'localtime'), mtime"
" FROM blob, event"
" WHERE blob.rid=%d"
" AND event.objid=%d",
rid, rid
);
sideBySide = atoi(PD("sbs","1"));
if( db_step(&q)==SQLITE_ROW ){
const char *zUuid = db_column_text(&q, 0);
char *zTitle = mprintf("Check-in [%.10s]", zUuid);
char *zEUser, *zEComment;
const char *zUser;
const char *zComment;
const char *zDate;
const char *zOrigDate;
style_header(zTitle);
login_anonymous_available();
free(zTitle);
zEUser = db_text(0,
"SELECT value FROM tagxref WHERE tagid=%d AND rid=%d",
TAG_USER, rid);
zEComment = db_text(0,
"SELECT value FROM tagxref WHERE tagid=%d AND rid=%d",
TAG_COMMENT, rid);
zUser = db_column_text(&q, 2);
zComment = db_column_text(&q, 3);
zDate = db_column_text(&q,1);
zOrigDate = db_column_text(&q, 4);
@
style_footer();
}
/*
** Look for "ci" and "filename" query parameters. If found, try to
** use them to extract the record ID of an artifact for the file.
*/
int artifact_from_ci_and_filename(void){
const char *zFilename;
const char *zCI;
int cirid;
Manifest *pManifest;
ManifestFile *pFile;
zCI = P("ci");
if( zCI==0 ) return 0;
zFilename = P("filename");
if( zFilename==0 ) return 0;
cirid = name_to_rid_www("ci");
pManifest = manifest_get(cirid, CFTYPE_MANIFEST);
if( pManifest==0 ) return 0;
manifest_file_rewind(pManifest);
while( (pFile = manifest_file_next(pManifest,0))!=0 ){
if( fossil_strcmp(zFilename, pFile->zName)==0 ){
int rid = db_int(0, "SELECT rid FROM blob WHERE uuid=%Q", pFile->zUuid);
manifest_destroy(pManifest);
return rid;
}
}
return 0;
}
/*
** The "z" argument is a string that contains the text of a source code
** file. This routine appends that text to the HTTP reply with line numbering.
**
** 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.
*/
void output_text_with_line_numbers(
const char *z,
const char *zLn
){
int iStart, iEnd; /* Start and end of region to highlight */
int n = 0; /* Current line number */
int i; /* Loop index */
int iTop = 0; /* Scroll so that this line is on top of screen. */
iStart = iEnd = atoi(zLn);
if( iStart>0 ){
for(i=0; fossil_isdigit(zLn[i]); i++){}
if( zLn[i]==',' || zLn[i]=='-' || zLn[i]=='.' ){
i++;
while( zLn[i]=='.' ){ i++; }
iEnd = atoi(&zLn[i]);
}
if( iEndiStart - 2 ) iTop = iStart-2;
}
@
ticket_output_change_artifact(pTktChng, 0);
manifest_destroy(pTktChng);
style_footer();
}
/*
** WEBPAGE: info
** URL: info/ARTIFACTID
**
** The argument is a artifact ID which might be a baseline or a file or
** a ticket changes or a wiki edit or something else.
**
** Figure out what the artifact ID is and jump to it.
*/
void info_page(void){
const char *zName;
Blob uuid;
int rid;
int rc;
zName = P("name");
if( zName==0 ) fossil_redirect_home();
if( validate16(zName, strlen(zName)) ){
if( db_exists("SELECT 1 FROM ticket WHERE tkt_uuid GLOB '%q*'", zName) ){
tktview_page();
return;
}
if( db_exists("SELECT 1 FROM tag WHERE tagname GLOB 'event-%q*'", zName) ){
event_page();
return;
}
}
blob_set(&uuid, zName);
rc = name_to_uuid(&uuid, -1, "*");
if( rc==1 ){
style_header("No Such Object");
@
@
@
db_prepare(&q,
"SELECT tag.tagid, tagname 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);
char zLabel[30];
sqlite3_snprintf(sizeof(zLabel), zLabel, "c%d", tagid);
@
}else{
@ Cancel special tag %h(zTagName)
}
}
db_finalize(&q);
@
@
Branching:
@
@
@
@
if( is_a_leaf(rid)
&& !db_exists("SELECT 1 FROM tagxref "
" WHERE tagid=%d AND rid=%d AND tagtype>0",
TAG_CLOSED, rid)
){
@