diff options
Diffstat (limited to 'tools')
115 files changed, 31457 insertions, 0 deletions
diff --git a/tools/co-rsync.tcl b/tools/co-rsync.tcl new file mode 100755 index 0000000..1193c50 --- /dev/null +++ b/tools/co-rsync.tcl @@ -0,0 +1,72 @@ +#!/bin/sh +# +# This is a TCL script that tries to sync the changes in a local +# Fossil checkout to another machine. The changes are gathered into +# a tarball, then sent via ssh to the remote and unpacked. +# +# Usage: +# +# co-rsync.tcl REMOTE +# +# Where REMOTE is the root of the remote repository into which changes +# are to be moved. +# +# Use Case: +# +# Sometimes while in the middle of an edit it is useful to transfer +# the incomplete changes to another machine for testing. This could +# be accomplished using scp, but doing it that was is tedious if many +# files in multiple directories have changed. This command does all +# the necessary transfer using a single command. +# +# A Tcl comment, whose contents don't matter \ +exec tclsh "$0" "$@" + +# Begin by changing directories to the root of the check-out. +# +set remote {} +set dryrun 0 +proc usage {} { + puts stderr "Usage: $::argv0 REMOTE" + puts stderr "Options:" + puts stderr " --dryrun No-op but print what would have happened" + exit 1 +} +foreach arg $argv { + if {$arg=="--dryrun" || $arg=="--dry-run"} { + set dryrun 1 + continue + } + if {$remote!=""} { + usage + } + set remote $arg +} +if {$remote==""} usage + +set in [open {|fossil status} rb] +set status [read $in] +if {[catch {close $in} msg]} { + puts stderr $msg + exit 1 +} +set root {} +regexp {local-root: +([^\n]+)} $status all root +if {$root==""} { + puts stderr "not in a fossil check-out" + exit 1 +} +cd $root +set tmpname filelist- +for {set i 0} {$i<3} {incr i} { + append tmpname [format %08x [expr {int(rand()*0xffffffff)}]] +} +set out [open $tmpname wb] +puts $out [exec fossil changes --no-classify --no-merge] +close $out +set cmd "rsync -v --files-from=$tmpname . $remote" +puts $cmd +if {!$dryrun} { + exec {*}$cmd +} +file delete $tmpname diff --git a/tools/codecheck1.c b/tools/codecheck1.c new file mode 100644 index 0000000..7a43d6f --- /dev/null +++ b/tools/codecheck1.c @@ -0,0 +1,700 @@ +/* +** Copyright (c) 2014 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 program reads Fossil source code files and tries to verify that +** printf-style format strings are correct. +** +** This program implements a compile-time validation step on the Fossil +** source code. Running this program is entirely optional. Its role is +** similar to the -Wall compiler switch on gcc, or the scan-build utility +** of clang, or other static analyzers. The purpose is to try to identify +** problems in the source code at compile-time. The difference is that this +** static checker is specifically designed for the particular printf formatter +** implementation used by Fossil. +** +** Checks include: +** +** * Verify that vararg formatting routines like blob_printf() or +** db_multi_exec() have the correct number of arguments for their +** format string. +** +** * For routines designed to generate SQL or HTML or a URL or JSON, +** detect and warn about possible injection attacks. +*/ +#include <stdio.h> +#include <stdlib.h> +#include <ctype.h> +#include <string.h> +#include <assert.h> + +/* +** Debugging switch +*/ +static int eVerbose = 0; + +/* +** Malloc, aborting if it fails. +*/ +void *safe_malloc(int nByte){ + void *x = malloc(nByte); + if( x==0 ){ + fprintf(stderr, "failed to allocate %d bytes\n", nByte); + exit(1); + } + return x; +} +void *safe_realloc(void *pOld, int nByte){ + void *x = realloc(pOld, nByte); + if( x==0 ){ + fprintf(stderr, "failed to allocate %d bytes\n", nByte); + exit(1); + } + return x; +} + +/* +** Read the entire content of the file named zFilename into memory obtained +** from malloc(). Add a zero-terminator to the end. +** Return a pointer to that memory. +*/ +static char *read_file(const char *zFilename){ + FILE *in; + char *z; + int nByte; + int got; + in = fopen(zFilename, "rb"); + if( in==0 ){ + return 0; + } + fseek(in, 0, SEEK_END); + nByte = ftell(in); + fseek(in, 0, SEEK_SET); + z = safe_malloc( nByte+1 ); + got = fread(z, 1, nByte, in); + z[got] = 0; + fclose(in); + return z; +} + +/* +** When parsing the input file, the following token types are recognized. +*/ +#define TK_SPACE 1 /* Whitespace or comments */ +#define TK_ID 2 /* An identifier */ +#define TK_STR 3 /* A string literal in double-quotes */ +#define TK_OTHER 4 /* Any other token */ +#define TK_EOF 99 /* End of file */ + +/* +** Determine the length and type of the token beginning at z[0] +*/ +static int token_length(const char *z, int *pType, int *pLN){ + int i; + if( z[0]==0 ){ + *pType = TK_EOF; + return 0; + } + if( z[0]=='"' || z[0]=='\'' ){ + for(i=1; z[i] && z[i]!=z[0]; i++){ + if( z[i]=='\\' && z[i+1]!=0 ){ + if( z[i+1]=='\n' ) (*pLN)++; + i++; + } + } + if( z[i]!=0 ) i++; + *pType = z[0]=='"' ? TK_STR : TK_OTHER; + return i; + } + if( isalnum(z[0]) || z[0]=='_' ){ + for(i=1; isalnum(z[i]) || z[i]=='_'; i++){} + *pType = isalpha(z[0]) || z[0]=='_' ? TK_ID : TK_OTHER; + return i; + } + if( isspace(z[0]) ){ + if( z[0]=='\n' ) (*pLN)++; + for(i=1; isspace(z[i]); i++){ + if( z[i]=='\n' ) (*pLN)++; + } + *pType = TK_SPACE; + return i; + } + if( z[0]=='/' && z[1]=='*' ){ + for(i=2; z[i] && (z[i]!='*' || z[i+1]!='/'); i++){ + if( z[i]=='\n' ) (*pLN)++; + } + if( z[i] ) i += 2; + *pType = TK_SPACE; + return i; + } + if( z[0]=='/' && z[1]=='/' ){ + for(i=2; z[i] && z[i]!='\n'; i++){} + if( z[i] ){ + (*pLN)++; + i++; + } + *pType = TK_SPACE; + return i; + } + if( z[0]=='\\' && (z[1]=='\n' || (z[1]=='\r' && z[2]=='\n')) ){ + *pType = TK_SPACE; + return 1; + } + *pType = TK_OTHER; + return 1; +} + +/* +** Return the next non-whitespace token +*/ +const char *next_non_whitespace(const char *z, int *pLen, int *pType){ + int len; + int eType; + int ln = 0; + while( (len = token_length(z, &eType, &ln))>0 && eType==TK_SPACE ){ + z += len; + } + *pLen = len; + *pType = eType; + return z; +} + +/* +** Return index into z[] for the first balanced TK_OTHER token with +** value cValue. +*/ +static int distance_to(const char *z, char cVal){ + int len; + int dist = 0; + int eType; + int nNest = 0; + int ln = 0; + while( z[0] && (len = token_length(z, &eType, &ln))>0 ){ + if( eType==TK_OTHER ){ + if( z[0]==cVal && nNest==0 ){ + break; + }else if( z[0]=='(' ){ + nNest++; + }else if( z[0]==')' ){ + nNest--; + } + } + dist += len; + z += len; + } + return dist; +} + +/* +** Return the first non-whitespace characters in z[] +*/ +static const char *skip_space(const char *z){ + while( isspace(z[0]) ){ z++; } + return z; +} + +/* +** Remove excess whitespace and nested "()" from string z. +*/ +static char *simplify_expr(char *z){ + int n = (int)strlen(z); + while( n>0 ){ + if( isspace(z[0]) ){ + z++; + n--; + continue; + } + if( z[0]=='(' && z[n-1]==')' ){ + z++; + n -= 2; + continue; + } + break; + } + z[n] = 0; + return z; +} + +/* +** Return true if the input is a string literal. +*/ +static int is_string_lit(const char *z){ + int nu1, nu2; + z = next_non_whitespace(z, &nu1, &nu2); + if( strcmp(z, "NULL")==0 ) return 1; + return z[0]=='"'; +} + +/* +** Return true if the input is an expression of string literals: +** +** EXPR ? "..." : "..." +*/ +static int is_string_expr(const char *z){ + int len = 0, eType; + const char *zOrig = z; + len = distance_to(z, '?'); + if( z[len]==0 && skip_space(z)[0]=='(' ){ + z = skip_space(z) + 1; + len = distance_to(z, '?'); + } + z += len; + if( z[0]=='?' ){ + z++; + z = next_non_whitespace(z, &len, &eType); + if( eType==TK_STR ){ + z += len; + z = next_non_whitespace(z, &len, &eType); + if( eType==TK_OTHER && z[0]==':' ){ + z += len; + z = next_non_whitespace(z, &len, &eType); + if( eType==TK_STR ){ + z += len; + z = next_non_whitespace(z, &len, &eType); + if( eType==TK_EOF ) return 1; + if( eType==TK_OTHER && z[0]==')' && skip_space(zOrig)[0]=='(' ){ + z += len; + z = next_non_whitespace(z, &len, &eType); + if( eType==TK_EOF ) return 1; + } + } + } + } + } + return 0; +} + +/* +** A list of functions that return strings that are safe to insert into +** SQL using %s. +*/ +static const char *azSafeFunc[] = { + "filename_collation", + "leaf_is_closed_sql", + "timeline_query_for_www", + "timeline_query_for_tty", + "blob_sql_text", + "glob_expr", + "fossil_all_reserved_names", + "configure_inop_rhs", + "db_setting_inop_rhs", +}; + +/* +** Return true if the input is an argument that is safe to use with %s +** while building an SQL statement. +*/ +static int is_sql_safe(const char *z){ + int len, eType; + int i; + + /* A string literal is safe for use with %s */ + if( is_string_lit(z) ) return 1; + + /* Certain functions are guaranteed to return a string that is safe + ** for use with %s */ + z = next_non_whitespace(z, &len, &eType); + for(i=0; i<sizeof(azSafeFunc)/sizeof(azSafeFunc[0]); i++){ + if( eType==TK_ID + && strncmp(z, azSafeFunc[i], len)==0 + && strlen(azSafeFunc[i])==len + ){ + return 1; + } + } + + /* Expressions of the form: EXPR ? "..." : "...." can count as + ** a string literal. */ + if( is_string_expr(z) ) return 1; + + /* If the "safe-for-%s" comment appears in the argument, then + ** let it through */ + if( strstr(z, "/*safe-for-%s*/")!=0 ) return 1; + + return 0; +} + +/* +** Return true if the input is an argument that is never safe for use +** with %s. +*/ +static int never_safe(const char *z){ + if( strstr(z,"/*safe-for-%s*/")!=0 ) return 0; + if( z[0]=='P' ){ + if( strncmp(z,"PIF(",4)==0 ) return 0; + if( strncmp(z,"PCK(",4)==0 ) return 0; + return 1; + } + if( strncmp(z,"cgi_param",9)==0 ) return 1; + return 0; +} + +/* +** Processing flags +*/ +#define FMT_SQL 0x00001 /* Generator for SQL text */ +#define FMT_HTML 0x00002 /* Generator for HTML text */ +#define FMT_URL 0x00004 /* Generator for URLs */ +#define FMT_JSON 0x00008 /* Generator for JSON */ +#define FMT_SAFE 0x00010 /* Generator for human-readable text */ +#define FMT_LIT 0x00020 /* Just verify that a string literal */ +#define FMT_PX 0x00040 /* Must have a literal prefix in format string */ + +/* +** A list of internal Fossil interfaces that take a printf-style format +** string. +*/ +struct FmtFunc { + const char *zFName; /* Name of the function */ + int iFmtArg; /* Index of format argument. Leftmost is 1. */ + unsigned fmtFlags; /* Processing flags */ +} aFmtFunc[] = { + { "admin_log", 1, FMT_SAFE }, + { "ajax_route_error", 2, FMT_SAFE }, + { "audit_append", 3, FMT_SAFE }, + { "backofficeTrace", 1, FMT_SAFE }, + { "backoffice_log", 1, FMT_SAFE }, + { "blob_append_sql", 2, FMT_SQL }, + { "blob_appendf", 2, FMT_SAFE }, + { "cgi_debug", 1, FMT_SAFE }, + { "cgi_panic", 1, FMT_SAFE }, + { "cgi_printf", 1, FMT_HTML }, + { "cgi_printf_header", 1, FMT_HTML }, + { "cgi_redirectf", 1, FMT_URL }, + { "chref", 2, FMT_URL }, + { "CX", 1, FMT_HTML }, + { "db_blob", 2, FMT_SQL }, + { "db_debug", 1, FMT_SQL }, + { "db_double", 2, FMT_SQL }, + { "db_err", 1, FMT_SAFE }, + { "db_exists", 1, FMT_SQL }, + { "db_get_mprintf", 2, FMT_SAFE }, + { "db_int", 2, FMT_SQL }, + { "db_int64", 2, FMT_SQL }, + { "db_lset", 1, FMT_LIT }, + { "db_lset_int", 1, FMT_LIT }, + { "db_multi_exec", 1, FMT_SQL }, + { "db_optional_sql", 2, FMT_SQL }, + { "db_prepare", 2, FMT_SQL }, + { "db_prepare_ignore_error", 2, FMT_SQL }, + { "db_set", 1, FMT_LIT }, + { "db_set_int", 1, FMT_LIT }, + { "db_set_mprintf", 3, FMT_PX }, + { "db_static_prepare", 2, FMT_SQL }, + { "db_text", 2, FMT_SQL }, + { "db_unset", 1, FMT_LIT }, + { "db_unset_mprintf", 2, FMT_PX }, + { "emailerError", 2, FMT_SAFE }, + { "entry_attribute", 4, FMT_LIT }, + { "fileedit_ajax_error", 2, FMT_SAFE }, + { "form_begin", 2, FMT_URL }, + { "fossil_error", 2, FMT_SAFE }, + { "fossil_errorlog", 1, FMT_SAFE }, + { "fossil_fatal", 1, FMT_SAFE }, + { "fossil_fatal_recursive", 1, FMT_SAFE }, + { "fossil_panic", 1, FMT_SAFE }, + { "fossil_print", 1, FMT_SAFE }, + { "fossil_trace", 1, FMT_SAFE }, + { "fossil_warning", 1, FMT_SAFE }, + { "gitmirror_message", 2, FMT_SAFE }, + { "href", 1, FMT_URL }, + { "json_new_string_f", 1, FMT_SAFE }, + { "json_set_err", 2, FMT_SAFE }, + { "json_warn", 2, FMT_SAFE }, + { "mprintf", 1, FMT_SAFE }, + { "multiple_choice_attribute", 3, FMT_LIT }, + { "onoff_attribute", 3, FMT_LIT }, + { "pop3_print", 2, FMT_SAFE }, + { "smtp_send_line", 2, FMT_SAFE }, + { "smtp_server_send", 2, FMT_SAFE }, + { "socket_set_errmsg", 1, FMT_SAFE }, + { "ssl_set_errmsg", 1, FMT_SAFE }, + { "style_copy_button", 6, FMT_SAFE }, + { "style_header", 1, FMT_HTML }, + { "style_set_current_page", 1, FMT_URL }, + { "style_submenu_element", 2, FMT_URL }, + { "style_submenu_sql", 3, FMT_SQL }, + { "textarea_attribute", 5, FMT_LIT }, + { "tktsetup_generic", 1, FMT_LIT }, + { "webpage_error", 1, FMT_SAFE }, + { "webpage_notfound_error", 1, FMT_SAFE }, + { "xfersetup_generic", 1, FMT_LIT }, + { "xhref", 2, FMT_URL }, +}; + +/* +** Comparison function for two FmtFunc entries +*/ +static int fmtfunc_cmp(const void *pAA, const void *pBB){ + const struct FmtFunc *pA = (const struct FmtFunc*)pAA; + const struct FmtFunc *pB = (const struct FmtFunc*)pBB; + return strcmp(pA->zFName, pB->zFName); +} + +/* +** Determine if the identifier zIdent of length nIndent is a Fossil +** internal interface that uses a printf-style argument. Return zero if not. +** Return the index of the format string if true with the left-most +** argument having an index of 1. +*/ +static int isFormatFunc(const char *zIdent, int nIdent, unsigned *pFlags){ + int upr, lwr; + lwr = 0; + upr = sizeof(aFmtFunc)/sizeof(aFmtFunc[0]) - 1; + while( lwr<=upr ){ + unsigned x = (lwr + upr)/2; + int c = strncmp(zIdent, aFmtFunc[x].zFName, nIdent); + if( c==0 ){ + if( aFmtFunc[x].zFName[nIdent]==0 ){ + *pFlags = aFmtFunc[x].fmtFlags; + return aFmtFunc[x].iFmtArg; + } + c = -1; + } + if( c<0 ){ + upr = x - 1; + }else{ + lwr = x + 1; + } + } + *pFlags = 0; + return 0; +} + +/* +** Return the expected number of arguments for the format string. +** Return -1 if the value cannot be computed. +** +** For each argument less than nType, store the conversion character +** for that argument in cType[i]. +** +** Store the number of initial literal characters of the format string +** in *pInit. +*/ +static int formatArgCount(const char *z, int nType, char *cType, int *pInit){ + int nArg = 0; + int i, k; + int len; + int eType; + int ln = 0; + *pInit = 0; + while( z[0] ){ + len = token_length(z, &eType, &ln); + if( eType==TK_STR ){ + for(i=1; i<len-1 && isalpha(z[i]); i++){} + *pInit = i-1; + for(i=1; i<len-1; i++){ + if( z[i]!='%' ) continue; + if( z[i+1]=='%' ){ i++; continue; } + for(k=i+1; k<len && !isalpha(z[k]); k++){ + if( z[k]=='*' || z[k]=='#' ){ + if( nArg<nType ) cType[nArg] = z[k]; + nArg++; + } + } + if( z[k]!='R' ){ + if( nArg<nType ) cType[nArg] = z[k]; + nArg++; + } + } + } + z += len; + } + return nArg; +} + +/* +** The function call that begins at zFCall[0] (which is on line lnFCall of the +** original file) is a function that uses a printf-style format string +** on argument number fmtArg. It has processings flags fmtFlags. Do +** compile-time checking on this function, output any errors, and return +** the number of errors. +*/ +static int checkFormatFunc( + const char *zFilename, /* Name of the file being processed */ + const char *zFCall, /* Pointer to start of function call */ + int lnFCall, /* Line number that holds z[0] */ + int fmtArg, /* Format string should be this argument */ + int fmtFlags /* Extra processing flags */ +){ + int szFName; + int eToken; + int ln = lnFCall; + int len; + const char *zStart; + char *z; + char *zCopy; + int nArg = 0; + const char **azArg = 0; + int i, k; + int nErr = 0; + char *acType; + int nInit = 0; + + szFName = token_length(zFCall, &eToken, &ln); + zStart = next_non_whitespace(zFCall+szFName, &len, &eToken); + assert( zStart[0]=='(' && len==1 ); + len = distance_to(zStart+1, ')'); + zCopy = safe_malloc( len + 1 ); + memcpy(zCopy, zStart+1, len); + zCopy[len] = 0; + azArg = 0; + nArg = 0; + z = zCopy; + while( z[0] ){ + char cEnd; + len = distance_to(z, ','); + cEnd = z[len]; + z[len] = 0; + azArg = safe_realloc((char*)azArg, (sizeof(azArg[0])+1)*(nArg+1)); + azArg[nArg++] = simplify_expr(z); + if( cEnd==0 ) break; + z += len + 1; + } + acType = (char*)&azArg[nArg]; + if( fmtArg>nArg ){ + printf("%s:%d: too few arguments to %.*s()\n", + zFilename, lnFCall, szFName, zFCall); + nErr++; + }else{ + const char *zFmt = azArg[fmtArg-1]; + const char *zOverride = strstr(zFmt, "/*works-like:"); + if( zOverride ) zFmt = zOverride + sizeof("/*works-like:")-1; + if( fmtFlags & FMT_LIT ){ + if( !is_string_lit(zFmt) ){ + printf("%s:%d: argument %d to %.*s() should be a string literal\n", + zFilename, lnFCall, fmtArg, szFName, zFCall); + nErr++; + } + }else if( !is_string_lit(zFmt) ){ + printf("%s:%d: %.*s() has non-constant format on arg[%d]\n", + zFilename, lnFCall, szFName, zFCall, fmtArg-1); + nErr++; + }else if( (k = formatArgCount(zFmt, nArg, acType, &nInit))>=0 + && nArg!=fmtArg+k ){ + printf("%s:%d: too %s arguments to %.*s() " + "- got %d and expected %d\n", + zFilename, lnFCall, (nArg<fmtArg+k ? "few" : "many"), + szFName, zFCall, nArg, fmtArg+k); + nErr++; + }else if( (fmtFlags & FMT_PX)!=0 ){ + if( nInit==0 ){ + printf("%s:%d: format string on %.*s() should have" + " an ASCII character prefix\n", + zFilename, lnFCall, szFName, zFCall); + nErr++; + } + }else if( (fmtFlags & FMT_SAFE)==0 ){ + for(i=0; i<nArg && i<k; i++){ + if( (acType[i]=='s' || acType[i]=='z' || acType[i]=='b') ){ + const char *zExpr = azArg[fmtArg+i]; + if( never_safe(zExpr) ){ + printf("%s:%d: Argument %d to %.*s() is not safe for" + " a query parameter\n", + zFilename, lnFCall, i+fmtArg, szFName, zFCall); + nErr++; + + }else if( (fmtFlags & FMT_SQL)!=0 && !is_sql_safe(zExpr) ){ + printf("%s:%d: Argument %d to %.*s() not safe for SQL\n", + zFilename, lnFCall, i+fmtArg, szFName, zFCall); + nErr++; + } + } + } + } + } + if( nErr ){ + for(i=0; i<nArg; i++){ + printf(" arg[%d]: %s\n", i, azArg[i]); + } + }else if( eVerbose>1 ){ + printf("%s:%d: %.*s() ok for %d arguments\n", + zFilename, lnFCall, szFName, zFCall, nArg); + } + free((char*)azArg); + free(zCopy); + return nErr; +} + + +/* +** Do a design-rule check of format strings for the file named zName +** with content zContent. Write errors on standard output. Return +** the number of errors. +*/ +static int scan_file(const char *zName, const char *zContent){ + const char *z; + int ln = 0; + int szToken; + int eToken; + const char *zPrev = 0; + int ePrev = 0; + int szPrev = 0; + int lnPrev = 0; + int nCurly = 0; + int x; + unsigned fmtFlags = 0; + int nErr = 0; + + if( zContent==0 ){ + printf("cannot read file: %s\n", zName); + return 1; + } + for(z=zContent; z[0]; z += szToken){ + szToken = token_length(z, &eToken, &ln); + if( eToken==TK_SPACE ) continue; + if( eToken==TK_OTHER ){ + if( z[0]=='{' ){ + nCurly++; + }else if( z[0]=='}' ){ + nCurly--; + }else if( nCurly>0 && z[0]=='(' && ePrev==TK_ID + && (x = isFormatFunc(zPrev,szPrev,&fmtFlags))>0 ){ + nErr += checkFormatFunc(zName, zPrev, lnPrev, x, fmtFlags); + } + } + zPrev = z; + ePrev = eToken; + szPrev = szToken; + lnPrev = ln; + } + return nErr; +} + +/* +** Check for format-string design rule violations on all files listed +** on the command-line. +** +** The eVerbose global variable is incremented with each "-v" argument. +*/ +int main(int argc, char **argv){ + int i; + int nErr = 0; + qsort(aFmtFunc, sizeof(aFmtFunc)/sizeof(aFmtFunc[0]), + sizeof(aFmtFunc[0]), fmtfunc_cmp); + for(i=1; i<argc; i++){ + char *zFile; + if( strcmp(argv[i],"-v")==0 ){ + eVerbose++; + continue; + } + if( eVerbose>0 ) printf("Processing %s...\n", argv[i]); + zFile = read_file(argv[i]); + nErr += scan_file(argv[i], zFile); + free(zFile); + } + return nErr; +} diff --git a/tools/copland/dist/copland-skin.config b/tools/copland/dist/copland-skin.config new file mode 100644 index 0000000..02392e8 --- /dev/null +++ b/tools/copland/dist/copland-skin.config @@ -0,0 +1,827 @@ +# Copland Fossil skin configuration +# Generated deterministically by tools/copland/scripts/build-config.mjs. +# Import with: fossil configuration import dist/copland-skin.config -R REPOSITORY +config /config 12869 +1 'css' value ':root { + color-scheme: light dark; + --page-bg: #fff; + --surface: #fff; + --surface-subtle: #f5f5f5; + --surface-muted: #e9ecef; + --surface-active: #ddd; + --text: #212529; + --text-muted: #6c757d; + --text-faint: #767676; + --border: #ced4da; + --border-strong: #888; + --link: #0645d9; + --link-hover: #002f9f; + --primary: #0056d6; + --primary-hover: #0044aa; + --primary-text: #fff; + --danger: #b00020; + --danger-bg: #f8d7da; + --success: #146c2e; + --success-bg: #d4edda; + --warning-bg: #fff3cd; + --info-bg: #d1ecf1; + --code-bg: #f1f3f5; + --selection: #b9d6ff; + --focus: #ffbf47; + --shadow: rgba(0, 0, 0, 0.12); + --container: 71.25rem; + --gutter: 1rem; + --radius: 0; + --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; +} + +@media (prefers-color-scheme: dark) { + :root { + --page-bg: #212529; + --surface: #212529; + --surface-subtle: #191c1f; + --surface-muted: #2d3237; + --surface-active: #3b4147; + --text: #f1f3f5; + --text-muted: #c2c7cc; + --text-faint: #adb5bd; + --border: #565e65; + --border-strong: #8b949c; + --link: #5c9cff; + --link-hover: #91bcff; + --primary: #4d8ee8; + --primary-hover: #70a7f2; + --primary-text: #101214; + --danger: #ff6b78; + --danger-bg: #57242a; + --success: #65d47d; + --success-bg: #183d24; + --warning-bg: #514318; + --info-bg: #193e47; + --code-bg: #17191b; + --selection: #173f70; + --focus: #ffd166; + --shadow: rgba(0, 0, 0, 0.4); + } +} +* { + box-sizing: border-box; +} + +html { + min-height: 100%; + scrollbar-gutter: stable; + font-size: 16px; +} + +body { + min-height: 100vh; + margin: 0; + padding: 0 0 1rem; + background: var(--page-bg); + color: var(--text); + font-family: var(--font-sans); + font-size: 1rem; + line-height: 1.5; + text-align: initial; +} + +::selection { + background: var(--selection); +} + +.container, +div.content { + width: 100%; + max-width: var(--container); + margin-inline: auto; + padding-inline: var(--gutter); +} + +div.content { + min-height: 45vh; +} + +body a { + color: var(--link); + text-decoration: underline; + text-underline-offset: 0.12em; +} + +body a:hover { + color: var(--link-hover); + text-decoration: none; +} + +a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible, +textarea:focus-visible, [tabindex]:focus-visible { + outline: 3px solid var(--focus); + outline-offset: 2px; +} + +h1, h2, h3, h4, h5, h6 { + margin: 1rem 0 0.5rem; + color: var(--text); + font-weight: 500; + line-height: 1.2; +} + +h1 { + font-size: 1.75rem; +} + +h2 { + font-size: 1.45rem; +} + +h3 { + font-size: 1.15rem; + border-bottom: 1px solid var(--border); +} + +h4 { + font-size: 1rem; + border-bottom: 1px solid var(--border); +} + +p { + margin: 0 0 1rem; +} + +hr { + border: 0; + border-top: 1px solid var(--border); +} + +.site-header { + margin-bottom: 0; +} + +.site-header-row { + min-height: 3.25rem; + display: flex; + align-items: center; + gap: 1rem; +} + +.site-brand, +.resource-title a, +.account-nav a, +.mainmenu a { + text-decoration: none; +} + +.site-brand { + flex: 0 0 auto; + color: var(--text); + font-size: 1.25rem; + font-weight: 600; + letter-spacing: -0.025em; +} + +.site-brand::before { + content: ""; + display: inline-block; + width: 0.78rem; + height: 0.78rem; + margin-right: 0.42rem; + border: 2px solid currentColor; + border-radius: 50%; + vertical-align: 0.02rem; +} + +.site-brand:hover, +.resource-title a:hover, +.account-nav a:hover, +.mainmenu a:hover { + color: var(--text); + text-decoration: underline; +} + +.resource-title { + min-width: 0; + display: flex; + align-items: baseline; + gap: 0.4rem; + overflow: hidden; + white-space: nowrap; +} + +.resource-title a, +.resource-page { + overflow: hidden; + text-overflow: ellipsis; +} + +.resource-title a { + color: var(--text); + font-weight: 600; +} + +.resource-separator, .resource-page { + color: var(--text-muted); +} + +.account-nav { + margin-left: auto; + white-space: nowrap; + font-size: 0.9rem; +} + +.resource-nav { + border-bottom: 3px solid var(--surface-active); +} + +.mainmenu { + min-height: 2rem; + display: flex; + align-items: end; + gap: 0; + overflow-x: auto; + scrollbar-width: thin; +} + +.mainmenu a { + flex: 0 0 auto; + padding: 0.25rem 0.85rem 0.18rem; + color: var(--text-muted); + white-space: nowrap; +} + +.mainmenu a.active, +.mainmenu a.selected, +.mainmenu a:hover { + color: var(--text); +} + +.mainmenu a.active, +.mainmenu a.selected { + background: var(--surface-active); +} + +.page-title { + margin-top: 0.9rem; + margin-bottom: 0.75rem; + font-size: 1.35rem; + font-weight: 500; +} + +.site-footer { + margin-top: 2rem; + color: var(--text-muted); + font-size: 0.8rem; +} + +.site-footer .container { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.4rem; +} + +pre, code, kbd, samp, tt { + font-family: var(--font-mono); +} + +code, tt { + overflow-wrap: anywhere; +} + +pre { + max-width: 100%; + overflow: auto; + margin: 0 0 1rem; + padding: 0.75rem 1rem; + background: var(--code-bg); + color: var(--text); + font-size: 0.875rem; + line-height: 1.35; + tab-size: 4; +} + +pre a { + color: inherit; +} + +blockquote { + margin: 0 0 1rem; + padding-left: 1rem; + border-left: 4px solid var(--border); +} + +.org-document { + max-width: 100%; +} + +.org-document .org-title { + margin-top: 0; +} + +.org-metadata { + display: grid; + grid-template-columns: max-content minmax(0, 1fr); + gap: 0.15rem 0.75rem; + margin: 0 0 1rem; + color: var(--text-muted); + font-size: 0.92rem; +} + +.org-metadata dt { + font-weight: 600; +} + +.org-metadata dd { + margin: 0; +} + +.org-toc { + margin: 0 0 1.25rem; + padding: 0.75rem 1rem; + border: 1px solid var(--border); + background: var(--surface-subtle); +} + +.org-toc h2 { + margin-top: 0; + font-size: 1.05rem; +} + +.org-toc ol { + margin: 0; + padding-left: 1.5rem; +} + +.org-toc-level-2 { + margin-left: 1rem; +} + +.org-toc-level-3 { + margin-left: 2rem; +} + +.org-toc-level-4, .org-toc-level-5, .org-toc-level-6 { + margin-left: 3rem; +} + +.org-table { + display: block; + overflow-x: auto; +} + +.org-block { + border-left: 3px solid var(--border-strong); +} + +.org-example { + color: var(--text); +} + +.org-unknown-block { + border-left-color: var(--danger); +} + +table { + width: 100%; + margin-bottom: 1rem; + border-collapse: collapse; + color: var(--text); +} + +th { + text-align: left; + font-weight: 600; +} + +th, td { + padding: 0.28rem 0.5rem; + vertical-align: top; + border-top: 1px solid var(--border); +} + +thead th { + border-bottom: 2px solid var(--border-strong); +} + +tbody tr:nth-child(odd) { + background: color-mix(in srgb, var(--surface-subtle) 70%, transparent); +} + +table.browser, table.report, table.timelineTable { + font-size: 0.92rem; +} + +table.browser td:first-child, table.browser td:nth-child(2) { + font-family: var(--font-mono); +} + +input, button, select, textarea { + font: inherit; +} + +input[type=text], input[type=search], input[type=email], input[type=password], +input[type=url], input[type=number], textarea, select { + max-width: 100%; + padding: 0.35rem 0.5rem; + border: 1px solid var(--border-strong); + border-radius: var(--radius); + background: var(--surface); + color: var(--text); +} + +textarea { + width: 100%; +} + +label { + display: inline-block; + margin-bottom: 0.25rem; +} + +button, input[type=submit], input[type=button], .button, a.button { + display: inline-block; + padding: 0.35rem 0.7rem; + border: 1px solid var(--primary); + border-radius: var(--radius); + background: var(--primary); + color: var(--primary-text); + cursor: pointer; + line-height: 1.25; + text-decoration: none; +} + +button:hover, input[type=submit]:hover, input[type=button]:hover, .button:hover { + border-color: var(--primary-hover); + background: var(--primary-hover); +} + +button:disabled, input:disabled { + cursor: not-allowed; + opacity: 0.65; +} + +fieldset { + margin: 0 0 1rem; + border: 1px solid var(--border); +} + +legend { + padding-inline: 0.35rem; +} + +.submenu, .sectionmenu { + margin: 0 0 1rem; + padding: 0.4rem 0.6rem; + background: var(--surface-subtle); + border: 1px solid var(--border); +} + +.submenu a, .sectionmenu a { + margin-right: 0.75rem; +} + +.generalError, .error, .fatalError, .loginError, +.warning, .notice, .success { + margin: 0.75rem 0; + padding: 0.65rem 0.8rem; + border: 1px solid var(--border); +} + +.generalError, .error, .fatalError, .loginError { + background: var(--danger-bg); + color: var(--text); +} + +.warning { + background: var(--warning-bg); +} + +.notice { + background: var(--info-bg); +} + +.success { + background: var(--success-bg); +} + +.timelineDate { + margin-top: 1rem; + color: var(--text); + font-weight: 600; +} + +.timelineHistDsp { + color: var(--text-muted); +} + +.timelineModernCell, .timelineColumnarCell, .timelineDetailCell { + padding-block: 0.35rem; +} + +.timelineModernDetail { + opacity: 1; +} + +.timelineCompactComment, .timelineComment { + line-height: 1.35; +} + +.timelineTime { + color: var(--text-muted); + white-space: nowrap; +} + +.timelineGraph { + min-width: 2rem; +} + +.tl-node { + border-radius: 50%; +} + +.timelineModernCell a, +.timelineColumnarCell a, +.timelineDetailCell a { + color: var(--link) !important; +} + +.timelineModernCell a:hover, +.timelineColumnarCell a:hover, +.timelineDetailCell a:hover { + color: var(--link-hover) !important; +} + +.filetree { + font-family: var(--font-mono); + font-size: 0.9rem; +} + +.filetree ul { + padding-left: 1.25rem; +} + +.filetree a { + color: var(--text); +} + +.filetree a:hover { + color: var(--link); +} + +.fileage tr:hover, table.browser tr:hover { + background: var(--surface-active); +} + +.diff, .udiff, .sbsdiff { + font-family: var(--font-mono); + font-size: 0.82rem; +} + +.diff pre, .udiff pre, .sbsdiff pre { + background: transparent; +} + +.diffln, .diffhr { + color: var(--text-muted); + background: var(--surface-muted); +} + +.diffadd, .diffins { + background: var(--success-bg); +} + +.diffrm, .diffdel { + background: var(--danger-bg); +} + +.diffchng { + background: var(--warning-bg); +} + +.selectedText { + background: var(--selection); +} + +.forumPost, .forumTime, .forumSel, .forumHierRoot, .forumHier { + border-color: var(--border); +} + +.forumPost, .forumTime { + background: var(--surface-subtle); + color: var(--text); +} + +.forumPostBody { + overflow-wrap: anywhere; +} + +.forumPostBody img, .wiki-content img, .markdown img { + max-width: 100%; + height: auto; +} + +.ticketDspLabel, .ticketDspValue { + padding: 0.35rem 0.5rem; +} + +.ticketDspLabel { + color: var(--text-muted); + font-weight: 600; +} + +.ticketDspValue { + border-bottom: 1px solid var(--border); +} + +.branchname, .tagname, .wikiTagCancelled, .infoTagCancelled { + display: inline-block; + padding: 0.05rem 0.3rem; + background: var(--surface-muted); + color: var(--text); + font-size: 0.82rem; +} + +.statistics-report-table-events th, .statistics-report-table-events td { + text-align: right; +} + +.statistics-report-table-events th:first-child, .statistics-report-table-events td:first-child { + text-align: left; +} + +.accordion_panel { + border-color: var(--border); + background: var(--surface); +} + +.accordion_marker { + color: var(--text-muted); +} + +body.repolist .resource-title, +body.repolist .resource-nav, +body.repolist > .page-title { + display: none; +} + +body.repolist div.content { + padding-top: 1rem; +} + +body.repolist table td:first-child { + font-family: var(--font-mono); + font-weight: 600; +} + +@media (prefers-color-scheme: dark) { + img { + opacity: 0.92; + } + body .timelineModernCell, + body .timelineColumnarCell, + body .timelineDetailCell, + .timelineModernCell[style*=background-color], + .timelineColumnarCell[style*=background-color], + .timelineDetailCell[style*=background-color], + td[style*=background-color] { + background-color: var(--surface-subtle) !important; + color: var(--text) !important; + } + .diffadd, .diffins { + color: var(--text); + } + .diffrm, .diffdel { + color: var(--text); + } +} +@media (max-width: 48rem) { + :root { + --gutter: 0.75rem; + } + .site-header-row { + min-height: 2.8rem; + gap: 0.65rem; + } + .resource-page, .resource-separator { + display: none; + } + .mainmenu a { + padding-inline: 0.65rem; + } + .page-title { + font-size: 1.2rem; + } + table:not(.sbsdiffcols) { + display: block; + max-width: 100%; + overflow-x: auto; + } + .desktoponly { + display: none !important; + } +} +@media (max-width: 36rem) { + .site-brand { + font-size: 1.1rem; + } + .resource-title { + font-size: 0.9rem; + } + .account-nav { + font-size: 0.82rem; + } + .page-title { + margin-top: 0.7rem; + } + pre { + padding: 0.6rem 0.7rem; + } +} +@media print { + .resource-nav, .account-nav, .submenu, .sectionmenu, .site-footer { + display: none !important; + } + body, pre, code { + background: #fff !important; + color: #000 !important; + } + a { + color: #000; + text-decoration: underline; + } + div.content, .container { + max-width: none; + padding: 0; + } +} +' +config /config 1631 +1 'header' value '<header class="site-header"> + <div class="container site-header-row"> + <a class="site-brand" href="$home$index_page">Copland</a> + <div class="resource-title"> + <a href="$home$index_page">$<project_name></a> + <span class="resource-separator" aria-hidden="true">/</span> + <span class="resource-page">$<title></span> + </div> + <div class="account-nav"> + <th1> + if {[info exists login]} { + html "<a href=''$home/login''>$login</a>" + } else { + html "<a href=''$home/login''>Log in</a>" + } + </th1> + </div> + </div> + <div class="resource-nav"> + <nav class="container mainmenu" aria-label="Repository"> + <th1> + foreach {name url expr class} $mainmenu { + if {![capexpr $expr]} continue + if {[string match /chat* $url] || [string match chat* $url] + || [string match /taglist* $url] || [string match taglist* $url] + || [string match /forum* $url] || [string match forum* $url] + || [string match /reportlist* $url] || [string match reportlist* $url] + || [string match /ticket* $url] || [string match ticket* $url] + || [string match /tkt* $url] || [string match tkt* $url]} continue + if {[string match /* $url]} { + if {[string match $url\[/?#\]* /$current_page/]} { + set class "active $class" + } + set url $home$url + } + html "<a href=''$url'' class=''$class''>$name</a>\n" + } + </th1> + </nav> + </div> +</header> +<h1 class="container page-title">$<title></h1> +' +config /config 351 +1 'footer' value '<footer class="site-footer"> + <div class="container"> + <span>Copland</span> + <span aria-hidden="true">·</span> + <span>Powered by Fossil $release_version</span> + <span aria-hidden="true">·</span> + <span>Generated in <th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s</span> + </div> +</footer> + +' +config /config 271 +1 'details' value 'pikchr-background: "" +pikchr-fontscale: "0.9" +pikchr-foreground: "" +pikchr-scale: "1.1" +timeline-arrowheads: 1 +timeline-circle-nodes: 1 +timeline-color-graph-lines: 1 +white-foreground: 0 + +' +config /config 16 +1 'js' value ' +' diff --git a/tools/copland/package-lock.json b/tools/copland/package-lock.json new file mode 100644 index 0000000..6388ad6 --- /dev/null +++ b/tools/copland/package-lock.json @@ -0,0 +1,549 @@ +{ + "name": "copland-fossil-skin", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "copland-fossil-skin", + "version": "0.1.0", + "devDependencies": { + "@axe-core/playwright": "4.13.0", + "@playwright/test": "1.61.1", + "sass": "1.103.0" + } + }, + "node_modules/@axe-core/playwright": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.13.0.tgz", + "integrity": "sha512-6YLx+kxXu5GJceG4ozFg+33a2EMTdjYwWGloJ3sb9Kta5pp+ZNS53uxGVog5JetIY8s++P5UrtX+cri+u0VAVg==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "axe-core": "~4.13.0" + }, + "peerDependencies": { + "playwright-core": ">= 1.0.0" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.6.0.tgz", + "integrity": "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.6.0", + "@parcel/watcher-darwin-arm64": "2.6.0", + "@parcel/watcher-darwin-x64": "2.6.0", + "@parcel/watcher-freebsd-x64": "2.6.0", + "@parcel/watcher-linux-arm-glibc": "2.6.0", + "@parcel/watcher-linux-arm-musl": "2.6.0", + "@parcel/watcher-linux-arm64-glibc": "2.6.0", + "@parcel/watcher-linux-arm64-musl": "2.6.0", + "@parcel/watcher-linux-x64-glibc": "2.6.0", + "@parcel/watcher-linux-x64-musl": "2.6.0", + "@parcel/watcher-win32-arm64": "2.6.0", + "@parcel/watcher-win32-x64": "2.6.0" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz", + "integrity": "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.6.0.tgz", + "integrity": "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.6.0.tgz", + "integrity": "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.6.0.tgz", + "integrity": "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.6.0.tgz", + "integrity": "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.6.0.tgz", + "integrity": "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.6.0.tgz", + "integrity": "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.6.0.tgz", + "integrity": "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz", + "integrity": "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.6.0.tgz", + "integrity": "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.6.0.tgz", + "integrity": "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.6.0.tgz", + "integrity": "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@playwright/test": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz", + "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/axe-core": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.13.0.tgz", + "integrity": "sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/immutable": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz", + "integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", + "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright/node_modules/playwright-core": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/readdirp": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sass": { + "version": "1.103.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.103.0.tgz", + "integrity": "sha512-+QpdXUDw19lVqRDlYIyje1Lq/0gHsnNHIl4x1CqeUg13zRhaQN11UvTvouwHWLXc9Q3rQVT6oBhFGRYJ5Z8gHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^5.0.0", + "immutable": "^5.1.5", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=20.19.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + } + } +} diff --git a/tools/copland/package.json b/tools/copland/package.json new file mode 100644 index 0000000..da314aa --- /dev/null +++ b/tools/copland/package.json @@ -0,0 +1,15 @@ +{ + "name": "copland-fossil-skin", + "version": "0.1.0", + "private": true, + "description": "A SourceHut-inspired skin for Fossil SCM", + "scripts": { + "build": "sass --no-source-map --style=expanded src/copland.scss ../../skins/copland/css.txt && node scripts/build-config.mjs", + "test:visual": "playwright test" + }, + "devDependencies": { + "@axe-core/playwright": "4.13.0", + "@playwright/test": "1.61.1", + "sass": "1.103.0" + } +} diff --git a/tools/copland/playwright.config.mjs b/tools/copland/playwright.config.mjs new file mode 100644 index 0000000..979000a --- /dev/null +++ b/tools/copland/playwright.config.mjs @@ -0,0 +1,20 @@ +import { defineConfig } from "@playwright/test"; + +export default defineConfig({ + testDir: "./tests", + testMatch: "visual.spec.mjs", + outputDir: "test-output", + reporter: [["list"], ["html", { open: "never" }]], + use: { + baseURL: "http://127.0.0.1:18082", + browserName: "chromium", + trace: "retain-on-failure", + }, + webServer: { + command: "COPLAND_TEST_PORT=18082 ./tests/serve-fixture.sh", + url: "http://127.0.0.1:18082/", + reuseExistingServer: false, + timeout: 30_000, + }, +}); + diff --git a/tools/copland/scripts/build-config.mjs b/tools/copland/scripts/build-config.mjs new file mode 100644 index 0000000..0a24317 --- /dev/null +++ b/tools/copland/scripts/build-config.mjs @@ -0,0 +1,30 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const skinDir = resolve(projectRoot, "../../skins/copland"); +const outputPath = resolve(projectRoot, "dist/copland-skin.config"); +const parts = ["css", "header", "footer", "details", "js"]; + +function fossilQuote(value) { + return `'${value.replaceAll("'", "''")}'`; +} + +const records = []; +for (const name of parts) { + const value = await readFile(resolve(skinDir, `${name}.txt`), "utf8"); + const record = `1 ${fossilQuote(name)} value ${fossilQuote(value)}`; + records.push(`config /config ${Buffer.byteLength(record, "utf8")}\n${record}\n`); +} + +const header = [ + "# Copland Fossil skin configuration", + "# Generated deterministically by tools/copland/scripts/build-config.mjs.", + "# Import with: fossil configuration import dist/copland-skin.config -R REPOSITORY", + "", +].join("\n"); + +await mkdir(dirname(outputPath), { recursive: true }); +await writeFile(outputPath, header + records.join(""), "utf8"); +console.log(`Wrote ${outputPath}`); diff --git a/tools/copland/scripts/create-demo.sh b/tools/copland/scripts/create-demo.sh new file mode 100755 index 0000000..7027e48 --- /dev/null +++ b/tools/copland/scripts/create-demo.sh @@ -0,0 +1,113 @@ +#!/usr/bin/env bash +set -euo pipefail + +root_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +source "$root_dir/scripts/fossil-env.sh" + +if [[ $# -ne 1 ]]; then + printf 'Usage: %s TARGET_DIRECTORY\n' "$0" >&2 + exit 2 +fi + +target_dir=$1 +mkdir -p "$target_dir/repositories" "$target_dir/checkouts" + +create_repository() { + local slug=$1 + local title=$2 + local repository="$target_dir/repositories/$slug.fossil" + local checkout="$target_dir/checkouts/$slug" + + local is_new=0 + if [[ ! -f "$repository" ]]; then + is_new=1 + "$FOSSIL" init \ + --admin-user copland \ + --date-override 2024-01-01T12:00:00Z \ + --project-name "$title" \ + "$repository" >/dev/null + fi + "$FOSSIL" user default copland -R "$repository" + mkdir -p "$checkout" + if [[ ! -f "$checkout/.fslckout" && ! -f "$checkout/_FOSSIL_" ]]; then + ( + cd /tmp + "$FOSSIL" open --nested --workdir "$checkout" "$repository" >/dev/null + ) + fi + ( + cd "$checkout" + "$FOSSIL" user default copland + if [[ "$is_new" -eq 1 ]]; then + mkdir -p src + printf '#+title: %s\n\nA small repository used to preview the Copland Fossil skin.\n\n* Usage\n\nRun the example from a C compiler.\n' "$title" > README.org + printf '#include <stdio.h>\n\nint main(void) {\n puts("hello from %s");\n return 0;\n}\n' "$slug" > src/main.c + fi + if [[ ! -f docs/org-support.org ]]; then + mkdir -p docs + printf '%s\n' \ + '#+title: Native Org preview' \ + '#+author: Copland' \ + '#+date: 2026-08-21' \ + '#+options: toc:2' \ + '' \ + '* Overview' \ + '' \ + 'This public fixture demonstrates *bold*, /italic/, _underlined_,' \ + '+struck+, ~code~, and =verbatim= text.' \ + '' \ + '- Rendered by Fossil' \ + ' 1. Nested ordered item' \ + '- [[file:../README.org][Back to the README]]' \ + '' \ + '* Data' \ + '' \ + '| Format | Native |' \ + '|--------+--------|' \ + '| Org | yes |' \ + '' \ + '#+begin_src nix' \ + '{ pkgs ? import <nixpkgs> {} }: pkgs.hello' \ + '#+end_src' \ + '' \ + '#+begin_example' \ + '<script>alert("escaped")</script>' \ + '#+end_example' \ + '' \ + '** Details' \ + '' \ + 'Jump [[*Overview][back to the overview]].' > docs/org-support.org + fi + "$FOSSIL" addremove >/dev/null + if [[ -n $("$FOSSIL" changes) ]]; then + "$FOSSIL" commit --no-warnings \ + --date-override 2024-01-03T12:00:00Z \ + --user-override copland \ + -m "Add Org documentation fixture" + fi + if [[ "$is_new" -eq 1 ]]; then + "$FOSSIL" tag add release-0.1 current + fi + ) + + local wiki_fixture="$target_dir/$slug-org-wiki.org" + printf '%s\n' \ + '#+TITLE: This title is intentionally suppressed' \ + '#+AUTHOR: Copland' \ + '#+OPTIONS: toc:2' \ + '' \ + '* Overview' \ + '' \ + 'Open [[wiki:Deployment Guide][the deployment guide]] or the' \ + '[[file:docs/org-support.org][repository documentation]].' \ + '' \ + '** Details' \ + '' \ + 'Return [[*Overview][to the overview]].' >"$wiki_fixture" + if ! "$FOSSIL" wiki list -R "$repository" | grep -Fxq 'Org Wiki'; then + "$FOSSIL" wiki create 'Org Wiki' "$wiki_fixture" -R "$repository" >/dev/null + fi +} + +create_repository alpha "Alpha" +create_repository beta "Beta" diff --git a/tools/copland/scripts/fossil-env.sh b/tools/copland/scripts/fossil-env.sh new file mode 100755 index 0000000..4df9a7b --- /dev/null +++ b/tools/copland/scripts/fossil-env.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +if [[ -z "${FOSSIL:-}" ]]; then + FOSSIL=fossil +fi +export FOSSIL + +fossil_binary_path() { + if [[ "$FOSSIL" == */* ]]; then + if [[ ! -x "$FOSSIL" ]]; then + printf 'Fossil binary is not executable: %s\n' "$FOSSIL" >&2 + return 1 + fi + (CDPATH= cd -- "$(dirname -- "$FOSSIL")" && printf '%s/%s\n' "$PWD" "$(basename -- "$FOSSIL")") + else + command -v "$FOSSIL" + fi +} + +require_org_fossil() { + local binary_path + if ! binary_path=$(fossil_binary_path); then + printf 'Fossil was not found. Run `make fossil` or set FOSSIL=/path/to/fossil.\n' >&2 + return 1 + fi + if ! "$FOSSIL" help test-org-render >/dev/null 2>&1; then + printf 'Fossil at %s has no native Org renderer. Run `make fossil` or set FOSSIL to the patched binary.\n' "$binary_path" >&2 + return 1 + fi + printf 'Fossil binary: %s\n' "$binary_path" +} + +verify_org_mime() { + local repository=$1 + local mime_result + mime_result=$("$FOSSIL" test-mimetype -R "$repository" probe.org) + if [[ "$mime_result" != *"text/x-org"* ]]; then + printf 'Selected Fossil does not map .org to text/x-org: %s\n' "$mime_result" >&2 + return 1 + fi + printf 'Org MIME check: text/x-org\n' +} diff --git a/tools/copland/scripts/install-repolist.sh b/tools/copland/scripts/install-repolist.sh new file mode 100755 index 0000000..d59f688 --- /dev/null +++ b/tools/copland/scripts/install-repolist.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +set -euo pipefail + +root_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +source "$root_dir/scripts/fossil-env.sh" +config_file="$root_dir/dist/copland-skin.config" + +if [[ $# -ne 1 ]]; then + printf 'Usage: %s REPOSITORY_DIRECTORY\n' "$0" >&2 + exit 2 +fi + +repository_dir=$1 +control_repository="$repository_dir/00-copland-skin.fossil" + +if [[ ! -d "$repository_dir" ]]; then + printf 'Repository directory does not exist: %s\n' "$repository_dir" >&2 + exit 1 +fi +if [[ ! -f "$config_file" ]]; then + printf 'Built skin not found. Run make build first.\n' >&2 + exit 1 +fi + +if [[ ! -f "$control_repository" ]]; then + "$FOSSIL" init "$control_repository" >/dev/null +fi + +"$FOSSIL" configuration import "$config_file" -R "$control_repository" +"$FOSSIL" settings repolist-skin 2 --exact -R "$control_repository" +"$FOSSIL" sql -R "$control_repository" \ + "REPLACE INTO config(name,value,mtime) VALUES('project-name','Copland',unixepoch());" + +printf 'Configured hidden repository-list skin: %s\n' "$control_repository" diff --git a/tools/copland/scripts/install.sh b/tools/copland/scripts/install.sh new file mode 100755 index 0000000..7fe0e1c --- /dev/null +++ b/tools/copland/scripts/install.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -euo pipefail + +root_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +source "$root_dir/scripts/fossil-env.sh" +config_file="$root_dir/dist/copland-skin.config" + +if [[ $# -ne 1 ]]; then + printf 'Usage: %s REPOSITORY\n' "$0" >&2 + exit 2 +fi + +repository=$1 +if [[ ! -f "$repository" ]]; then + printf 'Repository does not exist: %s\n' "$repository" >&2 + exit 1 +fi +if [[ ! -f "$config_file" ]]; then + printf 'Built skin not found. Run make build first.\n' >&2 + exit 1 +fi + +"$FOSSIL" configuration import "$config_file" -R "$repository" +printf 'Installed Copland into %s\n' "$repository" diff --git a/tools/copland/scripts/preview.sh b/tools/copland/scripts/preview.sh new file mode 100755 index 0000000..c82bec8 --- /dev/null +++ b/tools/copland/scripts/preview.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -euo pipefail + +root_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +source_root=$(CDPATH= cd -- "$root_dir/../.." && pwd) +source "$root_dir/scripts/fossil-env.sh" +preview_dir="$source_root/.copland-preview" +nix_repository="$source_root/.repository/nix.fossil" +port=${PORT:-8080} + +require_org_fossil +"$root_dir/scripts/create-demo.sh" "$preview_dir" +verify_org_mime "$preview_dir/repositories/alpha.fossil" + +if [[ ! -f "$nix_repository" ]]; then + printf 'Nix documentation repository not found: %s\n' "$nix_repository" >&2 + exit 1 +fi +ln -sfn "$nix_repository" "$preview_dir/repositories/nix.fossil" +verify_org_mime "$preview_dir/repositories/nix.fossil" + +printf 'Copland preview: http://127.0.0.1:%s/\n' "$port" +printf 'Nix wiki: http://127.0.0.1:%s/nix/wiki\n' "$port" +printf 'Press Ctrl-C to stop the server.\n' +FOSSIL_REPOLIST_TITLE=Copland exec "$FOSSIL" server \ + --localhost \ + --localauth \ + --port "$port" \ + --repolist \ + --skin copland \ + "$preview_dir/repositories" diff --git a/tools/copland/src/copland.scss b/tools/copland/src/copland.scss new file mode 100644 index 0000000..8c92ad5 --- /dev/null +++ b/tools/copland/src/copland.scss @@ -0,0 +1,443 @@ +$bp-sm: 36rem; +$bp-md: 48rem; +$bp-lg: 62rem; +$bp-xl: 75rem; + +:root { + color-scheme: light dark; + --page-bg: #fff; + --surface: #fff; + --surface-subtle: #f5f5f5; + --surface-muted: #e9ecef; + --surface-active: #ddd; + --text: #212529; + --text-muted: #6c757d; + --text-faint: #767676; + --border: #ced4da; + --border-strong: #888; + --link: #0645d9; + --link-hover: #002f9f; + --primary: #0056d6; + --primary-hover: #0044aa; + --primary-text: #fff; + --danger: #b00020; + --danger-bg: #f8d7da; + --success: #146c2e; + --success-bg: #d4edda; + --warning-bg: #fff3cd; + --info-bg: #d1ecf1; + --code-bg: #f1f3f5; + --selection: #b9d6ff; + --focus: #ffbf47; + --shadow: rgba(0, 0, 0, 0.12); + --container: 71.25rem; + --gutter: 1rem; + --radius: 0; + --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; +} + +@media (prefers-color-scheme: dark) { + :root { + --page-bg: #212529; + --surface: #212529; + --surface-subtle: #191c1f; + --surface-muted: #2d3237; + --surface-active: #3b4147; + --text: #f1f3f5; + --text-muted: #c2c7cc; + --text-faint: #adb5bd; + --border: #565e65; + --border-strong: #8b949c; + --link: #5c9cff; + --link-hover: #91bcff; + --primary: #4d8ee8; + --primary-hover: #70a7f2; + --primary-text: #101214; + --danger: #ff6b78; + --danger-bg: #57242a; + --success: #65d47d; + --success-bg: #183d24; + --warning-bg: #514318; + --info-bg: #193e47; + --code-bg: #17191b; + --selection: #173f70; + --focus: #ffd166; + --shadow: rgba(0, 0, 0, 0.4); + } +} + +* { box-sizing: border-box; } + +html { + min-height: 100%; + scrollbar-gutter: stable; + font-size: 16px; +} + +body { + min-height: 100vh; + margin: 0; + padding: 0 0 1rem; + background: var(--page-bg); + color: var(--text); + font-family: var(--font-sans); + font-size: 1rem; + line-height: 1.5; + text-align: initial; +} + +::selection { background: var(--selection); } + +.container, +div.content { + width: 100%; + max-width: var(--container); + margin-inline: auto; + padding-inline: var(--gutter); +} + +div.content { min-height: 45vh; } + +body a { + color: var(--link); + text-decoration: underline; + text-underline-offset: 0.12em; +} + +body a:hover { color: var(--link-hover); text-decoration: none; } +a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible, +textarea:focus-visible, [tabindex]:focus-visible { + outline: 3px solid var(--focus); + outline-offset: 2px; +} + +h1, h2, h3, h4, h5, h6 { + margin: 1rem 0 0.5rem; + color: var(--text); + font-weight: 500; + line-height: 1.2; +} + +h1 { font-size: 1.75rem; } +h2 { font-size: 1.45rem; } +h3 { font-size: 1.15rem; border-bottom: 1px solid var(--border); } +h4 { font-size: 1rem; border-bottom: 1px solid var(--border); } +p { margin: 0 0 1rem; } +hr { border: 0; border-top: 1px solid var(--border); } + +.site-header { margin-bottom: 0; } +.site-header-row { + min-height: 3.25rem; + display: flex; + align-items: center; + gap: 1rem; +} + +.site-brand, +.resource-title a, +.account-nav a, +.mainmenu a { + text-decoration: none; +} + +.site-brand { + flex: 0 0 auto; + color: var(--text); + font-size: 1.25rem; + font-weight: 600; + letter-spacing: -0.025em; +} + +.site-brand::before { + content: ""; + display: inline-block; + width: 0.78rem; + height: 0.78rem; + margin-right: 0.42rem; + border: 2px solid currentColor; + border-radius: 50%; + vertical-align: 0.02rem; +} + +.site-brand:hover, +.resource-title a:hover, +.account-nav a:hover, +.mainmenu a:hover { color: var(--text); text-decoration: underline; } + +.resource-title { + min-width: 0; + display: flex; + align-items: baseline; + gap: 0.4rem; + overflow: hidden; + white-space: nowrap; +} + +.resource-title a, +.resource-page { + overflow: hidden; + text-overflow: ellipsis; +} + +.resource-title a { color: var(--text); font-weight: 600; } +.resource-separator, .resource-page { color: var(--text-muted); } +.account-nav { margin-left: auto; white-space: nowrap; font-size: 0.9rem; } + +.resource-nav { + border-bottom: 3px solid var(--surface-active); +} + +.mainmenu { + min-height: 2rem; + display: flex; + align-items: end; + gap: 0; + overflow-x: auto; + scrollbar-width: thin; +} + +.mainmenu a { + flex: 0 0 auto; + padding: 0.25rem 0.85rem 0.18rem; + color: var(--text-muted); + white-space: nowrap; +} + +.mainmenu a.active, +.mainmenu a.selected, +.mainmenu a:hover { + color: var(--text); +} + +.mainmenu a.active, +.mainmenu a.selected { background: var(--surface-active); } + +.page-title { + margin-top: 0.9rem; + margin-bottom: 0.75rem; + font-size: 1.35rem; + font-weight: 500; +} + +.site-footer { + margin-top: 2rem; + color: var(--text-muted); + font-size: 0.8rem; +} +.site-footer .container { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.4rem; } + +pre, code, kbd, samp, tt { font-family: var(--font-mono); } +code, tt { overflow-wrap: anywhere; } +pre { + max-width: 100%; + overflow: auto; + margin: 0 0 1rem; + padding: 0.75rem 1rem; + background: var(--code-bg); + color: var(--text); + font-size: 0.875rem; + line-height: 1.35; + tab-size: 4; +} +pre a { color: inherit; } +blockquote { margin: 0 0 1rem; padding-left: 1rem; border-left: 4px solid var(--border); } + +.org-document { + max-width: 100%; +} + +.org-document .org-title { margin-top: 0; } + +.org-metadata { + display: grid; + grid-template-columns: max-content minmax(0, 1fr); + gap: 0.15rem 0.75rem; + margin: 0 0 1rem; + color: var(--text-muted); + font-size: 0.92rem; +} + +.org-metadata dt { font-weight: 600; } +.org-metadata dd { margin: 0; } + +.org-toc { + margin: 0 0 1.25rem; + padding: 0.75rem 1rem; + border: 1px solid var(--border); + background: var(--surface-subtle); +} + +.org-toc h2 { margin-top: 0; font-size: 1.05rem; } +.org-toc ol { margin: 0; padding-left: 1.5rem; } +.org-toc-level-2 { margin-left: 1rem; } +.org-toc-level-3 { margin-left: 2rem; } +.org-toc-level-4, .org-toc-level-5, .org-toc-level-6 { margin-left: 3rem; } + +.org-table { display: block; overflow-x: auto; } +.org-block { border-left: 3px solid var(--border-strong); } +.org-example { color: var(--text); } +.org-unknown-block { border-left-color: var(--danger); } + +table { + width: 100%; + margin-bottom: 1rem; + border-collapse: collapse; + color: var(--text); +} +th { text-align: left; font-weight: 600; } +th, td { padding: 0.28rem 0.5rem; vertical-align: top; border-top: 1px solid var(--border); } +thead th { border-bottom: 2px solid var(--border-strong); } +tbody tr:nth-child(odd) { background: color-mix(in srgb, var(--surface-subtle) 70%, transparent); } +table.browser, table.report, table.timelineTable { font-size: 0.92rem; } +table.browser td:first-child, table.browser td:nth-child(2) { font-family: var(--font-mono); } + +input, button, select, textarea { font: inherit; } +input[type="text"], input[type="search"], input[type="email"], input[type="password"], +input[type="url"], input[type="number"], textarea, select { + max-width: 100%; + padding: 0.35rem 0.5rem; + border: 1px solid var(--border-strong); + border-radius: var(--radius); + background: var(--surface); + color: var(--text); +} +textarea { width: 100%; } +label { display: inline-block; margin-bottom: 0.25rem; } +button, input[type="submit"], input[type="button"], .button, a.button { + display: inline-block; + padding: 0.35rem 0.7rem; + border: 1px solid var(--primary); + border-radius: var(--radius); + background: var(--primary); + color: var(--primary-text); + cursor: pointer; + line-height: 1.25; + text-decoration: none; +} +button:hover, input[type="submit"]:hover, input[type="button"]:hover, .button:hover { + border-color: var(--primary-hover); + background: var(--primary-hover); +} +button:disabled, input:disabled { cursor: not-allowed; opacity: 0.65; } +fieldset { margin: 0 0 1rem; border: 1px solid var(--border); } +legend { padding-inline: 0.35rem; } + +.submenu, .sectionmenu { + margin: 0 0 1rem; + padding: 0.4rem 0.6rem; + background: var(--surface-subtle); + border: 1px solid var(--border); +} +.submenu a, .sectionmenu a { margin-right: 0.75rem; } + +.generalError, .error, .fatalError, .loginError, +.warning, .notice, .success { + margin: 0.75rem 0; + padding: 0.65rem 0.8rem; + border: 1px solid var(--border); +} +.generalError, .error, .fatalError, .loginError { background: var(--danger-bg); color: var(--text); } +.warning { background: var(--warning-bg); } +.notice { background: var(--info-bg); } +.success { background: var(--success-bg); } + +.timelineDate { margin-top: 1rem; color: var(--text); font-weight: 600; } +.timelineHistDsp { color: var(--text-muted); } +.timelineModernCell, .timelineColumnarCell, .timelineDetailCell { padding-block: 0.35rem; } +.timelineModernDetail { opacity: 1; } +.timelineCompactComment, .timelineComment { line-height: 1.35; } +.timelineTime { color: var(--text-muted); white-space: nowrap; } +.timelineGraph { min-width: 2rem; } +.tl-node { border-radius: 50%; } +.timelineModernCell a, +.timelineColumnarCell a, +.timelineDetailCell a { color: var(--link) !important; } +.timelineModernCell a:hover, +.timelineColumnarCell a:hover, +.timelineDetailCell a:hover { color: var(--link-hover) !important; } + +.filetree { font-family: var(--font-mono); font-size: 0.9rem; } +.filetree ul { padding-left: 1.25rem; } +.filetree a { color: var(--text); } +.filetree a:hover { color: var(--link); } +.fileage tr:hover, table.browser tr:hover { background: var(--surface-active); } + +.diff, .udiff, .sbsdiff { font-family: var(--font-mono); font-size: 0.82rem; } +.diff pre, .udiff pre, .sbsdiff pre { background: transparent; } +.diffln, .diffhr { color: var(--text-muted); background: var(--surface-muted); } +.diffadd, .diffins { background: var(--success-bg); } +.diffrm, .diffdel { background: var(--danger-bg); } +.diffchng { background: var(--warning-bg); } +.selectedText { background: var(--selection); } + +.forumPost, .forumTime, .forumSel, .forumHierRoot, .forumHier { + border-color: var(--border); +} +.forumPost, .forumTime { background: var(--surface-subtle); color: var(--text); } +.forumPostBody { overflow-wrap: anywhere; } +.forumPostBody img, .wiki-content img, .markdown img { max-width: 100%; height: auto; } + +.ticketDspLabel, .ticketDspValue { padding: 0.35rem 0.5rem; } +.ticketDspLabel { color: var(--text-muted); font-weight: 600; } +.ticketDspValue { border-bottom: 1px solid var(--border); } + +.branchname, .tagname, .wikiTagCancelled, .infoTagCancelled { + display: inline-block; + padding: 0.05rem 0.3rem; + background: var(--surface-muted); + color: var(--text); + font-size: 0.82rem; +} + +.statistics-report-table-events th, .statistics-report-table-events td { text-align: right; } +.statistics-report-table-events th:first-child, .statistics-report-table-events td:first-child { text-align: left; } +.accordion_panel { border-color: var(--border); background: var(--surface); } +.accordion_marker { color: var(--text-muted); } + +body.repolist .resource-title, +body.repolist .resource-nav, +body.repolist > .page-title { display: none; } +body.repolist div.content { padding-top: 1rem; } +body.repolist table td:first-child { font-family: var(--font-mono); font-weight: 600; } + +@media (prefers-color-scheme: dark) { + img { opacity: 0.92; } + body .timelineModernCell, + body .timelineColumnarCell, + body .timelineDetailCell, + .timelineModernCell[style*="background-color"], + .timelineColumnarCell[style*="background-color"], + .timelineDetailCell[style*="background-color"], + td[style*="background-color"] { + background-color: var(--surface-subtle) !important; + color: var(--text) !important; + } + .diffadd, .diffins { color: var(--text); } + .diffrm, .diffdel { color: var(--text); } +} + +@media (max-width: $bp-md) { + :root { --gutter: 0.75rem; } + .site-header-row { min-height: 2.8rem; gap: 0.65rem; } + .resource-page, .resource-separator { display: none; } + .mainmenu a { padding-inline: 0.65rem; } + .page-title { font-size: 1.2rem; } + table:not(.sbsdiffcols) { display: block; max-width: 100%; overflow-x: auto; } + .desktoponly { display: none !important; } +} + +@media (max-width: $bp-sm) { + .site-brand { font-size: 1.1rem; } + .resource-title { font-size: 0.9rem; } + .account-nav { font-size: 0.82rem; } + .page-title { margin-top: 0.7rem; } + pre { padding: 0.6rem 0.7rem; } +} + +@media print { + .resource-nav, .account-nav, .submenu, .sectionmenu, .site-footer { display: none !important; } + body, pre, code { background: #fff !important; color: #000 !important; } + a { color: #000; text-decoration: underline; } + div.content, .container { max-width: none; padding: 0; } +} diff --git a/tools/copland/tests/fixtures/org/all.html b/tools/copland/tests/fixtures/org/all.html new file mode 100644 index 0000000..33fc537 --- /dev/null +++ b/tools/copland/tests/fixtures/org/all.html @@ -0,0 +1,50 @@ +<div class="org-document"> +<h1 class="org-title">Über & <Fossil></h1> +<dl class="org-metadata"> +<dt>Author</dt><dd>Ada <admin@example.test></dd> +<dt>Date</dt><dd>2026-08-21</dd> +</dl> +<nav class="org-toc" aria-label="Table of contents"><h2>Contents</h2> +<ol> +<li class="org-toc-level-1"><a href="#intro-safety">Intro & safety</a></li> +<li class="org-toc-level-2"><a href="#duplicate">Duplicate</a></li> +<li class="org-toc-level-1"><a href="#duplicate-2">Duplicate</a></li> +<li class="org-toc-level-1"><a href="#日本語">日本語</a></li> +</ol> +</nav> +<h1 id="intro-safety">Intro & safety</h1> +<p>A paragraph with <strong>bold</strong>, <em>italic</em>, <u>underline</u>, <del>strike</del>, <code><code></code>, and <code>verbatim & text</code>. Words_like_this and paths/like/this stay literal.</p> +<ul> +<li>first<ul> +<li>nested <strong>item</strong></li> +</ul> +<ol> +<li>nested ordered</li> +</ol> +</li> +<li>second</li> +</ul> +<ol> +<li>one</li> +<li>two</li> +</ol> +<table class="org-table"> +<thead> +<tr><th>Name</th><th>Value</th></tr> +</thead> +<tbody> +<tr><td>α</td><td><tag></td></tr> +<tr><td>two</td><td><strong>bold</strong></td></tr> +</tbody> +</table> +<pre class="org-block org-src has-language"><code class="language-nix">{ pkgs, ... }: <unsafe> & value</code></pre> +<pre class="org-block org-example"><code>example <script>alert(1)</script></code></pre> +<p><a href="https://example.test/a?x=1&y=2">external</a> <a href="guide.org">relative</a> <a href="#intro-safety">internal</a> unsafe</p> +<h2 id="duplicate">Duplicate</h2> +<p>Text.</p> +<h1 id="duplicate-2">Duplicate</h1> +<p>More.</p> +<h1 id="日本語">日本語</h1> +<p>Unicode heading.</p> +</div> + diff --git a/tools/copland/tests/fixtures/org/all.org b/tools/copland/tests/fixtures/org/all.org new file mode 100644 index 0000000..f5c644a --- /dev/null +++ b/tools/copland/tests/fixtures/org/all.org @@ -0,0 +1,52 @@ +#+TITLE: Über & <Fossil> +#+AUTHOR: Ada <admin@example.test> +#+DATE: 2026-08-21 +#+OPTIONS: toc:2 num:nil +#+STARTUP: overview + +:PROPERTIES: +:ID: hidden +:END: + +* Intro & safety + +A paragraph with *bold*, /italic/, _underline_, +strike+, ~<code>~, and +=verbatim & text=. Words_like_this and paths/like/this stay literal. + +- first + - nested *item* + 1. nested ordered +- second + +1. one +2. two + +| Name | Value | +|------+-------| +| α | <tag> | +| two | *bold* | + +#+begin_src nix +{ pkgs, ... }: <unsafe> & value +#+end_src + +#+begin_example +example <script>alert(1)</script> +#+end_example + +[[https://example.test/a?x=1&y=2][external]] +[[file:guide.org][relative]] +[[*Intro & safety][internal]] +[[javascript:alert(1)][unsafe]] + +** Duplicate + +Text. + +* Duplicate + +More. + +* 日本語 + +Unicode heading. diff --git a/tools/copland/tests/fixtures/org/all.txt b/tools/copland/tests/fixtures/org/all.txt new file mode 100644 index 0000000..266ab90 --- /dev/null +++ b/tools/copland/tests/fixtures/org/all.txt @@ -0,0 +1,66 @@ +Über & <Fossil> + + + +AuthorAda <admin@example.test> +Date2026-08-21 + + + +Contents + +Intro & safety +Duplicate +Duplicate +日本語 + + + +Intro & safety + +A paragraph with bold, italic, underline, strike, <code>, and verbatim & text. Words_like_this and paths/like/this stay literal. + +first +nested item + + +nested ordered + + +second + + +one +two + + + + + +NameValue + + + +α<tag> +twobold + + + +{ pkgs, ... }: <unsafe> & value + +example <script>alert(1)</script> + +external relative internal unsafe + +Duplicate + +Text. + +Duplicate + +More. + +日本語 + +Unicode heading. + diff --git a/tools/copland/tests/fixtures/org/security.html b/tools/copland/tests/fixtures/org/security.html new file mode 100644 index 0000000..38707e0 --- /dev/null +++ b/tools/copland/tests/fixtures/org/security.html @@ -0,0 +1,9 @@ +<div class="org-document"> +<h1 class="org-title"><img src=x onerror=alert(1)></h1> +<p>Raw <script>alert(1)</script> and "quotes".</p> +<p><a href="https://example.test/" onmouseover="alert(1)">attribute</a> file scheme confusion</p> +<pre class="org-block org-unknown-block"><code><img src=x onerror=alert(1)></code></pre> +<pre class="org-block org-src has-language"><code class="language-sh">echo "unterminated and escaped <tag>" +</code></pre> +</div> + diff --git a/tools/copland/tests/fixtures/org/security.org b/tools/copland/tests/fixtures/org/security.org new file mode 100644 index 0000000..3e7280a --- /dev/null +++ b/tools/copland/tests/fixtures/org/security.org @@ -0,0 +1,13 @@ +#+TITLE: <img src=x onerror=alert(1)> + +Raw <script>alert(1)</script> and "quotes". + +[[https://example.test/" onmouseover="alert(1)][attribute]] +[[file:javascript:alert(1)][file scheme confusion]] + +#+begin_export html +<img src=x onerror=alert(1)> +#+end_export + +#+begin_src sh +echo "unterminated and escaped <tag>" diff --git a/tools/copland/tests/org-renderer.sh b/tools/copland/tests/org-renderer.sh new file mode 100755 index 0000000..1822d52 --- /dev/null +++ b/tools/copland/tests/org-renderer.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -euo pipefail + +root_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +source "$root_dir/scripts/fossil-env.sh" +fixture_dir="$root_dir/tests/fixtures/org" +output_dir=$(mktemp -d -t copland-org-renderer.XXXXXX) + +for fixture in all security; do + "$FOSSIL" test-org-render "$fixture_dir/$fixture.org" >"$output_dir/$fixture.html" + diff -u "$fixture_dir/$fixture.html" "$output_dir/$fixture.html" +done + +"$FOSSIL" test-org-render --text "$fixture_dir/all.org" >"$output_dir/all.txt" +diff -u "$fixture_dir/all.txt" "$output_dir/all.txt" + +sed 's/$/\r/' "$fixture_dir/all.org" >"$output_dir/all-crlf.org" +"$FOSSIL" test-org-render "$output_dir/all-crlf.org" >"$output_dir/all-crlf.html" +diff -u "$fixture_dir/all.html" "$output_dir/all-crlf.html" + +if rg -n '<script|<img[^>]*onerror|href="javascript:' "$output_dir"/*.html; then + printf 'Unsafe active HTML escaped into Org renderer output.\n' >&2 + exit 1 +fi +if ! rg -q '<script>|<img' "$output_dir/security.html"; then + printf 'Escaped security fixture content is missing.\n' >&2 + exit 1 +fi + +printf 'Org renderer golden and security tests passed.\n' diff --git a/tools/copland/tests/serve-fixture.sh b/tools/copland/tests/serve-fixture.sh new file mode 100755 index 0000000..7f2bc8d --- /dev/null +++ b/tools/copland/tests/serve-fixture.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -euo pipefail + +root_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +source "$root_dir/scripts/fossil-env.sh" +fixture_dir=$(mktemp -d -t copland-visual.XXXXXX) +port=${COPLAND_TEST_PORT:-18082} + +require_org_fossil +"$root_dir/scripts/create-demo.sh" "$fixture_dir" +verify_org_mime "$fixture_dir/repositories/alpha.fossil" + +FOSSIL_REPOLIST_TITLE=Copland exec "$FOSSIL" server \ + --localhost \ + --localauth \ + --port "$port" \ + --repolist \ + "$fixture_dir/repositories" diff --git a/tools/copland/tests/smoke.sh b/tools/copland/tests/smoke.sh new file mode 100755 index 0000000..bd54bab --- /dev/null +++ b/tools/copland/tests/smoke.sh @@ -0,0 +1,188 @@ +#!/usr/bin/env bash +set -euo pipefail + +root_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +source "$root_dir/scripts/fossil-env.sh" +test_dir=$(mktemp -d -t copland-smoke.XXXXXX) +port=${COPLAND_TEST_PORT:-18081} +server_pid="" + +finish() { + if [[ -n "$server_pid" ]]; then + kill "$server_pid" 2>/dev/null || true + wait "$server_pid" 2>/dev/null || true + fi +} +trap finish EXIT INT TERM + +require_org_fossil +"$root_dir/scripts/create-demo.sh" "$test_dir" +verify_org_mime "$test_dir/repositories/alpha.fossil" +"$FOSSIL" sql -R "$test_dir/repositories/alpha.fossil" \ + "REPLACE INTO config(name,value,mtime) VALUES('doc-glob','*.org',unixepoch());" +"$FOSSIL" fts-config enable document -R "$test_dir/repositories/alpha.fossil" >/dev/null +"$FOSSIL" fts-config enable wiki -R "$test_dir/repositories/alpha.fossil" >/dev/null +"$FOSSIL" fts-config index on -R "$test_dir/repositories/alpha.fossil" >/dev/null +"$FOSSIL" fts-config reindex -R "$test_dir/repositories/alpha.fossil" >/dev/null + +FOSSIL_REPOLIST_TITLE=Copland "$FOSSIL" server \ + --localhost \ + --localauth \ + --port "$port" \ + --repolist \ + "$test_dir/repositories" >"$test_dir/server.log" 2>&1 & +server_pid=$! + +base_url="http://127.0.0.1:$port" +for attempt in $(seq 1 50); do + if curl --fail --location --silent "$base_url/" >/dev/null; then + break + fi + if [[ "$attempt" -eq 50 ]]; then + printf 'Fossil server did not start.\n' >&2 + sed -n '1,120p' "$test_dir/server.log" >&2 + exit 1 + fi + sleep 0.1 +done + +paths=( + "/" + "/alpha/home" + "/alpha/timeline" + "/alpha/tree?ci=tip" + "/alpha/dir?ci=tip" + "/alpha/brlist" + "/alpha/taglist" + "/alpha/wiki" + "/alpha/wiki?name=Org%20Wiki" + "/alpha/wikiedit?name=New%20Org%20Page" + "/alpha/whistory?name=Org%20Wiki" + "/alpha/reportlist" + "/alpha/forum" + "/alpha/login" + "/alpha/sitemap" + "/alpha/doc/tip/docs/org-support.org" + "/alpha/file?name=docs/org-support.org&ci=tip" + "/alpha/file?name=docs/org-support.org&ci=tip&txt=1" + "/alpha/search?s=Native%20Org%20preview&y=d" + "/alpha/search?s=deployment%20guide&y=w" +) + +for path in "${paths[@]}"; do + body=$(curl --fail --location --silent --show-error "$base_url$path") + if [[ "$body" != *"Copland"* ]]; then + printf 'Copland marker missing from %s\n' "$path" >&2 + exit 1 + fi + if [[ "$body" == *"thmainError"* ]]; then + printf 'TH1 template error on %s\n' "$path" >&2 + exit 1 + fi + if [[ "$path" == "/" && ("$body" != *"Alpha"* || "$body" != *"Beta"*) ]]; then + printf 'Demo repositories missing from repository list\n' >&2 + exit 1 + fi + if [[ "$path" == "/alpha/home" ]]; then + mainmenu=${body#*'<nav class="container mainmenu"'} + mainmenu=${mainmenu%%'</nav>'*} + for hidden_route in chat taglist forum reportlist sitemap ticket tkt; do + if [[ "$mainmenu" == *"/$hidden_route"* ]]; then + printf 'Hidden route /%s is still present in the main menu\n' "$hidden_route" >&2 + exit 1 + fi + done + fi + if [[ "$path" == "/alpha/wiki" ]]; then + if [[ "$body" != *"Available Wiki Pages"* || "$body" != *"Org Wiki"* ]]; then + printf 'Wiki landing page did not list available pages\n' >&2 + exit 1 + fi + fi + if [[ "$path" == "/alpha/tree?ci=tip" ]]; then + if [[ "$body" != *'class="org-document"'* || "$body" != *"A small repository"* ]]; then + printf 'README.org was not rendered below the repository tree\n' >&2 + exit 1 + fi + fi + if [[ "$path" == "/alpha/doc/tip/docs/org-support.org" ]]; then + if [[ "$body" != *'<title>Alpha: Native Org preview</title>'* + || "$body" != *'class="org-toc"'* + || "$body" != *'href="../README.org"'* + || "$body" != *'id="overview"'* ]]; then + printf 'Native Org /doc rendering is incomplete\n' >&2 + exit 1 + fi + fi + if [[ "$path" == "/alpha/wiki?name=Org%20Wiki" ]]; then + if [[ "$body" != *'<title>Alpha: Org Wiki</title>'* + || "$body" != *'class="org-document"'* + || "$body" == *'class="org-title"'* + || "$body" != *'href="wiki?name=Deployment+Guide"'* + || "$body" != *'href="doc/tip/docs/org-support.org"'* ]]; then + printf 'Native Org wiki rendering is incomplete\n' >&2 + exit 1 + fi + fi + if [[ "$path" == "/alpha/wikiedit?name=New%20Org%20Page" ]]; then + if [[ "$body" != *'value="text/x-org" selected'* ]]; then + printf 'Org is not the default format in the wiki editor\n' >&2 + exit 1 + fi + fi + if [[ "$path" == "/alpha/file?name=docs/org-support.org&ci=tip" ]]; then + if [[ "$body" != *'class="org-document"'* || "$body" != *'>Text</a>'* ]]; then + printf 'Native Org /file rendering or Text toggle is missing\n' >&2 + exit 1 + fi + fi + if [[ "$path" == "/alpha/file?name=docs/org-support.org&ci=tip&txt=1" ]]; then + if [[ "$body" == *'class="org-document"'* || "$body" != *'#+title: Native Org preview'* ]]; then + printf 'Org /file Text view did not show source\n' >&2 + exit 1 + fi + fi + if [[ "$path" == "/alpha/search?s=Native%20Org%20preview&y=d" ]]; then + if [[ "$body" != *"Native Org preview"* ]]; then + printf 'Org document was not found by full-text search\n' >&2 + exit 1 + fi + fi + if [[ "$path" == "/alpha/search?s=deployment%20guide&y=w" ]]; then + if [[ "$body" != *"Org Wiki"* ]]; then + printf 'Org wiki page was not found by full-text search\n' >&2 + exit 1 + fi + fi + printf 'ok %s\n' "$path" +done + +default_skin_body=$(curl --fail --location --silent --show-error \ + "$base_url/alpha/home?skin=default") +if [[ "$default_skin_body" == *'class="site-brand"'* ]]; then + printf 'The original Fossil default skin is not selectable\n' >&2 + exit 1 +fi +copland_skin_body=$(curl --fail --location --silent --show-error \ + "$base_url/alpha/home?skin=copland") +if [[ "$copland_skin_body" != *'class="site-brand"'* ]]; then + printf 'The built-in Copland skin is not selectable\n' >&2 + exit 1 +fi + +json_wiki=$(curl --fail --silent --show-error \ + "$base_url/alpha/json/wiki/get?name=Org%20Wiki&format=html") +if [[ "$json_wiki" != *'org-document'* || "$json_wiki" != *'contentFormat":"html'* ]]; then + printf 'JSON wiki output did not render Org\n' >&2 + exit 1 +fi +json_preview=$(curl --fail --silent --show-error \ + --header 'Content-Type: application/json' \ + --data '{"payload":{"body":"* JSON preview","mimetype":"text/x-org"}}' \ + "$base_url/alpha/json/wiki/preview") +if [[ "$json_preview" != *'org-document'* || "$json_preview" != *'JSON preview'* ]]; then + printf 'JSON wiki preview did not render Org\n' >&2 + exit 1 +fi + +printf 'Smoke tests passed. Temporary fixtures: %s\n' "$test_dir" diff --git a/tools/copland/tests/visual.spec.mjs b/tools/copland/tests/visual.spec.mjs new file mode 100644 index 0000000..73559e1 --- /dev/null +++ b/tools/copland/tests/visual.spec.mjs @@ -0,0 +1,94 @@ +import AxeBuilder from "@axe-core/playwright"; +import { expect, test } from "@playwright/test"; + +const pages = [ + ["repository-list", "/"], + ["home", "/alpha/home"], + ["timeline", "/alpha/timeline"], + ["tree", "/alpha/tree?ci=tip"], + ["org-document", "/alpha/doc/tip/docs/org-support.org"], + ["org-wiki", "/alpha/wiki?name=Org%20Wiki"], + ["login", "/alpha/login"], +]; + +for (const colorScheme of ["light", "dark"]) { + for (const [name, path] of pages) { + test(`${name} is legible in ${colorScheme} mode`, async ({ page }) => { + await page.emulateMedia({ colorScheme }); + await page.goto(path); + await expect(page.locator("body")).toContainText("Copland"); + + const viewportOverflow = await page.evaluate( + () => document.documentElement.scrollWidth > document.documentElement.clientWidth, + ); + expect(viewportOverflow).toBe(false); + + let accessibilityScan = new AxeBuilder({ page }) + .withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"]) + .disableRules(["html-has-lang"]) + .exclude(".submenu"); + if (name === "login") { + accessibilityScan = accessibilityScan.exclude("form"); + } + const accessibility = await accessibilityScan.analyze(); + expect(accessibility.violations).toEqual([]); + + const screenshotMasks = [page.locator(".site-footer span:last-child")]; + if (name === "login") { + screenshotMasks.push(page.locator("form")); + } + await expect(page).toHaveScreenshot(`${name}-${colorScheme}.png`, { + fullPage: true, + animations: "disabled", + mask: screenshotMasks, + maxDiffPixelRatio: 0.001, + }); + }); + } +} + +test.describe("mobile", () => { + test.use({ viewport: { width: 320, height: 720 } }); + + test("navigation remains usable", async ({ page }) => { + await page.goto("/alpha/timeline"); + await expect(page.locator(".site-brand")).toHaveText("Copland"); + await expect(page.locator(".mainmenu")).toBeVisible(); + const viewportOverflow = await page.evaluate( + () => document.documentElement.scrollWidth > document.documentElement.clientWidth, + ); + expect(viewportOverflow).toBe(false); + await expect(page).toHaveScreenshot("timeline-mobile.png", { + fullPage: true, + mask: [page.locator(".site-footer span:last-child")], + }); + }); + + test("Org documentation remains readable", async ({ page }) => { + await page.goto("/alpha/doc/tip/docs/org-support.org"); + await expect(page.locator(".org-document")).toBeVisible(); + await expect(page.locator(".org-toc")).toBeVisible(); + const viewportOverflow = await page.evaluate( + () => document.documentElement.scrollWidth > document.documentElement.clientWidth, + ); + expect(viewportOverflow).toBe(false); + await expect(page).toHaveScreenshot("org-document-mobile.png", { + fullPage: true, + mask: [page.locator(".site-footer span:last-child")], + }); + }); + + test("Org wiki remains readable", async ({ page }) => { + await page.goto("/alpha/wiki?name=Org%20Wiki"); + await expect(page.locator(".org-document")).toBeVisible(); + await expect(page.locator(".org-toc")).toBeVisible(); + const viewportOverflow = await page.evaluate( + () => document.documentElement.scrollWidth > document.documentElement.clientWidth, + ); + expect(viewportOverflow).toBe(false); + await expect(page).toHaveScreenshot("org-wiki-mobile.png", { + fullPage: true, + mask: [page.locator(".site-footer span:last-child")], + }); + }); +}); diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/home-dark-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/home-dark-linux.png Binary files differnew file mode 100644 index 0000000..52f9478 --- /dev/null +++ b/tools/copland/tests/visual.spec.mjs-snapshots/home-dark-linux.png diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/home-light-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/home-light-linux.png Binary files differnew file mode 100644 index 0000000..4660bf9 --- /dev/null +++ b/tools/copland/tests/visual.spec.mjs-snapshots/home-light-linux.png diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/login-dark-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/login-dark-linux.png Binary files differnew file mode 100644 index 0000000..738588b --- /dev/null +++ b/tools/copland/tests/visual.spec.mjs-snapshots/login-dark-linux.png diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/login-light-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/login-light-linux.png Binary files differnew file mode 100644 index 0000000..03ba2db --- /dev/null +++ b/tools/copland/tests/visual.spec.mjs-snapshots/login-light-linux.png diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/org-document-dark-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/org-document-dark-linux.png Binary files differnew file mode 100644 index 0000000..15d37f9 --- /dev/null +++ b/tools/copland/tests/visual.spec.mjs-snapshots/org-document-dark-linux.png diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/org-document-light-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/org-document-light-linux.png Binary files differnew file mode 100644 index 0000000..3c0785f --- /dev/null +++ b/tools/copland/tests/visual.spec.mjs-snapshots/org-document-light-linux.png diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/org-document-mobile-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/org-document-mobile-linux.png Binary files differnew file mode 100644 index 0000000..d1f5cba --- /dev/null +++ b/tools/copland/tests/visual.spec.mjs-snapshots/org-document-mobile-linux.png diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/org-wiki-dark-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/org-wiki-dark-linux.png Binary files differnew file mode 100644 index 0000000..464197b --- /dev/null +++ b/tools/copland/tests/visual.spec.mjs-snapshots/org-wiki-dark-linux.png diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/org-wiki-light-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/org-wiki-light-linux.png Binary files differnew file mode 100644 index 0000000..ff3bdc1 --- /dev/null +++ b/tools/copland/tests/visual.spec.mjs-snapshots/org-wiki-light-linux.png diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/org-wiki-mobile-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/org-wiki-mobile-linux.png Binary files differnew file mode 100644 index 0000000..50381e0 --- /dev/null +++ b/tools/copland/tests/visual.spec.mjs-snapshots/org-wiki-mobile-linux.png diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/repository-list-dark-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/repository-list-dark-linux.png Binary files differnew file mode 100644 index 0000000..0cf1b4f --- /dev/null +++ b/tools/copland/tests/visual.spec.mjs-snapshots/repository-list-dark-linux.png diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/repository-list-light-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/repository-list-light-linux.png Binary files differnew file mode 100644 index 0000000..af49135 --- /dev/null +++ b/tools/copland/tests/visual.spec.mjs-snapshots/repository-list-light-linux.png diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/timeline-dark-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/timeline-dark-linux.png Binary files differnew file mode 100644 index 0000000..469d7b0 --- /dev/null +++ b/tools/copland/tests/visual.spec.mjs-snapshots/timeline-dark-linux.png diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/timeline-light-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/timeline-light-linux.png Binary files differnew file mode 100644 index 0000000..e1dbe69 --- /dev/null +++ b/tools/copland/tests/visual.spec.mjs-snapshots/timeline-light-linux.png diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/timeline-mobile-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/timeline-mobile-linux.png Binary files differnew file mode 100644 index 0000000..02ed528 --- /dev/null +++ b/tools/copland/tests/visual.spec.mjs-snapshots/timeline-mobile-linux.png diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/tree-dark-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/tree-dark-linux.png Binary files differnew file mode 100644 index 0000000..164d3eb --- /dev/null +++ b/tools/copland/tests/visual.spec.mjs-snapshots/tree-dark-linux.png diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/tree-light-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/tree-light-linux.png Binary files differnew file mode 100644 index 0000000..ed51114 --- /dev/null +++ b/tools/copland/tests/visual.spec.mjs-snapshots/tree-light-linux.png diff --git a/tools/cvs2fossil/changeset b/tools/cvs2fossil/changeset new file mode 100644 index 0000000..c1c853e --- /dev/null +++ b/tools/cvs2fossil/changeset @@ -0,0 +1,119 @@ +#!/bin/sh +## -*- tcl -*- \ +exec tclsh "$0" ${1+"$@"} + +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Helper application, debugging of cvs2fossil. This application +## extracts all information about a changeset and writes it nicely +## formatted to stdout. The changeset is specified by its internal +## numerical id. + +# # ## ### ##### ######## ############# ##################### +## Requirements, extended package management for local packages. + +lappend auto_path [file join [file dirname [info script]] lib] + +package require Tcl 8.4 ; # Required runtime. +package require struct::graph ; # Graph handling. +package require struct::list ; # Higher order list ops. +package require vc::fossil::import::cvs::project::rev ; # Changesets +package require vc::fossil::import::cvs::state ; # State storage. +package require vc::tools::misc ; # Min/max. + +namespace import ::vc::fossil::import::cvs::state +namespace import ::vc::fossil::import::cvs::project::rev +namespace import ::vc::tools::misc::* +namespace import ::vc::tools::log +log verbosity 0 + +# Process the command line, i.e. get the database to access, and file +# of interest. The latter can be specified by name, id, or indirectly +# through the id of one of the revisions it contains. + +state use [lindex $argv 0] +state reading changeset +state reading cstype +state reading csrevision +state reading project +state reading revision +state reading file +state reading symbol +state reading meta +state reading author +state reading cmessage + +set cid [lindex $argv 1] + +struct::list assign [state run { + SELECT C.cid, C.pid, C.src, P.name, CT.name + FROM changeset C, project P, cstype CT + WHERE C.cid = $cid + AND P.pid = C.pid + AND CT.tid = C.type +}] cid pid src pname tname + +puts "" +puts "Changeset <$tname $cid> in project \"$pname\" ($pid)" + +if {$tname eq "sym"} { + puts "Symbol \"[state run { + SELECT name + FROM symbol + WHERE sid = $src + }]\"" +} else { + struct::list assign [state run { + SELECT P.name, S.name, A.name, L.text + FROM meta M, project P, symbol S, author A, cmessage L + WHERE M.mid = $src + AND P.pid = M.pid + AND S.sid = M.bid + AND A.aid = M.aid + AND L.cid = M.cid + }] project lod author cmessage + puts "Meta: Project = \"$project\"" + puts "Meta: LOD = \"$lod\"" + puts "Meta: Author = \"$author\"" + puts "Meta: Log |[join [split $cmessage \n] "\"\nMeta: Log |"]" +} + +array set rev {} +foreach {rid date pos fname frev default} [state run { + SELECT R.rid, R.date, C.pos, F.name, R.rev, R.isdefault + FROM csrevision C, revision R, file F + WHERE C.cid = $cid + AND R.rid = C.rid + AND F.fid = R.fid + ORDER BY C.pos, R.date +}] { + set rev($rid) [list $pos $date $fname $frev $default] + puts "File: [expr {$default?"D":" "}] [clock format $date] [format %3d $pos]/[format %6d $rid] ${frev}::$fname" +} + + +::vc::fossil::import::cvs::project::rev::PullPredecessorRevisions pdep [array names rev] +::vc::fossil::import::cvs::project::rev::PullSuccessorRevisions sdep [array names rev] + + + +puts "" +exit + + + + + + + + +exit diff --git a/tools/cvs2fossil/cvs2fossil b/tools/cvs2fossil/cvs2fossil new file mode 100644 index 0000000..968e442 --- /dev/null +++ b/tools/cvs2fossil/cvs2fossil @@ -0,0 +1,32 @@ +#!/bin/sh +## -*- tcl -*- \ +exec tclsh "$0" ${1+"$@"} + +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Command line application wrapped around the import packages. + +# # ## ### ##### ######## ############# ##################### +## Requirements, extended package management for local packages. + +lappend auto_path [file join [file dirname [info script]] lib] + +package require Tcl 8.4 ; # Required runtime. +package require vc::fossil::import::cvs ; # Main functionality. + +# # ## ### ##### ######## ############# ##################### +## Execution + +vc::fossil::import::cvs run $argv +exit 0 + +# # ## ### ##### ######## ############# ##################### diff --git a/tools/cvs2fossil/doc/LICENSE b/tools/cvs2fossil/doc/LICENSE new file mode 100644 index 0000000..2da6289 --- /dev/null +++ b/tools/cvs2fossil/doc/LICENSE @@ -0,0 +1,19 @@ +This code is under the same license as fossil itself. + +- - -- --- ----- --------- + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public +License version 2 as published by the Free Software Foundation. + +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. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public +License along with this library; if not, write to the +Free Software Foundation, Inc., 59 Temple Place - Suite 330, +Boston, MA 02111-1307, USA. + +- - -- --- ----- --------- diff --git a/tools/cvs2fossil/doc/README b/tools/cvs2fossil/doc/README new file mode 100644 index 0000000..74ccc84 --- /dev/null +++ b/tools/cvs2fossil/doc/README @@ -0,0 +1,7 @@ + +[Acknowledge the work done by the creators of and submitters to the +cvs2svn project/application. Needed their documentation, notes, and +code as guide for this implementation.] + +[Determine if their license allows me to copy their notes here for +reference.] diff --git a/tools/cvs2fossil/filerevtree b/tools/cvs2fossil/filerevtree new file mode 100644 index 0000000..7df6ae4 --- /dev/null +++ b/tools/cvs2fossil/filerevtree @@ -0,0 +1,178 @@ +#!/bin/sh +## -*- tcl -*- \ +exec tclsh "$0" ${1+"$@"} + +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Helper application, debugging of cvs2fossil. This application +## extracts the tree of revisions for a file of interest, specified +## either directly through its id, or indirectly through the id of a +## revision it contains, and generates a nice graphical representation +## of it (png image). It uses GraphiViz's 'dot' tool to do all the +## layouting. + +# # ## ### ##### ######## ############# ##################### +## Requirements, extended package management for local packages. + +lappend auto_path [file join [file dirname [info script]] lib] + +package require Tcl 8.4 ; # Required runtime. +package require struct::graph ; # Graph handling. +package require struct::list ; # Higher order list ops. +package require vc::fossil::import::cvs::project::rev ; # Changesets +package require vc::fossil::import::cvs::state ; # State storage. +package require vc::tools::misc ; # Min/max. +package require vc::tools::dot ; # Graph export to DOT. +package require vc::tools::trouble ; # Error reporting +package require vc::tools::log ; # User feedback + +namespace import ::vc::fossil::import::cvs::state +namespace import ::vc::fossil::import::cvs::project::rev +namespace import ::vc::tools::dot +namespace import ::vc::tools::trouble +namespace import ::vc::tools::log +namespace import ::vc::tools::misc::* + +log verbosity 0 + +# Process the command line, i.e. get the database to access, and file +# of interest. The latter can be specified by name, id, or indirectly +# through the id of one of the revisions it contains. + +state use [lindex $argv 0] +state reading project +state reading file +state reading revision +state reading revisionbranchchildren +state reading changeset +state reading csitem +state reading csorder + +set what [lindex $argv 1] +set centralrid -1 + +switch -exact -- $what { + rid { + # Get the revision of interest, identified by the internal + # numeric id used by cvs2fossil. + set centralrid [lindex $argv 2] + + puts "Revision : [state one { SELECT rev FROM revision WHERE rid = $centralrid }] ($centralrid)" + + # Map it to the file containing the revision of interest. + set fid [state one { SELECT fid FROM revision WHERE rid = $centralrid }] + } + fid { + # Get the file of interest, identified by internal numeric id + # used by cvs2fossil. + set fid [lindex $argv 2] + } + fname { + # Get the file of interest, identified by its name. + + set fname [lindex $argv 2] + set fid [state one { SELECT fid FROM file WHERE name == $fname }] + } + default { + trouble fatal \ + "Unknown spec \"$what\", expected one of \"fid\", \"fname\", or \"rid\"" + } +} +trouble abort? + +set pid [state one { SELECT pid FROM file WHERE fid == $fid }] + +puts "File : [state one { SELECT name FROM file WHERE fid = $fid }] ($fid)" +puts "Project : [state one { SELECT name FROM project WHERE pid = $pid }] ($pid)" + +# Get the data of all revisions in the file of interest, as a list for +# iteration, and as array for random access of neighbouring revisions. + +array set rev {} +foreach {rid revnr lod date isdefault} [set revisions [state run { + SELECT R.rid, R.rev, S.name, R.date, R.isdefault + FROM revision R, symbol S + WHERE R.fid = $fid + AND R.lod = S.sid +}]] { + set cs [state run { + SELECT CR.cid, CO.pos, CT.name + FROM csitem CR, csorder CO, cstype CT, changeset C + WHERE CR.iid = $rid + AND CR.cid = CO.cid + AND CR.cid = C.cid + AND CT.tid = C.type + }] + + set rev($rid) [list $revnr $lod $date $isdefault $cs] +} + +puts "#Revs : [array size rev]" + +# Start the graph + +struct::graph dg + +# Convert the revisions into nodes of the graph, and use node +# attributes to highlight various pieces of interest for the dot +# conversion. Label => Revnr, Symbol (LOD), Changeset id (if +# available), formatted date. Background fill colors to show the +# different branches ?. + +foreach {rid revnr lod date isdefault} $revisions { + set label "$rid = <$revnr> @ $lod / [clock format $date]" + set cs [lindex $rev($rid) 4] + if {[llength $cs]} { + foreach {cs ord cstype} $cs { + append label "\\nCS/${cstype}($cs) @$ord" + } + } + set key [list rev $rid] + dg node insert $key + dg node set $key label $label + dg node set $key shape [expr {$isdefault ? "diamond" : "box"}] +} + +# Go through the revisions a second time and set up the arcs based on +# the stored successor information. + +::vc::fossil::import::cvs::project::rev::rev successors dep [array names rev] + +proc Ord {cmd rid} {return 0 + global rev + set ords {} + foreach {cs ord cstype} [lindex $rev($rid) 4] { lappend ords $ord } + return [$cmd $ords] +} + +foreach {rid children} [array get dep] { + set max [Ord max $rid] + + foreach child $children { + if {[catch { + set a [dg arc insert $rid $child] + }]} continue + + if {$max > [Ord min $child]} { + puts "Backward : $rid -> $child" + dg arc set $a color red + } + } +} + +# Convert the graph to dot, then run the layouter and convert to png, +# at last show the image. + +vc::tools::dot layout png dg SymbolTree st.png +exec display st.png +file delete st.png +exit diff --git a/tools/cvs2fossil/getmemoryseries.tcl b/tools/cvs2fossil/getmemoryseries.tcl new file mode 100644 index 0000000..bef7e9f --- /dev/null +++ b/tools/cvs2fossil/getmemoryseries.tcl @@ -0,0 +1,69 @@ +#!/bin/bash +# -*- tcl -*- \ +exec tclsh "$0" ${1+"$@"} + +package require csv +foreach {in outbasic outmarker plot outbasicold} $argv break + +set in [open $in r] +set ba [open $outbasic w] +set mr [open $outmarker w] + +puts $ba "\# Time Memory MaxMemory" +puts $mr "\# Time Memory" + +set k 0 +while {![eof $in]} { + gets $in line + #puts -nonewline \r[incr k] + + if {[string match *|=|* $line]} { + # Basic series + regexp {^(.*)\|=\|} $line -> line + foreach {x _ cba _ _ _ mba} $line break + puts $ba [join [list $x $cba $mba] \t] + continue + } + + if {[string match *|@|* $line]} { + # Marker series + regexp {^(.*)\|@\|} $line -> line + foreach {x _ cba} $line break + puts $mr [join [list $x $cba] \t] + continue + } +} + +puts "" +close $in +close $ba +close $mr + +# Generate gnuplot control file for the series +set f [open $plot w] +puts $f "" +puts $f "plot \"$outbasic\" using 1:2 title 'Memory' with steps, \\" +puts $f " \"$outbasic\" using 1:3 title 'Max Memory' with steps" +puts $f "pause -1" +puts $f "" +close $f + +# Generate gnuplot control file for comparison of series +set f [open ${plot}-compare w] +puts $f "" +puts $f "plot \"$outbasicold\" using 1:2 title 'Memory Old' with steps, \\" +puts $f " \"$outbasic\" using 1:2 title 'Memory New' with steps" +puts $f "pause -1" +puts $f "" +close $f +exit + +# Comparison to baseline +plot "basic.dat" using 1:2 title 'Memory Base' with steps lt rgb "blue", \ + "newbasic.dat" using 1:2 title 'Memory Current' with steps lt rgb "red", \ + +# Comparison to baseline via normalization - need math op (div) +plot "basic.dat" using 1:2 title 'Memory Base' with steps lt rgb "blue", \ + "newbasic.dat" using 1:2 title 'Memory Current' with steps lt rgb "red", \ + + diff --git a/tools/cvs2fossil/lib/c2f_blobstore.tcl b/tools/cvs2fossil/lib/c2f_blobstore.tcl new file mode 100644 index 0000000..824adeb --- /dev/null +++ b/tools/cvs2fossil/lib/c2f_blobstore.tcl @@ -0,0 +1,155 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2008 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Blob storage. Each instance stores the blob data of a single rcs +## archive file, i.e. which file, all text ranges, delta dependencies, +## and associated revisions (as object references). The data is +## persistent and used by the import pass(es) to expand the revisions +## of a file. + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime. +package require snit ; # OO system. +package require vc::fossil::import::cvs::state ; # State storage. +package require vc::fossil::import::cvs::integrity ; # State integrity checks. +package require vc::tools::trouble ; # Error reporting. +package require vc::tools::log ; # User feedback +#package require vc::tools::misc ; # Text formatting + +# # ## ### ##### ######## ############# ##################### +## + +snit::type ::vc::fossil::import::cvs::blobstore { + # # ## ### ##### ######## ############# + ## Public API + + constructor {fid} { + set myfile $fid + array set myparent {} + array set myblob {} + return + } + + method setid {id} { + integrity assert {$myfile eq ""} {Already has an id, '$myfile'} + set myfile $id + return + } + + # Remember the file revision object for the revision REVNR. + + method add {revnr rev} { + set myblob($revnr) $rev + return + } + + # Remember that the DELTA revision is specified as a delta against + # the BASE revision. Both are specified as revision numbers. + + method delta {delta base} { + set myparent($delta) $base + return + } + + # Specify the text range in the archive file for the data of the + # revision identified by REVNR. + + method extend {revnr textrange} { + struct::list assign $textrange coff end + set clen [expr {$end - $coff}] + lappend myblob($revnr) $coff $clen + return + } + + # Write the stored information into the persistent state. + + method persist {} { + array set bids {} + state transaction { + # Phase I: Store the basic blob information. + + foreach revnr [lsort [array names myblob]] { + struct::list assign $myblob($revnr) rev coff clen + state run { + INSERT INTO blob (bid, rid, fid, coff, clen, pid) + VALUES (NULL, NULL, $myfile, $coff, $clen, NULL) + } + set current [state id] + set bids($revnr) $current + + # Ia. Set the reference to the revision of the blob, + # if applicable. We can have blobs without revisions, + # their revisions were removed as irrelevant. We need + # them however for the proper delta ordering and patch + # application when expanding a file (-> Import passes). + + set rid [$rev id] + if {$rid eq ""} continue + state run { + UPDATE blob + SET rid = $rid + WHERE bid = $current + } + } + + # Phase II: Set the parent links for deltas. + foreach revnr [array names myparent] { + set bid $bids($revnr) + set pid $bids($myparent($revnr)) + + state run { + UPDATE blob + SET pid = $pid + WHERE bid = $bid + } + } + } + return + } + + # # ## ### ##### ######## ############# + ## State + + variable myfile {} ; # Id of the file the blobs belong to. + variable myparent -array {} ; # Map delta-encoded revision numbers + # to their baseline revisions. + variable myblob -array {} ; # Map revision numbers to associated + # file revision object and text + # range. + + # # ## ### ##### ######## ############# + ## Configuration + + pragma -hastypeinfo no ; # no type introspection + pragma -hasinfo no ; # no object introspection + pragma -hastypemethods no ; # type is not relevant. + + # # ## ### ##### ######## ############# +} + +namespace eval ::vc::fossil::import::cvs { + namespace export blobstore + namespace eval blobstore { + namespace import ::vc::tools::trouble + namespace import ::vc::tools::log + namespace import ::vc::fossil::import::cvs::state + namespace import ::vc::fossil::import::cvs::integrity + } +} + +# # ## ### ##### ######## ############# ##################### +## Ready + +package provide vc::fossil::import::cvs::blobstore 1.0 +return diff --git a/tools/cvs2fossil/lib/c2f_cyclebreaker.tcl b/tools/cvs2fossil/lib/c2f_cyclebreaker.tcl new file mode 100644 index 0000000..3d8a21c --- /dev/null +++ b/tools/cvs2fossil/lib/c2f_cyclebreaker.tcl @@ -0,0 +1,579 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## This file provides a helper package for the passes 6 and 7 which +## contains the common code of the cycle breaking algorithm. + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime. +package require snit ; # OO system. +package require struct::graph ; # Graph handling. +package require struct::list ; # Higher order list operations. +package require vc::tools::dot ; # User feedback. DOT export. +package require vc::tools::log ; # User feedback. +package require vc::tools::trouble ; # Error reporting. +package require vc::tools::misc ; # Text formatting. +package require vc::fossil::import::cvs::project::rev ; # Project level changesets +package require vc::fossil::import::cvs::project::revlink ; # Cycle links. +package require vc::fossil::import::cvs::integrity ; # State integrity checks. + +# # ## ### ##### ######## ############# ##################### +## + +snit::type ::vc::fossil::import::cvs::cyclebreaker { + # # ## ### ##### ######## ############# + ## Public API + + typemethod precmd {cmd} { + ::variable myprecmd $cmd + return + } + + typemethod savecmd {cmd} { + ::variable mysavecmd $cmd + return + } + + typemethod breakcmd {cmd} { + ::variable mybreakcmd $cmd + return + } + + # # ## ### ##### ######## ############# + + typemethod dotsto {path} { + ::variable mydotdestination $path + return + } + + typemethod watch {id} { + ::variable mywatchids + lappend mywatchids $id + } + + typemethod dot {label changesets} { + ::variable mydotprefix $label + ::variable mydotid 0 + + set dg [Setup $changesets 0] + Mark $dg + $dg destroy + return + } + + typemethod mark {graph suffix {subgraph {}}} { + Mark $graph $suffix $subgraph + return + } + + # # ## ### ##### ######## ############# + + typemethod run {label changesetcmd} { + ::variable myat 0 + ::variable mydotprefix $label + ::variable mydotid 0 + + # We create a graph of the revision changesets, using the file + # level dependencies to construct a first approximation of the + # dependencies at the project level. Then we look for cycles + # in that graph and break them. + + # 1. Create nodes for all relevant changesets and a mapping + # from the revisions to their changesets/nodes. + + set changesets [uplevel #0 $changesetcmd] + set dg [Setup $changesets] + + # 3. Lastly we iterate the graph topologically. We mark off + # the nodes which have no predecessors, in order from + # oldest to youngest, saving and removing dependencies. If + # we find no nodes without predecessors we have a cycle, + # and work on breaking it. + + log write 3 cyclebreaker {Traverse changesets} + + InitializeCandidates $dg + + set k 0 + set max [llength [$dg nodes]] + while {1} { + while {[WithoutPredecessor $dg n]} { + log progress 2 cyclebreaker $k $max ; incr k + MarkWatch $dg + ProcessedHook $dg $n $myat + $dg node delete $n + incr myat + ShowPendingNodes + } + + if {![llength [$dg nodes]]} break + + BreakCycleHook $dg + InitializeCandidates $dg + MarkWatch $dg + } + + $dg destroy + + log write 3 cyclebreaker Done. + ClearHooks + + # Reread the graph and dump its final form, if graph export + # was activated. + + ::variable mydotdestination + if {$mydotdestination eq ""} return + + set dg [Setup [uplevel #0 $changesetcmd] 0] + Mark $dg -done + $dg destroy + return + } + + # # ## ### ##### ######## ############# + + typemethod break-segment {graph} { + BreakSegment $graph $path "segment ([project::rev strlist $path])" + return + } + + typemethod break {graph} { + set cycle [FindCycle $graph] + set label "cycle ([project::rev strlist $cycle])" + + # NOTE: cvs2svn uses the sequence "end-1, cycle, 0" to create + # the path from the cycle. The only effect I can see is + # that this causes the link-triples to be generated in a + # sightly different order, i.e. one link rotated to the + # right. This should have no effect on the search for + # the best of all. + + lappend cycle [lindex $cycle 0] [lindex $cycle 1] + BreakSegment $graph $cycle $label + return + } + + typemethod replace {graph n replacements} { + Replace $graph $n $replacements + return + } + + # # ## ### ##### ######## ############# + ## Internal methods + + proc Setup {changesets {log 1}} { + if {$log} { + log write 3 cyclebreaker "Creating graph of changesets" + } + + set dg [struct::graph dg] + + set n 0 + set max [llength $changesets] + + foreach cset $changesets { + log progress 2 cyclebreaker $n $max + set tr [$cset timerange] + $dg node insert $cset + $dg node set $cset timerange $tr + $dg node set $cset label "[$cset str]\\n[join [struct::list map $tr {::clock format}] "\\n"]" + $dg node set $cset __id__ [$cset id] + $dg node set $cset shape [expr {[$cset bysymbol] + ? "ellipse" + : "box"}] + incr n + } + + if {$log} { + log write 3 cyclebreaker "Has [nsp [llength $changesets] changeset]" + } + + # 2. Find for all relevant changeset their revisions and their + # dependencies. Map the latter back to changesets and + # construct the corresponding arcs. + + set n 0 + foreach cset $changesets { + log progress 2 cyclebreaker $n $max + foreach succ [$cset successors] { + # Changesets may have dependencies outside of the + # chosen set. These are ignored + if {![$dg node exists $succ]} continue + $dg arc insert $cset $succ + integrity assert { + $succ ne $cset + } {[$cset reportloop 0]Changeset loop was not detected during creation} + } + incr n + } + + if {$log} { + log write 3 cyclebreaker "Has [nsp [llength [$dg arcs]] dependency dependencies]" + } + + # Run the user hook to manipulate the graph before + # consummation. + + if {$log} { Mark $dg -start } + MarkWatch $dg + PreHook $dg + MarkWatch $dg + return $dg + } + + # Instead of searching the whole graph for the degree-0 nodes in + # each iteration we compute the list once to start, and then only + # update it incrementally based on the outgoing neighbours of the + # node chosen for commit. + + proc InitializeCandidates {dg} { + # bottom = list (list (node, range min, range max)) + ::variable mybottom + foreach n [$dg nodes] { + if {[$dg node degree -in $n]} continue + lappend mybottom [linsert [$dg node get $n timerange] 0 $n] + } + ScheduleCandidates + ShowPendingNodes + return + } + + proc WithoutPredecessor {dg nv} { + ::variable mybottom + + upvar 1 $nv n + if {![llength $mybottom]} { return 0 } + + set n [lindex [lindex $mybottom 0] 0] + set mybottom [lrange $mybottom 1 end] + set changed 0 + + # Update list of nodes without predecessor, based on the + # outgoing neighbours of the chosen node. This should be + # faster than iterating of the whole set of nodes, finding all + # without predecessors, sorting them by time, etc. pp. + foreach out [$dg nodes -out $n] { + if {[$dg node degree -in $out] > 1} continue + # Degree-1 neighbour, will have no predecessors after the + # removal of n. Put on the list. + lappend mybottom [linsert [$dg node get $out timerange] 0 $out] + set changed 1 + } + if {$changed} { + ScheduleCandidates + } + + # We do not delete the node immediately, to allow the Save + # procedure to save the dependencies as well (encoded in the + # arcs). + return 1 + } + + proc ScheduleCandidates {} { + ::variable mybottom + # Sort by cset object name, lower border of timerange, at last + # by the upper border. + set mybottom [lsort -index 2 -integer [lsort -index 1 -integer [lsort -index 0 -dict $mybottom]]] + return + } + + proc ShowPendingNodes {} { + if {[log verbosity?] < 10} return + ::variable mybottom + log write 10 cyclebreaker "Pending..............................." + foreach item [struct::list map $mybottom [myproc FormatPendingItem]] { + log write 10 cyclebreaker "Pending: $item" + } + return + } + + proc FormatPendingItem {item} { + join [list [[lindex $item 0] str] [clock format [lindex $item 1]] [clock format [lindex $item 2]]] + } + + proc FindCycle {dg} { + # This procedure is run if and only the graph is not empty and + # all nodes have predecessors. This means that each node is + # either part of a cycle or (indirectly) depending on a node + # in a cycle. We can start at an arbitrary node, follow its + # incoming edges to its predecessors until we see a node a + # second time. That node closes the cycle and the beginning is + # its first occurence. Note that we can choose an arbitrary + # predecessor of each node as well, we do not have to search. + + # We record for each node the index of the first appearance in + # the path, making it easy at the end to cut the cycle from + # it. + + # Choose arbitrary node to start our search at. + set start [lindex [$dg nodes] 0] + + # Initialize state, path of seen nodes, and when seen. + set path {} + array set seen {} + + while {1} { + # Stop searching when we have seen the current node + # already, the circle has been closed. + if {[info exists seen($start)]} break + lappend path $start + set seen($start) [expr {[llength $path]-1}] + # Choose arbitrary predecessor + set start [lindex [$dg nodes -in $start] 0] + } + + return [struct::list reverse [lrange $path $seen($start) end]] + } + + proc BreakSegment {dg path label} { + # The path, usually a cycle, we have gotten is broken by + # breaking apart one or more of the changesets in the + # cycle. This causes us to create one or more changesets which + # are to be committed, added to the graph, etc. pp. + + set bestlink {} + set bestnode {} + + foreach \ + prev [lrange $path 0 end-2] \ + cset [lrange $path 1 end-1] \ + next [lrange $path 2 end] { + + # Each triple PREV -> CSET -> NEXT of changesets, a + # 'link' in the cycle, is analysed and the best + # location where to at least weaken the cycle is + # chosen for further processing. + + set link [project::revlink %AUTO% $prev $cset $next] + if {$bestlink eq ""} { + set bestlink $link + set bestnode $cset + } elseif {[$link betterthan $bestlink]} { + $bestlink destroy + set bestlink $link + set bestnode $cset + } else { + $link destroy + } + } + + log write 5 cyclebreaker "Breaking $label by splitting changeset [$bestnode str]" + set ID [$bestnode id] + Mark $dg -${ID}-before + + set newcsets [$bestlink break] + $bestlink destroy + + # At this point the old changeset (BESTNODE) is gone + # already. We remove it from the graph as well and then enter + # the fragments generated for it. + + Replace $dg $bestnode $newcsets + + Mark $dg -${ID}-after + return + } + + # TODO: This should be a graph method. + proc HasArc {dg a b} { + #8.5: return [expr {$b in [$dg nodes -out $a]}] + if {[lsearch -exact [$dg nodes -out $a] $b] < 0} { return 0 } + return 1 + } + + proc Mark {dg {suffix {}} {subgraph {}}} { + ::variable mydotdestination + if {$mydotdestination eq ""} return + ::variable mydotprefix + ::variable mydotid + set fname $mydotdestination/${mydotprefix}${mydotid}${suffix}.dot + file mkdir [file dirname $fname] + dot write $dg $mydotprefix$suffix $fname $subgraph + incr mydotid + + log write 5 cyclebreaker ".dot export $fname" + return + } + + proc Replace {dg n replacements} { + # NOTE. We have to get the list of incoming neighbours and + # recompute their successors after the new nodes have been + # inserted. Their outgoing arcs will now go to one or both of + # the new nodes, and not redoing them may cause us to forget + # circles, leaving them in, unbroken. + + set pre [$dg nodes -in $n] + + $dg node delete $n + + foreach cset $replacements { + set tr [$cset timerange] + $dg node insert $cset + $dg node set $cset timerange $tr + $dg node set $cset label "[$cset str]\\n[join [struct::list map $tr {::clock format}] "\\n"]" + $dg node set $cset __id__ [$cset id] + $dg node set $cset shape [expr {[$cset bysymbol] + ? "ellipse" + : "box"}] + } + + foreach cset $replacements { + foreach succ [$cset successors] { + # The new changesets may have dependencies outside of + # the chosen set. These are ignored + if {![$dg node exists $succ]} continue + $dg arc insert $cset $succ + integrity assert { + $succ ne $cset + } {[$cset reportloop 0]Changeset loop was not detected during creation} + } + } + foreach cset $pre { + foreach succ [$cset successors] { + # Note that the arc may already exist in the graph. If + # so ignore it. The new changesets may have + # dependencies outside of the chosen set. These are + # ignored + if {![$dg node exists $succ]} continue + if {[HasArc $dg $cset $succ]} continue;# TODO should be graph method. + $dg arc insert $cset $succ + } + } + + return + } + + # # ## ### ##### ######## ############# + ## Callback invokation ... + + proc PreHook {graph} { + # Give the user of the cycle breaker the opportunity to work + # with the graph between setup and consummation. + + ::variable myprecmd + if {![llength $myprecmd]} return + + uplevel #0 [linsert $myprecmd end $graph] + Mark $graph -pre-done + return + } + + proc ProcessedHook {dg cset pos} { + # Give the user of the cycle breaker the opportunity to work + # with the changeset before it is removed from the graph. + + ::variable mysavecmd + if {![llength $mysavecmd]} return + + uplevel #0 [linsert $mysavecmd end $dg $pos $cset] + return + } + + proc BreakCycleHook {graph} { + # Call out to the chosen algorithm for cycle breaking. Finding + # a cycle if no breaker was chosen is an error. + + ::variable mybreakcmd + if {![llength $mybreakcmd]} { + trouble fatal "Found a cycle, expecting none." + exit 1 + } + + uplevel #0 [linsert $mybreakcmd end $graph] + return + } + + proc ClearHooks {} { + ::variable myprecmd {} + ::variable mysavecmd {} + ::variable mybreakcmd {} + return + } + + # # ## ### ##### ######## ############# + + proc MarkWatch {graph} { + ::variable mywatchids + set watched [Watched $graph $mywatchids] + if {![llength $watched]} return + set neighbours [eval [linsert $watched 0 $graph nodes -adj]] + #foreach n $neighbours { log write 6 cyclebreaker "Neighbor [$n id] => $n" } + Mark $graph watched [concat $watched $neighbours] + return + } + + proc Watched {graph watchids} { + set res {} + foreach id $watchids { + set nl [$graph nodes -key __id__ -value $id] + if {![llength $nl]} continue + lappend res $nl + #log write 6 breakrcycle "Watching $id => $nl" + $graph node set $nl fontcolor red + } + return $res + } + + # # ## ### ##### ######## ############# + + + typevariable myat 0 ; # Counter for commit ids for the + # changesets. + typevariable mybottom {} ; # List of the candidate nodes for + # committing. + + typevariable myprecmd {} ; # Callback, change graph before walk. + typevariable mysavecmd {} ; # Callback, for each processed node. + typevariable mybreakcmd {} ; # Callback, for each found cycle. + + typevariable mydotdestination {} ; # Destination directory for the + # generated .dot files. + typevariable mydotprefix {} ; # Prefix for dot files when + # exporting the graphs. + typevariable mydotid 0 ; # Counter for dot file name + # generation. + typevariable mywatchids {} ; # Changesets to watch the + # neighbourhood of. + + # # ## ### ##### ######## ############# + ## Configuration + + pragma -hasinstances no ; # singleton + pragma -hastypeinfo no ; # no introspection + pragma -hastypedestroy no ; # immortal + + # # ## ### ##### ######## ############# +} + +namespace eval ::vc::fossil::import::cvs { + namespace export cyclebreaker + namespace eval cyclebreaker { + namespace import ::vc::fossil::import::cvs::integrity + namespace eval project { + namespace import ::vc::fossil::import::cvs::project::rev + namespace import ::vc::fossil::import::cvs::project::revlink + } + namespace import ::vc::tools::misc::* + namespace import ::vc::tools::log + namespace import ::vc::tools::trouble + namespace import ::vc::tools::dot + log register cyclebreaker + } +} + +# # ## ### ##### ######## ############# ##################### +## Ready + +package provide vc::fossil::import::cvs::cyclebreaker 1.0 +return diff --git a/tools/cvs2fossil/lib/c2f_file.tcl b/tools/cvs2fossil/lib/c2f_file.tcl new file mode 100644 index 0000000..a113698 --- /dev/null +++ b/tools/cvs2fossil/lib/c2f_file.tcl @@ -0,0 +1,1452 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007-2008 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## File, part of a project, part of a CVS repository. Multiple +## instances are possible. + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime. +package require snit ; # OO system. +package require struct::set ; # Set operations. +package require struct::list ; # Higher order operations. +package require vc::fossil::import::cvs::blobstore ; # Blob storage. +package require vc::fossil::import::cvs::file::rev ; # CVS per file revisions. +package require vc::fossil::import::cvs::file::sym ; # CVS per file symbols. +package require vc::fossil::import::cvs::state ; # State storage. +package require vc::fossil::import::cvs::integrity ; # State integrity checks. +package require vc::fossil::import::cvs::gtcore ; # Graph traversal core. +package require vc::tools::trouble ; # Error reporting. +package require vc::tools::log ; # User feedback +package require vc::tools::misc ; # Text formatting + +# # ## ### ##### ######## ############# ##################### +## + +snit::type ::vc::fossil::import::cvs::file { + # # ## ### ##### ######## ############# + ## Public API + + constructor {id path usrpath executable project} { + set myid $id + set mypath $path + set myusrpath $usrpath + set myexecutable $executable + set myproject $project + set mytrunk [$myproject trunk] + set myblob [blobstore ${selfns}::%AUTO% $id] + return + } + + method setid {id} { + integrity assert {$myid eq ""} {File '$mypath' already has an id, '$myid'} + set myid $id + $myblob setid $id + return + } + + method id {} { return $myid } + method path {} { return $mypath } + method usrpath {} { return $myusrpath } + method project {} { return $myproject } + + delegate method commitmessageof to myproject + + # # ## ### ##### ######## ############# + ## Methods required for the class to be a sink of the rcs parser + + #method begin {} {puts begin} + #method sethead {h} {puts head=$h} + #method setprincipalbranch {b} {puts pb=$b} + #method deftag {s r} {puts $s=$r} + #method setcomment {c} {puts comment=$c} + #method admindone {} {puts admindone} + #method def {rev date author state next branches} {puts "def $rev $date $author $state $next $branches"} + #method defdone {} {puts def-done} + #method setdesc {d} {puts desc=$d} + #method extend {rev commitmsg deltarange} {puts "extend $commitmsg $deltarange"} + #method done {} {puts done} + + # # ## ### ##### ######## ############# + ## Persistence (pass II) + + method persist {} { + # First collect the reachable revisions and symbols, then + # assign id's to all. They are sorted so that we will have ids + # which sort in order of creation. Then we can save them. This + # is done bottom up. Revisions, then symbols. __NOTE__ This + # works only because sqlite is not checking foreign key + # references during insert. This allows to have dangling + # references which are fixed later. The longest dangling + # references are for the project level symbols, these we do + # not save here, but at the end of the pass. What we need are + # the ids, hence the two phases. + + struct::list assign [$self Active] revisions symbols + foreach rev $revisions { $rev defid } + foreach sym $symbols { $sym defid } + + state transaction { + foreach rev $revisions { $rev persist } + foreach sym $symbols { $sym persist } + $myblob persist + } + return + } + + method drop {} { + foreach {_ rev} [array get myrev] { $rev destroy } + foreach {_ branch} [array get mybranches] { $branch destroy } + foreach {_ taglist} [array get mytags] { + foreach tag $taglist { $tag destroy } + } + return + } + + # # ## ### ##### ######## ############# + ## Implement the sink + + method begin {} {#ignore} + + method sethead {revnr} { + set myheadrevnr $revnr + return + } + + method setprincipalbranch {branchnr} { + set myprincipal $branchnr + return + } + + method deftag {name revnr} { + # FUTURE: Perform symbol transformation here. + + if {[struct::set contains $mysymbols $name]} { + trouble fatal "Multiple definitions of the symbol '$name' in '$mypath'" + return + } + + struct::set add mysymbols $name + + if {[rev isbranchrevnr $revnr -> branchnr]} { + $self AddBranch $name $branchnr + } else { + $self AddTag $name $revnr + } + return + } + + method setcomment {c} {# ignore} + + method admindone {} { + # We do nothing at the boundary of admin and revision data + } + + method def {revnr date author state next branches} { + $self RecordBranchCommits $branches + + if {[info exists myrev($revnr)]} { + trouble fatal "File $mypath contains duplicate definitions for revision $revnr." + return + } + + set myaid($revnr) [$myproject defauthor $author] + set myrev($revnr) [rev %AUTO% $revnr $date $state $self] + + $myblob add $revnr $myrev($revnr) + + if {$next ne ""} { + # parent revision NEXT is a delta of current. + $myblob delta $next $revnr + } + foreach b $branches { + # branch child revision B is a delta of current. + $myblob delta $b $revnr + } + + $self RecordBasicDependencies $revnr $next + return + } + + method defdone {} { + # This is all done after the revision tree has been extracted + # from the file, before the commit mesages and delta texts are + # processed. + + $self ProcessPrimaryDependencies + $self ProcessBranchDependencies + $self SortBranches + $self ProcessTagDependencies + $self DetermineTheRootRevision + return + } + + method setdesc {d} {# ignore} + + method extend {revnr commitmsg textrange} { + set cmid [$myproject defcmessage [string trim $commitmsg]] + + set rev $myrev($revnr) + + if {[$rev hasmeta]} { + # Apparently repositories exist in which the delta data + # for revision 1.1 is provided several times, at least + # twice. The actual cause of this duplication is not + # known. Speculation centers on RCS/CVS bugs, or from + # manual edits of the repository which borked the + # internals. Whatever the cause, testing showed that both + # cvs and rcs use the first definition when performing a + # checkout, and we follow their lead. Side notes: 'cvs + # log' fails on such a file, and 'cvs rlog' prints the log + # message from the first delta, ignoring the second. + + log write 1 file "In file $mypath : Duplicate delta data for revision $revnr" + log write 1 file "Ignoring the duplicate" + return + } + + # Determine the line of development for the revision (project + # level). This gives us the branchid too, required for the + # meta data group the revision is in. (Note: By putting both + # branch/lod and project information into the group we ensure + # that any cross-project and cross-branch commits are + # separated into multiple commits, one in each of the projects + # and/or branches). + + set lod [$self GetLOD $revnr] + + $rev setmeta [$myproject defmeta [$lod id] $myaid($revnr) $cmid] + # Note: We keep settext on file::rev for the hastext queries + # used by several checks. + $rev settext $textrange + $rev setlod $lod + + $myblob extend $revnr $textrange + + # If this is revision 1.1, we have to determine whether the + # file seems to have been created through 'cvs add' instead of + # 'cvs import'. This can be done by looking at the un- + # adulterated commit message, as CVS uses a hardwired magic + # message for the latter, i.e. "Initial revision\n", no + # period. (This fact also helps us when the time comes to + # determine whether this file might have had a default branch + # in the past.) + + if {$revnr eq "1.1"} { + set myimported [expr {$commitmsg eq "Initial revision\n"}] + } + + # Here we also keep track of the order in which the revisions + # were added to the file. + + lappend myrevisions $rev + return + } + + method done {} { + # Complete the revisions, branches, and tags. This includes + # looking for a non-trunk default branch, marking its members + # and linking them into the trunk, possibly excluding + # non-trunk data, and collecting aggregate symbol statistics. + + $self DetermineRevisionOperations + $self DetermineLinesOfDevelopment + $self HandleNonTrunkDefaultBranch + $self RemoveIrrelevantDeletions + $self RemoveInitialBranchDeletions + + if {[$myproject trunkonly]} { + $self ExcludeNonTrunkInformation + } + + $self AggregateSymbolData + return + } + + # # ## ### ##### ######## ############# + ## Pass XII (Import). + + method pushto {repository} { + log write 2 file {Importing file "$mypath"} + + set ws [$repository workspace] + struct::list assign [$self Expand $ws] filemap revmap + # filemap = dict (path -> uuid) + # revmap = dict (path -> rid) + + array set idmap [$repository importfiles $filemap] + + # Wipe workspace clean of the imported files. + foreach x [glob -directory $ws r*] { ::file delete $x } + + foreach {path rid} $revmap { + set uuid $idmap($path) + state run { + INSERT INTO revuuid (rid, uuid) + VALUES ($rid, $uuid) + } + } + return + } + + method Expand {dir} { + set ex [struct::graph ex] ; # Expansion graph. + set zp [struct::graph zp] ; # Zip/Import graph. + + close [open $dir/r__empty__ w];# Base for detached roots on branches. + + # Phase I: Pull blobs and referenced revisions from the state + # and fill the graphs with them... + + # Note: We use the blobs for expansion because we need them + # all, even those without revision, for both proper + # ordering and exact patch application. + + set earcs {} ; # Arcs for expansion graph + set zarcs {} ; # Arcs for zip graph + set revmap {} ; # path -> rid map to later merge uuid information + + state foreachrow { + SELECT B.rid AS xrid, + R.rev AS revnr, + R.child AS xchild, + B.coff AS xcoff, + B.clen AS xclen, + B.bid AS cid, + B.pid AS cparent + FROM blob B + LEFT OUTER JOIN revision R + ON B.rid = R.rid + WHERE B.fid = $myid + } { + # Main data are blobs, most will have revisions, but not + # all. The expansion graph is blob based, whereas the + # recompression graph is revision based. + + if {$revnr ne ""} { + # Blob has revision, extend recompression graph. + + lappend revmap r$revnr $xrid + + $zp node insert $xrid + $zp node set $xrid revnr $revnr + $zp node set $xrid label <$revnr> + + if {$xchild ne ""} { + lappend zarcs $xchild $xrid + } + } else { + # We fake a revnr for the blobs which have no + # revision, for use in the expansion graph. + set revnr ghost$cid + } + + # Now the expansion graph. + + $ex node insert $cid + $ex node set $cid text [list $xcoff $xclen] + $ex node set $cid revnr $revnr + $ex node set $cid label <$revnr> + + if {$cparent ne ""} { + # The expansion arcs go from baseline to delta + # descendant, based on the blob information. + lappend earcs $cparent $cid + } + } + + # Phase II: Insert the accumulated dependencies + + foreach {from to} $earcs { $ex arc insert $from $to } + foreach {from to} $zarcs { $zp arc insert $from $to } + + # Phase III: Traverse the graphs, expand the file, and + # generate import instructions. + + set archive [::file join [$myproject fullpath] $mypath] + set ac [open $archive r] + fconfigure $ac -translation binary + + # First traverse the expansion graph, this gives us the + # revisions in the order we have to expand them, which we do. + + set max [llength [$ex nodes]] + set myimport 0 + + gtcore datacmd [mymethod ExpandData] + gtcore formatcmd [mymethod ExpandFormat] + gtcore sortcmd [mymethod ExpandSort] + gtcore savecmd [mymethod Expand1 $ac $dir $max] + + gtcore traverse $ex {Expanding revisions...} + close $ac + + # Now traverse the import graph, this builds the instruction + # map for the fossil deltas. + + gtcore datacmd [mymethod ExpandData] + gtcore formatcmd [mymethod ExpandFormat] + gtcore sortcmd [mymethod ExpandSort] + gtcore savecmd [mymethod Expand2] + + set myimport {} + gtcore traverse $zp {Generating revision order for import...} + set filemap $myimport + unset myimport + + # And back to import control + + $ex destroy + $zp destroy + + return [list $filemap $revmap] + } + + method ExpandData {graph node} { return [$graph node get $node revnr] } + method ExpandFormat {graph item} { return <[lindex $item 1]> } ; # revnr + method ExpandSort {graph candidates} { + # candidates = list(item), item = list(node revnr) + # Sort by node and revnr -> Trunk revisions come first. + return [lsort -index 1 -dict [lsort -index 0 -dict $candidates]] + } + method Expand1 {chan dir max graph node} { + log progress 3 file $myimport $max ; incr myimport + + set revnr [$graph node get $node revnr] + set fname r$revnr + struct::list assign [$graph node get $node text] offset length + + if {$length < 0} { + set data "" + } else { + seek $chan $offset start + set data [string map {@@ @} [read $chan $length]] + } + + if {![$graph node keyexists $node __base__]} { + # Full text node. Get the data, decode it, and save. + + log write 8 file {Expanding <$revnr>, full text} + + fileutil::writeFile -translation binary $dir/$fname $data + } else { + # Delta node. __base__ is the name of the file containing + # the baseline. The patch is at the specified location of + # the archive file. + + set fbase [$graph node get $node __base__] + log write 8 file {Expanding <$revnr>, is delta of <$fbase>} + + set base [fileutil::cat -translation binary $dir/$fbase] + + # Writing the patch to disk is just for better + # debugging. It is not used otherwise. + fileutil::writeFile $dir/rpatch $data + fileutil::writeFile -translation binary $dir/$fname \ + [Apply $base $data] + } + + # Post to all successors that the just generated file is their + # baseline. + + foreach out [$graph nodes -out $node] { + $graph node set $out __base__ $fname + } + return + } + + proc Apply {base delta} { + # base = base text. + # delta = delta in rcs format. + # + # Both strings are unencoded, i.e. things like @@, etc. have + # already been replaced with their proper characters. + # + # Return value is the patched text. + + set base [split $base \n] + set blen [llength $base] + set ooff 0 + set res "" + + set lines [split $delta \n] + set nlines [llength $lines] + + log write 11 file {Base lines = $blen} + log write 11 file {Delta lines = $nlines} + + for {set i 0} {$i < $nlines} {} { + log write 11 file { @ $i = [lindex $lines $i]} + log write 11 file { ooff $ooff} + + if {![regexp {^([ad])(\d+)\s(\d+)$} [lindex $lines $i] -> cmd sl cn]} { + trouble internal "Bad ed command '[lindex $lines $i]'" + } + + log write 11 file { cmd $cmd} + log write 11 file { sl $sl} + log write 11 file { cn $cn} + + incr i + set el [expr {$sl + $cn}] + + log write 11 file { el $el} + + switch -exact -- $cmd { + d { + incr sl -1 + incr el -1 + if {$sl < $ooff} { trouble internal {Deletion before last edit} } + if {$sl > $blen} { trouble internal {Deletion past file end} } + if {$el > $blen} { trouble internal {Deletion beyond file end} } + foreach x [lrange $base $ooff [expr {$sl - 1}]] { + log write 15 file {.|$x|} + lappend res $x + } + set ooff $el + } + a { + if {$sl < $ooff} { trouble internal {Insert before last edit} } + if {$sl > $blen} { trouble internal {Insert past file end} } + + foreach x [lrange $base $ooff [expr {$sl - 1}]] { + log write 15 file {.|$x|} + lappend res $x + } + foreach x [lrange $lines $i [expr {$i + $cn - 1}]] { + log write 15 file {+|$x|} + lappend res $x + } + set ooff $sl + incr i $cn + } + } + } + foreach x [lrange $base $ooff end] { lappend res $x } + return [join $res \n] + } + + method Expand2 {graph node} { + set revnr [$graph node get $node revnr] + + # First import the file. + lappend myimport [list A r$revnr {}] + + if {[$graph node keyexists $node __base__]} { + # Delta node. __base__ is the name of the file containing + # the baseline. Generate instruction to make the delta as + # well. + + set fbase [$graph node get $node __base__] + lappend myimport [list D r$revnr r$fbase] + } + + # Post to all successors that the just generated file is their + # baseline. Exception: Those which ave already a baseline set. + # Together with the sorting of trunk revisions first the trunk + # should one uninterupted line, with branch roots _not_ delta + # compressed per their branches. + + foreach out [$graph nodes -out $node] { + if {[$graph node keyexists $out __base__]} continue + $graph node set $out __base__ $revnr + } + return + } + + variable myimport + + # # ## ### ##### ######## ############# + ## State + + variable myid {} ; # File id in the persistent state. + variable mypath {} ; # Path of the file's rcs archive. + variable myusrpath {} ; # Path of the file as seen by users. + variable myexecutable 0 ; # Boolean flag 'file executable'. + variable myproject {} ; # Reference to the project object + # the file belongs to. + variable myrev -array {} ; # Maps revision number to the + # associated revision object. + variable myrevisions {} ; # Same as myrev, but a list, + # giving us the order of + # revisions. + variable myaid -array {} ; # Map revision numbers to the id + # of the author who committed + # it. This is later aggregated + # with commit message, branch name + # and project id for a meta id. + variable myheadrevnr {} ; # Head revision (revision number) + variable myprincipal {} ; # Principal branch (branch number). + # Contrary to the name this is the + # default branch. + variable mydependencies {} ; # Dictionary parent -> child, + # records primary dependencies. + variable myimported 0 ; # Boolean flag. Set if and only if + # rev 1.1 of the file seemingly + # was imported instead of added + # normally. + variable myroot {} ; # Reference to the revision object + # holding the root revision. Its + # number usually is '1.1'. Can be + # a different number, because of + # gaps created via 'cvsadmin -o'. + variable mybranches -array {} ; # Maps branch number to the symbol + # object handling the branch. + variable mytags -array {} ; # Maps revision number to the list + # of symbol objects for the tags + # associated with the revision. + variable mysymbols {} ; # Set of the symbol names found in + # this file. + + variable mybranchcnt 0 ; # Counter for branches, to record their + # order of definition. This also defines + # their order of creation, which is the + # reverse of definition. I.e. a smaller + # number means 'Defined earlier', means + # 'Created later'. + + variable mytrunk {} ; # Direct reference to myproject -> trunk. + variable myroots {} ; # List of roots in the forest of + # lod's. Object references to revisions and + # branches. The latter can appear when they + # are severed from their parent. + + variable myblob {} ; # Reference to the object managing the blob + # information (textrange of revisions, and + # delta dependencies) of this file. + + # # ## ### ##### ######## ############# + ## Internal methods + + method RecordBranchCommits {branches} { + foreach branchrevnr $branches { + if {[catch { + set branch [$self Rev2Branch $branchrevnr] + }]} { + set branch [$self AddUnlabeledBranch [rev 2branchnr $branchrevnr]] + } + + # Record the commit, just as revision number for + # now. ProcesBranchDependencies will extend that ito a + # proper object reference. + + $branch setchildrevnr $branchrevnr + } + return + } + + method Rev2Branch {revnr} { + integrity assert {![rev istrunkrevnr $revnr]} {Expected a branch revision number} + return $mybranches([rev 2branchnr $revnr]) + } + + method AddUnlabeledBranch {branchnr} { + return [$self AddBranch unlabeled-$branchnr $branchnr] + } + + method AddBranch {name branchnr} { + if {[info exists mybranches($branchnr)]} { + log write 1 file "In '$mypath': Branch '$branchnr' named '[$mybranches($branchnr) name]'" + log write 1 file "Cannot have second name '$name', ignoring it" + return + } + set branch [sym %AUTO% branch $branchnr [$myproject getsymbol $name] $self] + $branch setposition [incr mybranchcnt] + set mybranches($branchnr) $branch + return $branch + } + + method AddTag {name revnr} { + set tag [sym %AUTO% tag $revnr [$myproject getsymbol $name] $self] + lappend mytags($revnr) $tag + return $tag + } + + method RecordBasicDependencies {revnr next} { + # Handle the revision dependencies. Record them for now, do + # nothing with them yet. + + # On the trunk the 'next' field points to the previous + # revision, i.e. the _parent_ of the current one. Example: + # 1.6's next is 1.5 (modulo cvs admin -o). + + # Contrarily on a branch the 'next' field points to the + # primary _child_ of the current revision. As example, + # 1.1.3.2's 'next' will be 1.1.3.3. + + # The 'next' field actually always refers to the revision + # containing the delta needed to retrieve that revision. + + # The dependencies needed here are the logical structure, + # parent/child, and not the implementation dependent delta + # pointers. + + if {$next eq ""} return + # parent -> child + if {[rev istrunkrevnr $revnr]} { + lappend mydependencies $next $revnr + } else { + lappend mydependencies $revnr $next + } + return + } + + method ProcessPrimaryDependencies {} { + foreach {parentrevnr childrevnr} $mydependencies { + set parent $myrev($parentrevnr) + set child $myrev($childrevnr) + $parent setchild $child + $child setparent $parent + } + return + } + + method ProcessBranchDependencies {} { + foreach {branchnr branch} [array get mybranches] { + set revnr [$branch parentrevnr] + + if {![info exists myrev($revnr)]} { + log write 1 file "In '$mypath': The branch '[$branch name]' references" + log write 1 file "the bogus revision '$revnr' and will be ignored." + $branch destroy + unset mybranches($branchnr) + } else { + set rev $myrev($revnr) + $rev addbranch $branch + $branch setparent $rev + + # If revisions were committed on the branch we store a + # reference to the branch there, and further declare + # the first child's parent to be branch's parent, and + # list this child in the parent revision. + + if {[$branch haschildrev]} { + set childrevnr [$branch childrevnr] + set child $myrev($childrevnr) + $branch setchild $child + + $child setparentbranch $branch + $child setparent $rev + $rev addchildonbranch $child + } + } + } + return + } + + method SortBranches {} { + foreach {revnr rev} [array get myrev] { $rev sortbranches } + return + } + + method ProcessTagDependencies {} { + foreach {revnr taglist} [array get mytags] { + if {![info exists myrev($revnr)]} { + set n [llength $taglist] + log write 1 file "In '$mypath': The following [nsp $n tag] reference" + log write 1 file "the bogus revision '$revnr' and will be ignored." + foreach tag $taglist { + log write 1 file " [$tag name]" + $tag destroy + } + unset mytags($revnr) + } else { + set rev $myrev($revnr) + foreach tag $taglist { + $rev addtag $tag + $tag settagrev $rev + } + } + } + return + } + + method DetermineTheRootRevision {} { + # The root is the one revision which has no parent. By + # checking all revisions we ensure that we can detect and + # report the case of multiple roots. Without that we could + # simply take one revision and follow the parent links to + # their root (sic!). + + foreach {revnr rev} [array get myrev] { + if {[$rev hasparent]} continue + integrity assert {$myroot eq ""} {Multiple root revisions found} + set myroot $rev + } + + # In the future we also need a list, as branches can become + # severed from their parent, making them their own root. + set myroots [list $myroot] + return + } + + method DetermineRevisionOperations {} { + foreach rev $myrevisions { $rev determineoperation } + return + } + + method DetermineLinesOfDevelopment {} { + # For revisions this has been done already, in 'extend'. Now + # we do this for the branches and tags. + + foreach {_ branch} [array get mybranches] { + $branch setlod [$self GetLOD [$branch parentrevnr]] + } + + foreach {_ taglist} [array get mytags] { + foreach tag $taglist { + $tag setlod [$self GetLOD [$tag tagrevnr]] + } + } + return + } + + method GetLOD {revnr} { + if {[rev istrunkrevnr $revnr]} { + return $mytrunk + } else { + return [$self Rev2Branch $revnr] + } + } + + method HandleNonTrunkDefaultBranch {} { + set revlist [$self NonTrunkDefaultRevisions] + if {![llength $revlist]} return + + $self AdjustNonTrunkDefaultBranch $revlist + $self CheckLODs + return + } + + method NonTrunkDefaultRevisions {} { + # From cvs2svn the following explanation (with modifications + # for our algorithm): + + # Determine whether there are any non-trunk default branch + # revisions. + + # If a non-trunk default branch is determined to have existed, + # return a list of objects for all revisions that were once + # non-trunk default revisions, in dependency order (i.e. root + # first). + + # There are two cases to handle: + + # One case is simple. The RCS file lists a default branch + # explicitly in its header, such as '1.1.1'. In this case, we + # know that every revision on the vendor branch is to be + # treated as head of trunk at that point in time. + + # But there's also a degenerate case. The RCS file does not + # currently have a default branch, yet we can deduce that for + # some period in the past it probably *did* have one. For + # example, the file has vendor revisions 1.1.1.1 -> 1.1.1.96, + # all of which are dated before 1.2, and then it has 1.1.1.97 + # -> 1.1.1.100 dated after 1.2. In this case, we should + # record 1.1.1.96 as the last vendor revision to have been the + # head of the default branch. + + if {$myprincipal ne ""} { + # There is still a default branch; that means that all + # revisions on that branch get marked. + + log write 5 file "Found explicitly marked NTDB" + + set rnext [$myroot child] + if {$rnext ne ""} { + trouble fatal "File with default branch $myprincipal also has revision [$rnext revnr]" + return + } + + set rev [$mybranches($myprincipal) child] + set res {} + + while {$rev ne ""} { + lappend res $rev + set rev [$rev child] + } + + return $res + + } elseif {$myimported} { + # No default branch, but the file appears to have been + # imported. So our educated guess is that all revisions + # on the '1.1.1' branch with timestamps prior to the + # timestamp of '1.2' were non-trunk default branch + # revisions. + + # This really only processes standard '1.1.1.*'-style + # vendor revisions. One could conceivably have a file + # whose default branch is 1.1.3 or whatever, or was that + # at some point in time, with vendor revisions 1.1.3.1, + # 1.1.3.2, etc. But with the default branch gone now, + # we'd have no basis for assuming that the non-standard + # vendor branch had ever been the default branch anyway. + + # Note that we rely on comparisons between the timestamps + # of the revisions on the vendor branch and that of + # revision 1.2, even though the timestamps might be + # incorrect due to clock skew. We could do a slightly + # better job if we used the changeset timestamps, as it is + # possible that the dependencies that went into + # determining those timestamps are more accurate. But + # that would require an extra pass or two. + + if {![info exists mybranches(1.1.1)]} { return {} } + + log write 5 file "Deduced existence of NTDB" + + set rev [$mybranches(1.1.1) child] + set res {} + set stop [$myroot child] + + if {$stop eq ""} { + # Get everything on the branch + while {$rev ne ""} { + lappend res $rev + set rev [$rev child] + } + } else { + # Collect everything on the branch which seems to have + # been committed before the first primary child of the + # root revision. + set stopdate [$stop date] + while {$rev ne ""} { + if {[$rev date] >= $stopdate} break + lappend res $rev + set rev [$rev child] + } + } + + return $res + + } else { + return {} + } + } + + # General note: In the following methods we only modify the links + # between revisions and symbols to restructure the revision + # tree. We do __not__ destroy the objects. Given the complex links + # GC is difficult at this level. It is much easier to drop + # everything when we we are done. This happens in 'drop', using + # the state variable 'myrev', 'mybranches', and 'mytags'. What we + # have to persist, performed by 'persist', we know will be + # reachable through the revisions listed in 'myroots' and their + # children and symbols. + + method AdjustNonTrunkDefaultBranch {revlist} { + set stop [$myroot child] ;# rev '1.2' + + log write 5 file "Adjusting NTDB containing [nsp [llength $revlist] revision]" + + # From cvs2svn the following explanation (with modifications + # for our algorithm): + + # Adjust the non-trunk default branch revisions found in the + # 'revlist'. + + # 'myimported' is a boolean flag indicating whether this file + # appears to have been imported, which also means that + # revision 1.1 has a generated log message that need not be + # preserved. 'revlist' is a list of object references for the + # revisions that have been determined to be non-trunk default + # branch revisions. + + # Note that the first revision on the default branch is + # handled strangely by CVS. If a file is imported (as opposed + # to being added), CVS creates a 1.1 revision, then creates a + # vendor branch 1.1.1 based on 1.1, then creates a 1.1.1.1 + # revision that is identical to the 1.1 revision (i.e., its + # deltatext is empty). The log message that the user typed + # when importing is stored with the 1.1.1.1 revision. The 1.1 + # revision always contains a standard, generated log message, + # 'Initial revision\n'. + + # When we detect a straightforward import like this, we want + # to handle it by deleting the 1.1 revision (which doesn't + # contain any useful information) and making 1.1.1.1 into an + # independent root in the file's dependency tree. In SVN, + # 1.1.1.1 will be added directly to the vendor branch with its + # initial content. Then in a special 'post-commit', the + # 1.1.1.1 revision is copied back to trunk. + + # If the user imports again to the same vendor branch, then CVS + # creates revisions 1.1.1.2, 1.1.1.3, etc. on the vendor branch, + # *without* counterparts in trunk (even though these revisions + # effectively play the role of trunk revisions). So after we add + # such revisions to the vendor branch, we also copy them back to + # trunk in post-commits. + + # We mark the revisions found in 'revlist' as default branch + # revisions. Also, if the root revision has a primary child + # we set that revision to depend on the last non-trunk default + # branch revision and possibly adjust its type accordingly. + + set first [lindex $revlist 0] + + log write 6 file "<[$first revnr]> [expr {$myimported ? "imported" : "not imported"}], [$first operation], [expr {[$first hastext] ? "has text" : "no text"}]" + + if {$myimported && + [$first revnr] eq "1.1.1.1" && + [$first operation] eq "change" && + ![$first hastext]} { + + set rev11 [$first parent] ; # Assert: Should be myroot. + log write 3 file "Removing irrelevant revision [$rev11 revnr]" + + # Cut out the old myroot revision. + + ldelete myroots $rev11 ; # Not a root any longer. + + $first cutfromparent ; # Sever revision from parent revision. + if {$stop ne ""} { + $stop cutfromparent + lappend myroots $stop ; # New root, after vendor branch + } + + # Cut out the vendor branch symbol + + set vendor [$first parentbranch] + integrity assert {$vendor ne ""} {First NTDB revision has no branch} + if {[$vendor parent] eq $rev11} { + $rev11 removebranch $vendor + $rev11 removechildonbranch $first + $vendor cutbranchparent ;# bp = rev11, about to be gone + $first cutfromparentbranch ;# pb = vendor, to be a detached LOD + lappend myroots $first + } + + # Change the type of first (typically from Change to Add): + $first retype add + + # Move any tags and branches from the old to the new root. + $rev11 movesymbolsto $first + } + + # Mark all the special revisions as such + foreach rev $revlist { + log write 3 file "Revision on default branch: [$rev revnr]" + $rev setondefaultbranch 1 + } + + if {$stop ne ""} { + # Revision 1.2 logically follows the imported revisions, + # not 1.1. Accordingly, connect it to the last NTDBR and + # possibly change its type. + + set last [lindex $revlist end] + $stop setdefaultbranchparent $last ; # Retypes the revision too. + $last setdefaultbranchchild $stop + } + return + } + + method CheckLODs {} { + foreach {_ branch} [array get mybranches] { $branch checklod } + foreach {_ taglist} [array get mytags] { + foreach tag $taglist { $tag checklod } + } + return + } + + method RemoveIrrelevantDeletions {} { + # From cvs2svn: If a file is added on a branch, then a trunk + # revision is added at the same time in the 'Dead' state. + # This revision doesn't do anything useful, so delete it. + + foreach root $myroots { + if {[$root isneeded]} continue + log write 2 file "Removing unnecessary dead revision [$root revnr]" + + # Remove as root, make its child new root after + # disconnecting it from the revision just going away. + + ldelete myroots $root + if {[$root haschild]} { + set child [$root child] + $child cutfromparent + lappend myroots $child + } + + # Cut out the branches spawned by the revision to be + # deleted. If the branch has revisions they should already + # use operation 'add', no need to change that. The first + # revision on each branch becomes a new and disconnected + # root. + + foreach branch [$root branches] { + $branch cutbranchparent + if {![$branch haschild]} continue + set first [$branch child] + $first cutfromparent + lappend myroots $first + } + $root removeallbranches + + # Tagging a dead revision doesn't do anything, so remove + # any tags that were set on it. + + $root removealltags + + # This can only happen once per file, and we might have + # just changed myroots, so end the loop + break + } + return + } + + method RemoveInitialBranchDeletions {} { + # From cvs2svn: If the first revision on a branch is an + # unnecessary delete, remove it. + # + # If a file is added on a branch (whether or not it already + # existed on trunk), then new versions of CVS add a first + # branch revision in the 'dead' state (to indicate that the + # file did not exist on the branch when the branch was + # created) followed by the second branch revision, which is an + # add. When we encounter this situation, we sever the branch + # from trunk and delete the first branch revision. + + # At this point we may have already multiple roots in myroots, + # we have to process them all. + + foreach root [$self LinesOfDevelopment] { + if {[$root isneededbranchdel]} continue + log write 2 file "Removing unnecessary initial branch delete [$root revnr]" + + set branch [$root parentbranch] + set parent [$root parent] + set child [$root child] + + ldelete myroots $root + lappend myroots $child + + $branch cutbranchparent + $branch cutchild + $child cutfromparent + + $branch setchild $child + $child setparentbranch $branch + + $parent removebranch $branch + $parent removechildonbranch $root + } + return + } + + method LinesOfDevelopment {} { + # Determine all lines of development for the file. This are + # the known roots, and the root of all branches found on the + # line of primary children. + + set lodroots {} + foreach root $myroots { + $self AddBranchedLinesOfDevelopment lodroots $root + lappend lodroots $root + } + return $lodroots + } + + method AddBranchedLinesOfDevelopment {lv root} { + upvar 1 $lv lodroots + while {$root ne ""} { + foreach branch [$root branches] { + if {![$branch haschild]} continue + set child [$branch child] + # Recurse into the branch for deeper branches. + $self AddBranchedLinesOfDevelopment lodroots $child + lappend lodroots $child + } + set root [$root child] + } + return + } + + method ExcludeNonTrunkInformation {} { + # Remove all non-trunk branches, revisions, and tags. We do + # keep the tags which are on the trunk. + + set ntdbroot "" + foreach root [$self LinesOfDevelopment] { + # Note: Here the order of the roots is important, + # i.e. that we get them in depth first order. This ensures + # that the removal of a branch happens only after the + # branches spawned from it were removed. Otherwise the + # system might try to access deleted objects. + + # Do not exclude the trunk. + if {[[$root lod] istrunk]} continue + $self ExcludeBranch $root ntdbroot + } + + if {$ntdbroot ne ""} { + $self GraftNTDB2Trunk $ntdbroot + } + return + } + + method ExcludeBranch {root nv} { + # Exclude the branch/lod starting at root, a revision. + # + # If the LOD starts with non-trunk default branch revisions, + # we leave them in place and do not delete the branch. In that + # case the command sets the variable in NV so that we can + # later rework these revisons to be purely trunk. + + if {[$root isondefaultbranch]} { + # Handling a NTDB. This branch may consists not only of + # NTDB revisions, but also some non-NTDB. The latter are + # truly on a branch and have to be excluded. The following + # loop determines if there are such revisions. + + upvar 1 $nv ntdbroot + set ntdbroot $root + $root cutfromparentbranch + + set rev $root + while {$rev ne ""} { + $rev removeallbranches + # See note [x]. + + if {[$rev isondefaultbranch]} { + set rev [$rev child] + } else { + break + } + } + + # rev now contains the first non-NTDB revision after the + # NTDB, or is empty if there is no such. If we have some + # they have to removed. + + if {$rev ne ""} { + set lastntdb [$rev parent] + $lastntdb cutfromchild + while {$rev ne ""} { + $rev removealltags + $rev removeallbranches + # Note [x]: We may still have had branches on the + # revision. Branches without revisions committed + # on them do not show up in the list of roots aka + # lines of development. + set rev [$rev child] + } + } + return + } + + # No NTDB stuff to deal with. First delete the branch object + # itself, after cutting all the various connections. + + set branch [$root parentbranch] + if {$branch ne ""} { + set branchparent [$branch parent] + $branchparent removebranch $branch + $branchparent removechildonbranch $root + } + + # The root is no such any longer either. + ldelete myroots $root + + # Now go through the line and remove all its revisions. + + while {$root ne ""} { + $root removealltags + $root removeallbranches + # Note: See the note [x]. + + # From cvs2svn: If this is the last default revision on a + # non-trunk default branch followed by a 1.2 revision, + # then the 1.2 revision depends on this one. FIXME: It is + # questionable whether this handling is correct, since the + # non-trunk default branch revisions affect trunk and + # should therefore not just be discarded even if + # --trunk-only. + + if {[$root hasdefaultbranchchild]} { + set ntdbchild [$root defaultbranchchild] + integrity assert { + [$ntdbchild defaultbranchparent] eq $ntdbchild + } {ntdb - trunk linkage broken} + $ntdbchild cutdefaultbranchparent + if {[$ntdbchild hasparent]} { + lappend myroots [$ntdbchild parent] + } + } + + set root [$root child] + } + + return + } + + method GraftNTDB2Trunk {root} { + # We can now graft the non-trunk default branch revisions to + # trunk. They should already be alone on a CVSBranch-less + # branch. + + integrity assert {![$root hasparentbranch]} {NTDB root still has its branch symbol} + integrity assert {![$root hasbranches]} {NTDB root still has spawned branches} + + set last $root + while {[$last haschild]} {set last [$last child]} + + if {[$last hasdefaultbranchchild]} { + + set rev12 [$last defaultbranchchild] + $rev12 cutdefaultbranchparent + $last cutdefaultbranchchild + + $rev12 changeparent $last + $last changechild $rev12 + + ldelete myroots $rev12 + + # Note and remember that the type of rev12 was already + # adjusted by AdjustNonTrunkDefaultBranch, so we don't + # have to change its type here. + } + + while {$root ne ""} { + $root setondefaultbranch 0 + $root setlod $mytrunk + foreach tag [$root tags] { + $tag setlod $mytrunk + } + set root [$root child] + } + + return + } + + method Active {} { + set revisions {} + set symbols {} + + foreach root [$self LinesOfDevelopment] { + if {[$root hasparentbranch]} { lappend symbols [$root parentbranch] } + while {$root ne ""} { + lappend revisions $root + foreach tag [$root tags] { lappend symbols $tag } + foreach branch [$root branches] { + integrity assert { + [$branch parent] eq $root + } {Backreference branch to its root is missing or wrong} + lappend symbols $branch + } + set lod [$root lod] + if {![$lod istrunk]} { + integrity assert { + [$lod haschild] + } {Branch is LOD symbol without revisions} + lappend symbols $lod + } + set root [$root child] + } + } + + return [list [lsort -unique -dict $revisions] [lsort -unique -dict $symbols]] + } + + + method AggregateSymbolData {} { + # Now that the exact set of revisions (and through that + # branches and tags) is known we can update the aggregate + # symbol statistics. + + foreach root [$self LinesOfDevelopment] { + set lod [$root lod] + + # Note: If the LOD is the trunk the count*, etc. methods + # will do nothing, as it is always present (cannot be + # excluded), and is always a branch too. + + # Lines of development count as branches and have a commit + # on them (root). If they are still attached to a tree we + # have to compute and register possible parents. + + $lod countasbranch + $lod countacommit + + if {[$root hasparentbranch]} { + # Note lod == [$root parentbranch] + $lod possibleparents + } elseif {![$lod istrunk] && [$root isondefaultbranch]} { + # This is the root revision of a detached NTDB. We + # have to manually set the only possible parent for + # this LOD, the trunk itself. + + [$lod symbol] possibleparent $mytrunk + } + + # For the revisions in the line we register their branches + # and tags as blockers for the lod, and update the type + # counters as well. As branch symbols without commits on + # them are not listed as lines of development, we have to + # count them here as well, as plain branches. At last we + # have to compute and register the possible parents of the + # tags, in case they are later converted as branches. + + while {$root ne ""} { + foreach branch [$root branches] { + $lod blockedby $branch + $branch possibleparents + if {[$branch haschild]} continue + $branch countasbranch + } + + foreach tag [$root tags] { + $lod blockedby $tag + $tag possibleparents + $tag countastag + } + + set root [$root child] + } + } + + return + } + + # # ## ### ##### ######## ############# + ## Configuration + + pragma -hastypeinfo no ; # no type introspection + pragma -hasinfo no ; # no object introspection + pragma -hastypemethods no ; # type is not relevant. + + # # ## ### ##### ######## ############# +} + +namespace eval ::vc::fossil::import::cvs { + namespace export file + namespace eval file { + # Import not required, already a child namespace. + # namespace import ::vc::fossil::import::cvs::file::rev + # namespace import ::vc::fossil::import::cvs::file::sym + namespace import ::vc::tools::misc::* + namespace import ::vc::tools::trouble + namespace import ::vc::tools::log + namespace import ::vc::fossil::import::cvs::blobstore + namespace import ::vc::fossil::import::cvs::state + namespace import ::vc::fossil::import::cvs::integrity + namespace import ::vc::fossil::import::cvs::gtcore + } +} + +# # ## ### ##### ######## ############# ##################### +## Ready + +package provide vc::fossil::import::cvs::file 1.0 +return diff --git a/tools/cvs2fossil/lib/c2f_fossil.tcl b/tools/cvs2fossil/lib/c2f_fossil.tcl new file mode 100644 index 0000000..6465d98 --- /dev/null +++ b/tools/cvs2fossil/lib/c2f_fossil.tcl @@ -0,0 +1,273 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007-2008 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Fossil, a helper class managing the access to fossil repositories. + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime. +package require fileutil ; # Temp.dir/file +package require snit ; # OO system. +package require vc::tools::trouble ; # Error reporting. +package require vc::tools::log ; # User feedback +package require vc::fossil::import::cvs::integrity ; # State integrity checks. + +# # ## ### ##### ######## ############# ##################### +## + +snit::type ::vc::fossil::import::cvs::fossil { + # # ## ### ##### ######## ############# + ## Public API + + constructor {} { + return + } + + method initialize {} { + set myrepository [fileutil::tempfile cvs2fossil_repo_] + set myworkspace [fileutil::tempfile cvs2fossil_wspc_] + ::file delete $myworkspace + ::file mkdir $myworkspace + + Do new [::file nativename $myrepository] + $self InWorkspace ; Do open [::file nativename $myrepository] + $self RestorePwd + + log write 8 fossil {Scratch repository created @ $myrepository} + log write 8 fossil {Scratch workspace created @ $myworkspace } + return + } + + method load {r w} { + set myrepository $r + set myworkspace $w + + log write 8 fossil {Scratch repository found @ $myrepository} + log write 8 fossil {Scratch workspace found @ $myworkspace} + return + } + + method space {} { + return [list $myrepository $myworkspace] + } + + # # ## ### ##### ######## ############# + ## + + method root {} { + # The id of the root manifest is hardwired into fossil. This + # manifest is created when a new repository is made (See + # 'new', in the constructor). + return 1 + } + + method workspace {} { return $myworkspace } + + method importfiles {map} { + # map = list (instruction), instruction = add|delta + # add = list ('A', path) + # delta = list ('D', path, src) + + log write 3 fossil {Importing revisions...} + + array set id {} + $self InWorkspace + + set n 0 + set max [llength $map] + + foreach insn $map { + log progress 3 fossil $n $max ; incr n + + struct::list assign $insn cmd pa pb + switch -exact -- $cmd { + A { + log write 8 fossil {Importing <$pa>,} + + # Result = 'inserted as record :FOO:' + # 0 1 2 3 + set res [Do test-content-put $pa] + integrity assert { + [regexp {^inserted as record \d+$} $res] + } {Unable to process unexpected fossil output '$res'} + set id($pa) [lindex $res 3] + } + D { + log write 8 fossil {Compressing <$pa>, as delta of <$pb>} + + Do test-content-deltify $id($pa) $id($pb) 1 + } + } + } + $self RestorePwd + + log write 3 fossil Done. + return [array get id] + } + + method importrevision {label user message date parent revisions} { + # Massage the commit message to remember the old user name + # which did the commit in CVS. + + set message "By $user:\n$message" + + log write 2 fossil {== $user @ [clock format $date]} + log write 2 fossil {-> $parent} + log write 9 fossil {%% [join [split $message \n] "\n%% "]} + + lappend cmd Do test-import-manifest $date $message + if {$parent ne ""} { lappend cmd -p $parent } + foreach {frid fpath flabel} $revisions { + lappend cmd -f $frid $fpath + log write 12 fossil {** <[format %5d $frid]> = <$flabel>} + } + + # run fossil test-command performing the import. + log write 12 fossil { [lreplace $cmd 3 3 @@]} + + $self InWorkspace + set res [eval $cmd] + $self RestorePwd + + integrity assert { + [regexp {^inserted as record \d+, [0-9a-fA-F]+$} $res] + } {Unable to process unexpected fossil output '$res'} + set rid [string trim [lindex $res 3] ,] + set uuid [lindex $res 4] + + log write 2 fossil {== $rid ($uuid)} + + return [list $rid $uuid] + } + + method tag {uuid name} { + log write 2 fossil {Tag '$name' @ $uuid} + + $self InWorkspace + Do tag add sym-$name $uuid + $self RestorePwd + return + } + + method branchmark {uuid name} { + # We do not mark the trunk + if {$name eq ":trunk:"} return + + log write 2 fossil {Begin branch '$name' @ $uuid} + + $self InWorkspace + Do tag branch sym-$name $uuid + $self RestorePwd + return + } + + method branchcancel {uuid name} { + # The trunk is unmarked, thus cancellation is not needed + # either. + if {$name eq ":trunk:"} return + + log write 2 fossil {Cancel branch '$name' @ $uuid} + + $self InWorkspace + Do tag delete sym-$name $uuid + $self RestorePwd + return + } + + method finalize {destination} { + log write 2 fossil {Finalize, rebuilding repository} + Do rebuild [::file nativename $myrepository] + + ::file rename -force $myrepository $destination + ::file delete -force $myworkspace + $self destroy + + log write 2 fossil {destination $destination} + return + } + + # # ## ### ##### ######## ############# + ## + + typemethod setlocation {path} { + set myfossilcmd $path + set myneedlocation 0 + return + } + + typemethod validate {} { + if {!$myneedlocation} { + if {![fileutil::test $myfossilcmd efrx msg]} { + trouble fatal "Bad path for fossil executable: $msg" + } + } else { + trouble fatal "Don't know where to find the 'fossil' executable" + } + return + } + + typeconstructor { + set location [auto_execok fossil] + set myneedlocation [expr {$location eq ""}] + if {$myneedlocation} return + $type setlocation $location + return + } + + # # ## ### ##### ######## ############# + ## State + + variable mypwd {} ; # Path to last CWD + variable myrepository {} ; # Path to our fossil database. + variable myworkspace {} ; # Path to the workspace for our fossil + # database. + + typevariable myfossilcmd ; # Path to fossil executable. + typevariable myneedlocation ; # Boolean, indicates if user has to + # tell us where fossil lives or not. + + # # ## ### ##### ######## ############# + ## Internal methods + + proc Do {args} { + # 8.5: exec $myfossilcmd {*}$args + log write 14 fossil {Doing '$args'} + return [eval [linsert $args 0 exec $myfossilcmd]] + } + + method InWorkspace {} { set mypwd [pwd] ; cd $myworkspace ; return } + method RestorePwd {} { cd $mypwd ; set mypwd {} ; return } + + # # ## ### ##### ######## ############# + ## Configuration + + pragma -hastypeinfo no ; # no type introspection + pragma -hasinfo no ; # no object introspection + + # # ## ### ##### ######## ############# +} + +namespace eval ::vc::fossil::import::cvs { + namespace export fossil + namespace eval fossil { + namespace import ::vc::tools::trouble + namespace import ::vc::tools::log + namespace import ::vc::fossil::import::cvs::integrity + } +} + +# # ## ### ##### ######## ############# ##################### +## Ready + +package provide vc::fossil::import::cvs::fossil 1.0 +return diff --git a/tools/cvs2fossil/lib/c2f_frev.tcl b/tools/cvs2fossil/lib/c2f_frev.tcl new file mode 100644 index 0000000..fa05513 --- /dev/null +++ b/tools/cvs2fossil/lib/c2f_frev.tcl @@ -0,0 +1,552 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007-2008 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Revisions per file. + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime. +package require snit ; # OO system. +package require vc::tools::misc ; # Text formatting +package require vc::fossil::import::cvs::state ; # State storage. +package require vc::fossil::import::cvs::integrity ; # State integrity checks. + +# # ## ### ##### ######## ############# ##################### +## + +snit::type ::vc::fossil::import::cvs::file::rev { + # # ## ### ##### ######## ############# + ## Public API + + constructor {revnr date state thefile} { + set myrevnr $revnr + set mydate $date + set myorigdate $date + set mystate $state + set myfile $thefile + return + } + + method defid {} { + set myid [incr myidcounter] + return + } + + method id {} { return $myid } + method file {} { return $myfile } + + # Basic pieces ________________________ + + method hasmeta {} { return [expr {$mymetaid ne ""}] } + method hastext {} { + return [expr {$mytextstart <= $mytextend}] + } + + method setmeta {meta} { set mymetaid $meta ; return } + method settext {text} { + struct::list assign $text mytextstart mytextend + return + } + method setlod {lod} { set mylod $lod ; return } + + method revnr {} { return $myrevnr } + method state {} { return $mystate } + method lod {} { return $mylod } + method date {} { return $mydate } + + method isneeded {} { + if {$myoperation ne "nothing"} {return 1} + if {$myrevnr ne "1.1"} {return 1} + if {![$mylod istrunk]} {return 1} + if {![llength $mybranches]} {return 1} + set firstbranch [lindex $mybranches 0] + if {![$firstbranch haschild]} {return 1} + if {$myisondefaultbranch} {return 1} + + # FIX: This message will not match if the RCS file was renamed + # manually after it was created. + + set gen "file [file tail [$myfile usrpath]] was initially added on branch [$firstbranch name]." + set log [$myfile commitmessageof $mymetaid] + + return [expr {$log ne $gen}] + } + + method isneededbranchdel {} { + if {$myparentbranch eq ""} {return 1} ; # not first on a branch, needed + set base [$myparentbranch parent] + if {$base eq ""} {return 1} ; # branch has parent lod, needed + if {[$self LODLength] < 2} {return 1} ; # our lod contains only ourselves, needed. + if {$myoperation ne "delete"} {return 1} ; # Not a deletion, needed + if {[llength $mytags]} {return 1} ; # Have tags, needed + if {[llength $mybranches]} {return 1} ; # Have other branches, needed + if {abs($mydate - [$base date]) > 2} {return 1} ; # Next rev > 2 seconds apart, needed + + # FIXME: This message will not match if the RCS file was + # renamed manually after it was created. + + set qfile [string map { + . \\. ? \\? * \\* \\ \\\\ + \\+ ^ \\^ $ \\$ + \[ \\\[ \] \\\] ( \\( ) \\) \{ \\\{ \} \\\} + } [file tail [$myfile usrpath]]] + set pattern "file $qfile was added on branch .* on \\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}( \[+-\]\\d{4})?" + set log [$myfile commitmessageof $mymetaid] + + # Not the special message, needed + if {![regexp -- $pattern $log]} {return 1} + + # This is an unneeded initial branch delete. + return 0 + } + + method LODLength {} { + set n 1 ; # count self + set rev $mychild + while {$rev ne ""} { + incr n + set rev [$rev child] + } + return $n + } + + # Basic parent/child linkage __________ + + method hasparent {} { return [expr {$myparent ne ""}] } + method haschild {} { return [expr {$mychild ne ""}] } + + method setparent {parent} { + integrity assert {$myparent eq ""} {Parent already defined} + set myparent $parent + return + } + + method cutfromparent {} { set myparent "" ; return } + method cutfromchild {} { set mychild "" ; return } + + method setchild {child} { + integrity assert {$mychild eq ""} {Child already defined} + set mychild $child + return + } + + method changeparent {parent} { set myparent $parent ; return } + method changechild {child} { set mychild $child ; return } + + method parent {} { return $myparent } + method child {} { return $mychild } + + # Branch linkage ______________________ + + method setparentbranch {branch} { + integrity assert {$myparentbranch eq ""} {Branch parent already defined} + set myparentbranch $branch + return + } + + method hasparentbranch {} { return [expr {$myparentbranch ne ""}] } + method hasbranches {} { return [llength $mybranches] } + + method parentbranch {} { return $myparentbranch } + method branches {} { return $mybranches } + + method addbranch {branch} { + lappend mybranches $branch + return + } + + method addchildonbranch {child} { + lappend mybranchchildren $child + return + } + + method cutfromparentbranch {} { set myparentbranch "" ; return } + + method removebranch {branch} { + ldelete mybranches $branch + return + } + + method removechildonbranch {rev} { + ldelete mybranchchildren $rev + return + } + + method sortbranches {} { + # Pass 2: CollectRev + + if {[llength $mybranches] < 2} return + + # Sort the branches spawned by this revision in creation + # order. To help in this our file gave all branches a position + # id, in order of their definition by the RCS archive. + # + # The creation order is (apparently) the reverse of the + # definition order. (If a branch is created then deleted, a + # later branch can be assigned the recycled branch number; + # therefore branch numbers are not an indication of creation + # order.) + + set tmp {} + foreach branch $mybranches { + lappend tmp [list $branch [$branch position]] + } + + set mybranches {} + foreach item [lsort -index 1 -decreasing $tmp] { + struct::list assign $item branch position + lappend mybranches $branch + } + return + } + + method movebranchesto {rev} { + set revlod [$rev lod] + foreach branch $mybranches { + $rev addbranch $branch + $branch setparent $rev + $branch setlod $revlod + } + foreach branchrev $mybranchchildren { + $rev addchildonbranch $branchrev + $branchrev cutfromparent + $branchrev setparent $rev + } + set mybranches {} + set mybranchchildren {} + return + } + + method removeallbranches {} { + set mybranches {} + set mybranchchildren {} + return + } + + # Tag linkage _________________________ + + method addtag {tag} { + lappend mytags $tag + return + } + + method tags {} { return $mytags } + + method removealltags {} { + set mytags {} + return + } + + method movetagsto {rev} { + set revlod [$rev lod] + foreach tag $mytags { + $rev addtag $tag + $tag settagrev $rev + $tag setlod $revlod + } + set mytags {} + return + } + + # general symbol operations ___________ + + method movesymbolsto {rev} { + # Move the tags and branches attached to this revision to the + # destination and fix all pointers. + + $self movetagsto $rev + $self movebranchesto $rev + return + } + + # Derived stuff _______________________ + + method determineoperation {} { + # Look at the state of both this revision and its parent to + # determine the type opf operation which was performed (add, + # modify, delete, none). + # + # The important information is dead vs not-dead for both, + # giving rise to four possible types. + + set sdead [expr {$mystate eq "dead"}] + set pdead [expr {$myparent eq "" || [$myparent state] eq "dead"}] + + set myoperation $myopstate([list $pdead $sdead]) + return + } + + method operation {} { return $myoperation } + method retype {x} { set myoperation $x ; return } + + method isondefaultbranch {} { return $myisondefaultbranch } + + method setondefaultbranch {x} { set myisondefaultbranch $x ; return } + + method setdefaultbranchchild {rev} { set mydbchild $rev ; return } + method setdefaultbranchparent {rev} { + set mydbparent $rev + + # Retype the revision (may change from 'add' to 'change'). + + set sdead [expr {$myoperation ne "change"}] + set pdead [expr {[$rev operation] ne "change"}] + set myoperation $myopstate([list $pdead $sdead]) + return + } + + method cutdefaultbranchparent {} { set mydbparent "" ; return } + method cutdefaultbranchchild {} { set mydbchild "" ; return } + + method defaultbranchchild {} { return $mydbchild } + method defaultbranchparent {} { return $mydbparent } + + method hasdefaultbranchchild {} { return [expr {$mydbchild ne ""}] } + method hasdefaultbranchparent {} { return [expr {$mydbparent ne ""}] } + + # # ## ### ##### ######## ############# + ## Type API + + typemethod istrunkrevnr {revnr} { + return [expr {[llength [split $revnr .]] == 2}] + } + + typemethod isbranchrevnr {revnr _ bv} { + if {[regexp $mybranchpattern $revnr -> head tail]} { + upvar 1 $bv branchnr + set branchnr ${head}$tail + return 1 + } + return 0 + } + + typemethod 2branchnr {revnr} { + # Input is a branch revision number, i.e. a revision number + # with an even number of components; for example '2.9.2.1' + # (never '2.9.2' nor '2.9.0.2'). The return value is the + # branch number (for example, '2.9.2'). For trunk revisions, + # like '3.4', we return the empty string. + + if {[$type istrunkrevnr $revnr]} { + return "" + } + return [join [lrange [split $revnr .] 0 end-1] .] + } + + typemethod 2branchparentrevnr {branchnr} { + # Chop the last segment off + return [join [lrange [split $branchnr .] 0 end-1] .] + } + + # # ## ### ##### ######## ############# + + method persist {} { + set fid [$myfile id] + set lod [$mylod id] + set op $myopcode($myoperation) + set idb $myisondefaultbranch + + lappend map @P@ [expr { ($myparent eq "") ? "NULL" : [$myparent id] }] + lappend map @C@ [expr { ($mychild eq "") ? "NULL" : [$mychild id] }] + lappend map @DP [expr { ($mydbparent eq "") ? "NULL" : [$mydbparent id] }] + lappend map @DC [expr { ($mydbchild eq "") ? "NULL" : [$mydbchild id] }] + lappend map @BP [expr { ($myparentbranch eq "") ? "NULL" : [$myparentbranch id] }] + + set cmd { + INSERT INTO revision ( rid, fid, rev, lod, parent, child, isdefault, dbparent, dbchild, bparent, op, date, state, mid) + VALUES ($myid, $fid, $myrevnr, $lod, @P@, @C@, $idb, @DP, @DC, @BP , $op, $mydate, $mystate, $mymetaid); + } + + state transaction { + state run [string map $map $cmd] + + # And the branch children as well, for pass 5. + foreach bc $mybranchchildren { + set bcid [$bc id] + state run { + INSERT INTO revisionbranchchildren (rid, brid) + VALUES ($myid, $bcid); + } + } + } + return + } + + # # ## ### ##### ######## ############# + ## State + + # Persistent: myid - revision.rid + # myfile - revision.fid + # mylod - revision.lod + # myrevnr - revision.rev + # mydate - revision.date + # mystate - revision.state + # mymetaid - revision.mid + # mytext{start,end} - revision.{cs,ce} + # myparent - revision.parent + # mychild - revision.child + # myparentbranch - revision.bparent + # myoperation - revision.op + # myisondefaultbranch - revision.isdefault + # mydbparent - revision.dbparent + # mydbchild - revision.dbchild + + method DUMP {label} { + puts "$label = $self <$myrevnr> (NTDB=$myisondefaultbranch) \{" + puts "\tP\t$myparent" + puts "\tC\t$mychild" + puts "\tPB\t$myparentbranch" + puts "\tdbP\t$mydbparent" + puts "\tdbC\t$mydbchild" + foreach b $mybranches { + puts \t\tB\t$b + } + foreach b $mybranchchildren { + puts \t\tBC\t$b + } + puts "\}" + return + } + + typevariable mybranchpattern {^((?:\d+\.\d+\.)+)(?:0\.)?(\d+)$} + # First a nonzero even number of digit groups with trailing dot + # CVS then sticks an extra 0 in here; RCS does not. + # And the last digit group. + + typevariable myidcounter 0 ; # Counter for revision ids. + variable myid {} ; # Revision id. + + variable myrevnr {} ; # Revision number of the revision. + variable mydate {} ; # Timestamp of the revision, seconds since epoch + variable myorigdate {} ; # Original unmodified timestamp. + variable mystate {} ; # State of the revision. + variable myfile {} ; # Ref to the file object the revision belongs to. + variable mytextstart {} ; # Start of the range of the (delta) text + # for this revision in the file. + variable mytextend {} ; # End of the range of the (delta) text + # for this revision in the file. + variable mymetaid {} ; # Id of the meta data group the revision + # belongs to. This is later used to put + # the file revisions into preliminary + # changesets (aka project revisions). + # This id encodes 4 pieces of data, + # namely: the project and branch the + # revision was committed to, the author + # who did the commit, and the message + # used. + variable mylod {} ; # Reference to the line-of-development + # object the revision belongs to. An + # alternative idiom would be to call it + # the branch the revision is on. This + # reference is to either project-level + # trunk or file-level symbol. + + # Basic parent/child linkage (lines of development) + + variable myparent {} ; # Ref to parent revision object. Link required because of + # ; # 'cvsadmin -o', which can create arbitrary gaps in the + # ; # numbering sequence. This is in the same line of development + # ; # Note: For the first revision on a branch the revision + # ; # it was spawned from is the parent. Only the root revision + # ; # of myfile's revision tree has nothing set here. + # ; # + + variable mychild {} ; # Ref to the primary child revision object, i.e. the next + # ; # revision in the same line of development. + + # Branch linkage ____________________ + + variable mybranches {} ; # List of the branches (objs) spawned by this revision. + variable myparentbranch {} ; # For the first revision on a branch the relevant + # ; # branch object. This also allows us to determine if + # ; # myparent is in the same LOD, or the revision the + # ; # branch spawned from. + + # List of the revision objects of the first commits on any + # branches spawned by this revision on which commits occurred. + # This dependency is kept explicitly because otherwise a + # revision-only topological sort would miss the dependency that + # exists via -> mybranches. + + variable mybranchchildren {} ; # List of the revisions (objs) which are the first + # ; # commits on any of the branches spawned from this + # ; # revision. The dependency is kept explicitly to + # ; # ensure that a revision-only topological sort will + # ; # not miss it, as it otherwise exists only via + # ; # mybranches. + + # Tag linkage ________________________ + + variable mytags {} ; # List of tags (objs) associated with this revision. + + # More derived data + + variable myoperation {} ; # One of 'add', 'change', 'delete', or + # 'nothing'. Derived from our and + # its parent's state. + variable myisondefaultbranch 0 ; # Boolean flag, set if the + # revision is on the non-trunk + # default branch, aka vendor + # branch. + variable mydbparent {} ; # Reference to the last revision + # on the vendor branch if this is + # the primary child of the + # regular root. + variable mydbchild {} ; # Reference to the primary child + # of the regular root if this is + # the last revision on the vendor + # branch. + + # dead(self) x dead(parent) -> operation + typevariable myopstate -array { + {0 0} change + {0 1} delete + {1 0} add + {1 1} nothing + } + + typemethod getopcodes {} { + state foreachrow { + SELECT oid, name FROM optype; + } { set myopcode($name) $oid } + return + } + + typevariable myopcode -array {} + + # # ## ### ##### ######## ############# + ## Internal methods + + # # ## ### ##### ######## ############# + ## Configuration + + pragma -hastypeinfo no ; # no type introspection + pragma -hasinfo no ; # no object introspection + pragma -simpledispatch yes ; # simple fast dispatch + + # # ## ### ##### ######## ############# +} + +namespace eval ::vc::fossil::import::cvs::file { + namespace export rev + namespace eval rev { + namespace import ::vc::tools::misc::* + namespace import ::vc::fossil::import::cvs::state + namespace import ::vc::fossil::import::cvs::integrity + } +} + +# # ## ### ##### ######## ############# ##################### +## Ready + +package provide vc::fossil::import::cvs::file::rev 1.0 +return diff --git a/tools/cvs2fossil/lib/c2f_fsym.tcl b/tools/cvs2fossil/lib/c2f_fsym.tcl new file mode 100644 index 0000000..d61b46e --- /dev/null +++ b/tools/cvs2fossil/lib/c2f_fsym.tcl @@ -0,0 +1,325 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007-2008 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Symbols (Tags, Branches) per file. + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime. +package require snit ; # OO system. +package require vc::tools::trouble ; # Error reporting. +package require vc::fossil::import::cvs::file::rev ; # CVS per file revisions. +package require vc::fossil::import::cvs::state ; # State storage. +package require vc::fossil::import::cvs::integrity ; # State integrity checks. + +# # ## ### ##### ######## ############# ##################### +## + +snit::type ::vc::fossil::import::cvs::file::sym { + # # ## ### ##### ######## ############# + ## Public API + + constructor {symtype nr symbol file} { + set myfile $file + set mytype $symtype + set mynr $nr + set mysymbol $symbol + + switch -exact -- $mytype { + branch { SetupBranch ; return } + tag { return } + } + integrity assert 0 {Bad symbol type '$mytype'} + return + } + + method defid {} { + set myid [incr myidcounter] + return + } + + method fid {} { return $myid } + method symbol {} { return $mysymbol } + + # Symbol acessor methods. + + delegate method name to mysymbol + delegate method id to mysymbol + + # Symbol aggregation methods + + delegate method countasbranch to mysymbol + delegate method countastag to mysymbol + delegate method countacommit to mysymbol + + method blockedby {fsymbol} { + $mysymbol blockedby [$fsymbol symbol] + return + } + + method possibleparents {} { + switch -exact -- $mytype { + branch { $self BranchParents } + tag { $self TagParents } + } + return + } + + method BranchParents {} { + # The "obvious" parent of a branch is the branch holding the + # revision spawning the branch. Any other branches that are + # rooted at the same revision and were committed earlier than + # the branch are also possible parents. + + # Ignore this if the branch symbol is detached. + if {$mybranchparent eq ""} return + + $mysymbol possibleparent [[$mybranchparent lod] symbol] + + foreach branch [$mybranchparent branches] { + # A branch cannot be its own parent. Nor can a branch + # created after this one be its parent. This means that we + # can abort the loop when we have reached ourselves in the + # list of branches. Here the order of file::rev.mybranches + # comes into play, as created by file::rev::sortbranches. + + if {$branch eq $self} break + $mysymbol possibleparent [$branch symbol] + } + return + } + + method TagParents {} { + # The "obvious" parent of a tag is the branch holding the + # revision spawning the tag. Branches that are spawned by the + # same revision are also possible parents. + + $mysymbol possibleparent [[$mytagrev lod] symbol] + + foreach branch [$mytagrev branches] { + $mysymbol possibleparent [$branch symbol] + } + return + } + + # + + method istrunk {} { return 0 } + + # Branch acessor methods. + + method setchildrevnr {revnr} { + integrity assert {$mybranchchildrevnr eq ""} {Child already defined} + set mybranchchildrevnr $revnr + return + } + + method setposition {n} { set mybranchposition $n ; return } + method setparent {rev} { set mybranchparent $rev ; return } + method setchild {rev} { set mybranchchild $rev ; return } + method cutchild {} { set mybranchchild "" ; return } + method cutbranchparent {} { set mybranchparent "" ; return } + + method branchnr {} { return $mynr } + method parentrevnr {} { return $mybranchparentrevnr } + method childrevnr {} { return $mybranchchildrevnr } + method haschildrev {} { return [expr {$mybranchchildrevnr ne ""}] } + method haschild {} { return [expr {$mybranchchild ne ""}] } + method parent {} { return $mybranchparent } + method child {} { return $mybranchchild } + method position {} { return $mybranchposition } + + # Tag acessor methods. + + method tagrevnr {} { return $mynr } + method settagrev {rev} { set mytagrev $rev ; return } + + # Derived information + + method lod {} { return $mylod } + + method setlod {lod} { + set mylod $lod + $self checklod + return + } + + method checklod {} { + # Consistency check. The symbol's line-of-development has to + # be same as the line-of-development of its source (parent + # revision of a branch, revision of a tag itself). + + switch -exact -- $mytype { + branch { + # However, ignore this if the branch symbol is + # detached. + if {$mybranchparent eq ""} return + + set slod [$mybranchparent lod] + } + tag { set slod [$mytagrev lod] } + } + + if {$mylod ne $slod} { + trouble fatal "For $mytype [$mysymbol name]: LOD conflict with source, '[$mylod name]' vs. '[$slod name]'" + return + } + return + } + + # # ## ### ##### ######## ############# + + method persist {} { + # Save the information we need after the collection pass. + + set fid [$myfile id] + set sid [$mysymbol id] + set lod [$mylod id] + + switch -exact -- $mytype { + tag { + set rid [$mytagrev id] + state transaction { + state run { + INSERT INTO tag ( tid, fid, lod, sid, rev) + VALUES ($myid, $fid, $lod, $sid, $rid); + } + } + } + branch { + lappend map @F@ [expr { ($mybranchchild eq "") ? "NULL" : [$mybranchchild id] }] + lappend map @P@ [expr { ($mybranchparent eq "") ? "NULL" : [$mybranchparent id] }] + + set cmd { + INSERT INTO branch ( bid, fid, lod, sid, root, first, bra, pos ) + VALUES ($myid, $fid, $lod, $sid, @P@, @F@, $mynr, $mybranchposition); + } + state transaction { + state run [string map $map $cmd] + } + } + } + + return + } + + method DUMP {label} { + puts "$label = $self $mytype [$self name] \{" + switch -exact -- $mytype { + tag { + puts "\tR\t$mytagrev" + } + branch { + puts "\tP\t$mybranchparent" + puts "\tC\t$mybranchchild" + puts "\t\t<$mynr>" + } + } + puts "\}" + return + } + + # # ## ### ##### ######## ############# + ## State + + # Persistent: + # Tag: myid - tag.tid + # myfile - tag.fid + # mylod - tag.lod + # mysymbol - tag.sid + # mytagrev - tag.rev + # + # Branch: myid - branch.bid + # myfile - branch.fid + # mylod - branch.lod + # mysymbol - branch.sid + # mybranchparent - branch.root + # mybranchchild - branch.first + # mynr - branch.bra + + typevariable myidcounter 0 ; # Counter for symbol ids. + variable myid {} ; # Symbol id. + + ## Basic, all symbols _________________ + + variable myfile {} ; # Reference to the file the symbol is in. + variable mytype {} ; # Symbol type, 'tag', or 'branch'. + variable mynr {} ; # Revision number of a 'tag', branch number + # of a 'branch'. + variable mysymbol {} ; # Reference to the symbol object of this + # symbol at the project level. + variable mylod {} ; # Reference to the line-of-development + # object the symbol belongs to. An + # alternative idiom would be to call it the + # branch the symbol is on. This reference + # is to a project-level object (symbol or + # trunk). + + ## Branch symbols _____________________ + + variable mybranchparentrevnr {} ; # The number of the parent + # revision, derived from our + # branch number (mynr). + variable mybranchparent {} ; # Reference to the revision + # (object) which spawns the + # branch. + variable mybranchchildrevnr {} ; # Number of the first revision + # committed on this branch. + variable mybranchchild {} ; # Reference to the revision + # (object) first committed on + # this branch. + variable mybranchposition {} ; # Relative id of the branch in + # the file, to sort into + # creation order. + + ## Tag symbols ________________________ + + variable mytagrev {} ; # Reference to the revision object the tag + # is on, identified by 'mynr'. + + # ... nothing special ... (only mynr, see basic) + + # # ## ### ##### ######## ############# + ## Internal methods + + proc SetupBranch {} { + upvar 1 mybranchparentrevnr mybranchparentrevnr mynr mynr + set mybranchparentrevnr [rev 2branchparentrevnr $mynr] + return + } + + # # ## ### ##### ######## ############# + ## Configuration + + pragma -hastypeinfo no ; # no type introspection + pragma -hasinfo no ; # no object introspection + pragma -hastypemethods no ; # type is not relevant. + + # # ## ### ##### ######## ############# +} + +namespace eval ::vc::fossil::import::cvs::file { + namespace export sym + namespace eval sym { + namespace import ::vc::fossil::import::cvs::file::rev + namespace import ::vc::fossil::import::cvs::state + namespace import ::vc::fossil::import::cvs::integrity + namespace import ::vc::tools::trouble + } +} + +# # ## ### ##### ######## ############# ##################### +## Ready + +package provide vc::fossil::import::cvs::file::sym 1.0 +return diff --git a/tools/cvs2fossil/lib/c2f_ftrunk.tcl b/tools/cvs2fossil/lib/c2f_ftrunk.tcl new file mode 100644 index 0000000..75ecbef --- /dev/null +++ b/tools/cvs2fossil/lib/c2f_ftrunk.tcl @@ -0,0 +1,57 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Trunk, the special main line of development in a file. + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime. +package require snit ; # OO system. + +# # ## ### ##### ######## ############# ##################### +## + +snit::type ::vc::fossil::import::cvs::file::trunk { + # # ## ### ##### ######## ############# + ## Public API + + constructor {} { + return + } + + # # ## ### ##### ######## ############# + ## State + + # # ## ### ##### ######## ############# + ## Internal methods + + # # ## ### ##### ######## ############# + ## Configuration + + pragma -hastypeinfo no ; # no type introspection + pragma -hasinfo no ; # no object introspection + pragma -hastypemethods no ; # type is not relevant. + pragma -simpledispatch yes ; # simple fast dispatch + + # # ## ### ##### ######## ############# +} + +namespace eval ::vc::fossil::import::cvs::file { + namespace export trunk +} + +# # ## ### ##### ######## ############# ##################### +## Ready + +package provide vc::fossil::import::cvs::file::trunk 1.0 +return diff --git a/tools/cvs2fossil/lib/c2f_gtcore.tcl b/tools/cvs2fossil/lib/c2f_gtcore.tcl new file mode 100644 index 0000000..8087bf3 --- /dev/null +++ b/tools/cvs2fossil/lib/c2f_gtcore.tcl @@ -0,0 +1,239 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## This file provides a helper package implementing the core of +## traversing the nodes of a graph in topological order. This is used +## by the cycle breaker code (not yet), and the import backend. + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime. +package require snit ; # OO system. +package require struct::graph ; # Graph handling. +package require struct::list ; # Higher order list operations. +package require vc::tools::log ; # User feedback. +package require vc::tools::trouble ; # Error reporting. + +# # ## ### ##### ######## ############# ##################### +## + +snit::type ::vc::fossil::import::cvs::gtcore { + # # ## ### ##### ######## ############# + ## Public API + + typemethod savecmd {cmd} { ::variable mysavecmd $cmd ; return } + typemethod cyclecmd {cmd} { ::variable mycyclecmd $cmd ; return } + typemethod sortcmd {cmd} { ::variable mysortcmd $cmd ; return } + typemethod datacmd {cmd} { ::variable mydatacmd $cmd ; return } + typemethod formatcmd {cmd} { ::variable myformatcmd $cmd ; return } + + # # ## ### ##### ######## ############# + + typemethod traverse {graph {label Traverse}} { + InitializeCandidates $graph + + log write 3 gtcore {$label} + + set k 0 + set max [llength [$graph nodes]] + + while {1} { + while {[WithoutPredecessor $graph node]} { + log progress 2 gtcore $k $max + incr k + + ProcessedHook $graph $node + ShowPendingNodes $graph + $graph node delete $node + } + + if {![llength [$graph nodes]]} break + + CycleHook $graph + InitializeCandidates $graph + } + + log write 3 gtcore Done. + ClearHooks + return + } + + # # ## ### ##### ######## ############# + ## Internal methods + + # Instead of searching the whole graph for the degree-0 nodes in + # each iteration we compute the list once to start, and then only + # update it incrementally based on the outgoing neighbours of the + # node chosen for commit. + + proc InitializeCandidates {graph} { + # bottom = list (list (node, range min, range max)) + ::variable mybottom + foreach node [$graph nodes] { + if {[$graph node degree -in $node]} continue + lappend mybottom [list $node [DataHook $graph $node]] + } + ScheduleCandidates $graph + ShowPendingNodes $graph + return + } + + proc WithoutPredecessor {graph nodevar} { + ::variable mybottom + + upvar 1 $nodevar node + if {![llength $mybottom]} { return 0 } + + set node [lindex [lindex $mybottom 0] 0] + set mybottom [lrange $mybottom 1 end] + set changed 0 + + # Update list of nodes without predecessor, based on the + # outgoing neighbours of the chosen node. This should be + # faster than iterating of the whole set of nodes, finding all + # without predecessors, sorting them by time, etc. pp. + + foreach out [$graph nodes -out $node] { + if {[$graph node degree -in $out] > 1} continue + # Degree-1 neighbour, will have no predecessors after the + # removal of n. Put on the list of candidates we can + # process. + lappend mybottom [list $out [DataHook $graph $out]] + set changed 1 + } + if {$changed} { + ScheduleCandidates $graph + } + + # We do not delete the node immediately, to allow the Save + # procedure to save the dependencies as well (encoded in the + # arcs). + return 1 + } + + proc ScheduleCandidates {graph} { + ::variable mybottom + ::variable mysortcmd + if {[llength $mysortcmd]} { + set mybottom [uplevel \#0 [linsert $mysortcmd end $graph $mybottom]] + } else { + set mybottom [lsort -index 0 -dict $mybottom] + } + return + } + + proc ShowPendingNodes {graph} { + if {[log verbosity?] < 10} return + ::variable mybottom + ::variable myformatcmd + + log write 10 gtcore "Pending..............................." + foreach item [struct::list map $mybottom \ + [linsert $myformatcmd end $graph]] { + log write 10 gtcore "Pending: $item" + } + return + } + + # # ## ### ##### ######## ############# + ## Callback invokation ... + + proc DataHook {graph node} { + # Allow the user of the traverser to a client data to a node + # in the list of nodes available for immediate processing. + # This data can be used by the sort callback. + + ::variable mydatacmd + if {![llength $mydatacmd]} { return {} } + + return [uplevel \#0 [linsert $mydatacmd end $graph $node]] + } + + proc FormatHook {graph item} { + # Allow the user to format a pending item (node + client data) + # according to its wishes. + + ::variable myformatcmd + if {![llength $myformatcmd]} { return $item } + + return [uplevel \#0 [linsert $myformatcmd end $graph $item]] + } + + proc ProcessedHook {graph node} { + # Give the user of the traverser the opportunity to work with + # the node before it is removed from the graph. + + ::variable mysavecmd + if {![llength $mysavecmd]} return + + uplevel \#0 [linsert $mysavecmd end $graph $node] + return + } + + proc CycleHook {graph} { + # Call out to the chosen algorithm for handling cycles. It is + # an error to find a cycle if no hook was defined. + + ::variable mycyclecmd + if {![llength $mycyclecmd]} { + trouble fatal "Found a cycle, expecting none." + exit 1 + } + + uplevel \#0 [linsert $mycyclecmd end $graph] + return + } + + proc ClearHooks {} { + ::variable mysortcmd {} + ::variable myformatcmd {} + ::variable mydatacmd {} + ::variable mysavecmd {} + ::variable mycyclecmd {} + return + } + + # # ## ### ##### ######## ############# + + typevariable mybottom {} ; # List of the nodes pending traversal. + + typevariable mysortcmd {} ; # Callback, sort list of pending nodes + typevariable mydatacmd {} ; # Callback, get client data for a pending node + typevariable myformatcmd {} ; # Callback, format a pending node for display + typevariable mysavecmd {} ; # Callback, for each processed node. + typevariable mycyclecmd {} ; # Callback, when a cycle was encountered. + + # # ## ### ##### ######## ############# + ## Configuration + + pragma -hasinstances no ; # singleton + pragma -hastypeinfo no ; # no introspection + pragma -hastypedestroy no ; # immortal + + # # ## ### ##### ######## ############# +} + +namespace eval ::vc::fossil::import::cvs { + namespace export gtcore + namespace eval gtcore { + namespace import ::vc::tools::log + namespace import ::vc::tools::trouble + log register gtcore + } +} + +# # ## ### ##### ######## ############# ##################### +## Ready + +package provide vc::fossil::import::cvs::gtcore 1.0 +return diff --git a/tools/cvs2fossil/lib/c2f_integrity.tcl b/tools/cvs2fossil/lib/c2f_integrity.tcl new file mode 100644 index 0000000..05c17fc --- /dev/null +++ b/tools/cvs2fossil/lib/c2f_integrity.tcl @@ -0,0 +1,964 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007-2008 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## This package holds a number of integrity checks done on the +## persistent state. This is used by the passes II and IV. + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime. +package require snit ; # OO system. +package require vc::tools::trouble ; # Error reporting. +package require vc::tools::log ; # User feedback. +package require vc::fossil::import::cvs::state ; # State storage. + +# # ## ### ##### ######## ############# ##################### +## + +snit::type ::vc::fossil::import::cvs::integrity { + # # ## ### ##### ######## ############# + ## Public API + + typemethod assert {expression failmessage} { + set ok [uplevel 1 [list ::expr $expression]] + if {$ok} return + trouble internal [uplevel 1 [list ::subst $failmessage]] + return + } + + typemethod strict {} { + log write 4 integrity {Check database consistency} + + set n 0 + AllButMeta + Meta + return + } + + typemethod metarelaxed {} { + log write 4 integrity {Check database consistency} + + set n 0 + AllButMeta + return + } + + typemethod changesets {} { + log write 4 integrity {Check database consistency} + + set n 0 + RevisionChangesets + TagChangesets + BranchChangesets + return + } + + # # ## ### ##### ######## ############# + ## Internal methods + + proc AllButMeta {} { + # This code performs a number of paranoid checks of the + # database, searching for inconsistent cross-references. + + upvar 1 n n ; # Counter for the checks (we print an id before + # the main label). + + # Find all revisions which disagree with their line of + # development about the project they are owned by. + CheckRev \ + {Revisions and their LODs have to be in the same project} \ + {disagrees with its LOD about owning project} { + SELECT F.name, R.rev + FROM revision R, file F, symbol S + WHERE R.fid = F.fid -- get file of rev + AND R.lod = S.sid -- get symbol of its lod + AND F.pid != S.pid -- disagreement about the owning project + ; + } + # Find all revisions which disgree with their meta data about + # the project they are owned by. + CheckRev \ + {Revisions and their meta data have to be in the same project} \ + {disagrees with its meta data about owning project} { + SELECT F.name, R.rev + FROM revision R, file F, meta M + WHERE R.fid = F.fid -- get file of rev + AND R.mid = M.mid -- get meta of rev + AND F.pid != M.pid -- disagreement about owning project + ; + } + # Find all revisions with a primary child which disagrees + # about the file they belong to. + CheckRev \ + {Revisions and their primary children have to be in the same file} \ + {disagrees with its primary child about the owning file} { + SELECT F.name, R.rev + FROM revision R, revision C, file F + WHERE R.fid = F.fid -- get file of rev + AND R.child IS NOT NULL -- get all with primary children + AND R.child = C.rid -- get primary child + AND C.fid != R.fid -- wrongly in different file + ; + } + + # Find all revisions with a branch parent symbol whose parent + # disagrees about the file they belong to. + CheckRev \ + {Revisions and their branch children have to be in the same file} \ + {at the beginning of its branch and its parent disagree about the owning file} { + SELECT F.name, R.rev + FROM revision R, revision P, file F + WHERE R.fid = F.fid -- get file of rev + AND R.bparent IS NOT NULL -- get first-of-branch revisions + AND R.parent = P.rid -- get out-of-branch parent + AND R.fid != P.fid -- wrongly in different file + ; + } + # Find all revisions with a non-NTDB child which disagrees + # about the file they belong to. + CheckRev \ + {Revisions and their non-NTDB children have to be in the same file} \ + {disagrees with its non-NTDB child about the owning file} { + SELECT F.name, R.rev + FROM revision R, revision C, file F + WHERE R.fid = F.fid -- get file of rev + AND R.dbchild IS NOT NULL -- get last NTDB revisions + AND R.dbchild = C.rid -- get their child + AND C.fid != R.fid -- wrongly in different file + ; + } + # Find all revisions which have a primary child, but the child + # does not have them as parent. + CheckRev \ + {Revisions have to be parents of their primary children} \ + {is not the parent of its primary child} { + SELECT F.name, R.rev + FROM revision R, revision C, file F + WHERE R.fid = F.fid -- get file of rev + AND R.child IS NOT NULL -- get all with primary children + AND R.child = C.rid -- get primary child + AND C.parent != R.rid -- child's parent wrongly not us + ; + } + # Find all revisions which have a primrary child, but the + # child has a branch parent symbol making them brach starters. + CheckRev \ + {Primary children of revisions must not start branches} \ + {is parent of a primary child which is the beginning of a branch} { + SELECT F.name, R.rev + FROM revision R, revision C, file F + WHERE R.fid = F.fid -- get file of rev + AND R.child IS NOT NULL -- get all with primary children + AND R.child = C.rid -- get primary child + AND C.bparent IS NOT NULL -- but indicates to be on branch + ; + } + # Find all revisions without branch parent symbol which have a + # parent, but the parent does not have them as primary child. + CheckRev \ + {Revisions have to be primary children of their parents, if any} \ + {is not the child of its parent} { + SELECT F.name, R.rev + FROM revision R, revision P, file F + WHERE R.fid = F.fid -- get file of revision + AND R.bparent IS NULL -- exclude all first-on-branch revisions + AND R.parent IS NOT NULL -- which are not root of their line + AND R.parent = P.rid -- get in-lod parent + AND P.child != R.rid -- but does not have rev as primary child + ; + } + # Find all revisions with a branch parent symbol which do not + # have a parent. + CheckRev \ + {Branch starting revisions have to have a parent, if not detached} \ + {at the beginning of its branch has no parent, but its branch has} { + SELECT F.name, R.rev + FROM revision R, file F, branch B + WHERE R.fid = F.fid -- get file of revision + AND R.bparent IS NOT NULL -- limit to first-on-branch revisions + AND R.parent IS NULL -- which are detached + AND B.sid = R.bparent -- get branch governing the rev + AND B.fid = R.fid -- in the revision's file + AND B.root IS NOT NULL -- but says that branch is attached + ; + } + # Find all revisions with a branch parent symbol whose parent + # has them as primary child. + CheckRev \ + {Branch starting revisions must not be primary children of their parents} \ + {at the beginning of its branch is the primary child of its parent} { + SELECT F.name, R.rev + FROM revision R, revision P, file F + WHERE R.fid = F.fid -- get file of revision + AND R.bparent IS NOT NULL -- limit to first-on-branch revisions + AND R.parent IS NOT NULL -- which are attached + AND R.parent = P.rid -- get out-of-branch parent + AND P.child = R.rid -- wrongly has rev as primary child + ; + } + # Find all revisions with a non-NTDB child which are not on + # the NTDB. + CheckRev \ + {NTDB to trunk transition has to begin on NTDB} \ + {has a non-NTDB child, yet is not on the NTDB} { + SELECT F.name, R.rev + FROM revision R, file F + WHERE R.fid = F.fid -- get file of revision + AND R.dbchild IS NOT NULL -- limit to last NTDB revision + AND NOT R.isdefault -- but signals not-NTDB + ; + } + # Find all revisions with a NTDB parent which are on the NTDB. + CheckRev \ + {NTDB to trunk transition has to end on non-NTDB} \ + {has a NTDB parent, yet is on the NTDB} { + SELECT F.name, R.rev + FROM revision R, file F + WHERE R.fid = F.fid -- get file of revision + AND R.dbparent IS NOT NULL -- limit to roots of non-NTDB + AND R.isdefault -- but signals to be NTDB + ; + } + # Find all revisions with a child which disagrees about the + # line of development they belong to. + CheckRev \ + {Revisions and their primary children have to be in the same LOD} \ + {and its primary child disagree about their LOD} { + SELECT F.name, R.rev + FROM revision R, revision C, file F + WHERE R.fid = F.fid -- get file of revision + AND R.child IS NOT NULL -- revision has a primary child + AND R.child = C.rid -- get that child + AND C.lod != R.lod -- child wrongly disagrees with lod + ; + } + # Find all revisions with a non-NTDB child which agrees about + # the line of development they belong to. + CheckRev \ + {NTDB and trunk revisions have to be in different LODs} \ + {on NTDB and its non-NTDB child wrongly agree about their LOD} { + SELECT F.name, R.rev + FROM revision R, revision C, file F + WHERE R.fid = F.fid -- get file of revision + AND R.dbchild IS NOT NULL -- limit to last NTDB revision + AND R.dbchild = C.rid -- get non-NTDB child + AND C.lod = R.lod -- child wrongly has same lod + ; + } + # Find all revisions with a branch parent symbol which is not + # their LOD. + CheckRev \ + {Branch starting revisions have to have their LOD as branch parent symbol} \ + {at the beginning of its branch does not have the branch symbol as its LOD} { + SELECT F.name, R.rev + FROM revision R, file F + WHERE R.fid = F.fid -- get file of revision + AND R.bparent IS NOT NULL -- limit to branch-first revisions + AND R.lod != R.bparent -- out-of-branch parent wrongly is not the lod + ; + } + # Find all revisions with a branch parent symbol whose parent + # is in the same line of development. + CheckRev \ + {Revisions and their branch children have to be in different LODs} \ + {at the beginning of its branch and its parent wrongly agree about their LOD} { + SELECT F.name, R.rev + FROM revision R, revision P, file F + WHERE R.fid = F.fid -- get file of revision + AND R.bparent IS NOT NULL -- limit to branch-first revisions + AND R.parent = P.rid -- get out-of-branch parent of revision + AND R.lod = P.lod -- rev and parent wrongly agree on lod + ; + } + return + } + + proc Meta {} { + # This code performs a number of paranoid checks of the + # database, searching for inconsistent cross-references. + + upvar 1 n n ; # Counter for the checks (we print an id before + # the main label). + + # Find all revisions which disgree with their meta data about + # the branch/line of development they belong to. + CheckRev \ + {Revisions and their meta data have to be in the same LOD} \ + {disagrees with its meta data about owning LOD} { + SELECT F.name, R.rev + FROM revision R, meta M, file F + WHERE R.mid = M.mid -- get meta data of revision + AND R.lod != M.bid -- rev wrongly disagrees with meta about lod + AND R.fid = F.fid -- get file of revision + ; + } + return + } + + proc RevisionChangesets {} { + # This code performs a number of paranoid checks of the + # database, searching for inconsistent changeset/revision + # information. + + upvar 1 n n ; # Counter for the checks (we print an id before + # the main label). + + # Find all revisions which are not used by at least one + # changeset. + CheckRev \ + {All revisions have to be used by least one changeset} \ + {is not used by a revision changeset} { + -- Unused revisions = All revisions + -- - revisions used by revision changesets. + -- + -- Both sets can be computed easily, and subtracted + -- from each other. Then we can get the associated + -- file (name) for display. + + SELECT F.name, R.rev + FROM revision R, file F + WHERE R.rid IN (SELECT rid + FROM revision -- All revisions + EXCEPT -- subtract + SELECT CI.iid + FROM csitem CI, changeset C -- revisions used + WHERE C.cid = CI.cid -- by any revision + AND C.type = 0) -- changeset + AND R.fid = F.fid -- get file of unused revision + } + # Find all revisions which are used by more than one + # changeset. + CheckRev \ + {All revisions have to be used by at most one changeset} \ + {is used by multiple changesets} { + -- Principle of operation: Get all revision/changeset + -- pairs for all revision changesets, group by + -- revision to aggregate the changeset, counting + -- them. From the resulting revision/count table + -- select those with more than one user, and get their + -- associated file (name) for display. + + SELECT F.name, R.rev + FROM revision R, file F, + (SELECT CI.iid AS rid, -- revision item + count(CI.cid) AS count -- number of csets using item + FROM csitem CI, changeset C + WHERE C.type = 0 -- limit to revision csets + AND C.cid = CI.cid -- get item in changeset + GROUP BY CI.iid -- aggregate by item, count csets/item + ) AS U + WHERE U.count > 1 -- limit to item with multiple users + AND R.rid = U.rid -- get revision of item + AND R.fid = F.fid -- get file of revision + } + # All revisions have to refer to the same meta information as + # their changeset. + CheckRevCS \ + {All revisions have to agree with their changeset about the used meta information} \ + {disagrees with its changeset @ about the meta information} { + SELECT CT.name, C.cid, F.name, R.rev + FROM changeset C, cstype CT, revision R, file F, csitem CI + WHERE C.type = 0 -- revision changesets only + AND C.cid = CI.cid -- changeset --> its revisions + AND R.rid = CI.iid -- look at them + AND R.mid != C.src -- Only those which disagree with changeset about the meta + AND R.fid = F.fid -- get file of the revision + AND CT.tid = C.type -- get changeset type, for labeling + } + # All revisions have to agree on the LOD their changeset + # belongs to. In other words, all revisions in a changeset + # have to refer to the same line of development. + # + # Instead of looking at all pairs of revisions in all + # changesets we generate the distinct set of all LODs + # referenced by the revisions of a changeset, look for those + # with cardinality > 1, and get the identifying information + # for the changesets found thusly. + CheckCS \ + {All revisions in a changeset have to belong to the same LOD} \ + {: Its revisions disagree about the LOD they belong to} { + SELECT T.name, C.cid + FROM changeset C, cstype T + WHERE C.cid IN (SELECT U.cid + FROM (SELECT DISTINCT -- unique cset/lod pairs + CI.cid AS cid, -- revision cset + R.lod AS lod -- lod of item in cset + FROM csitem CI, changeset C, revision R + WHERE CI.iid = R.rid -- get rev of item in cset + AND C.cid = CI.cid -- get changeset of item + AND C.type = 0 -- limit to rev csets + ) AS U + GROUP BY U.cid -- aggregate by cset, count lods/cset + HAVING COUNT(U.lod) > 1 -- find csets with multiple lods + ) + AND T.tid = C.type + } + # All revisions have to agree on the project their changeset + # belongs to. In other words, all revisions in a changeset + # have to refer to the same project. + # + # Instead of looking at all pairs of revisions in all + # changesets we generate the distinct set of all projects + # referenced by the revisions of a changeset, look for those + # with cardinality > 1, and get the identifying information + # for the changesets found thusly. + CheckCS \ + {All revisions in a changeset have to belong to the same project} \ + {: Its revisions disagree about the project they belong to} { + SELECT T.name, C.cid + FROM changeset C, cstype T + WHERE C.cid IN (SELECT U.cid + FROM (SELECT DISTINCT -- unique cset/proj pairs + CI.cid AS cid, -- rev cset + F.pid AS pid -- project of item in cset + FROM csitem CI, changeset C, revision R, file F + WHERE CI.iid = R.rid -- get rev of item in cset + AND C.cid = CI.cid -- get changeset of item + AND C.type = 0 -- limit to rev changesets + AND F.fid = R.fid -- get file of revision + ) AS U + GROUP BY U.cid -- aggregate by csets, count proj/cset + HAVING COUNT(U.pid) > 1 -- find csets with multiple projects + ) + AND T.tid = C.type -- get readable changeset type + } + # All revisions in a single changeset have to belong to + # different files. Conversely: No two revisions of a single + # file are allowed to be in the same changeset. + # + # Instead of looking at all pairs of revisions in all + # changesets we generate the distinct set of all files + # referenced by the revisions of a changeset, and look for + # those with cardinality < the cardinality of the set of + # revisions, and get the identifying information for the + # changesets found thusly. + CheckCS \ + {All revisions in a changeset have to belong to different files} \ + {: Its revisions share files} { + SELECT T.name, C.cid + FROM changeset C, cstype T + WHERE C.cid IN (SELECT VV.cid + FROM (SELECT U.cid AS cid, -- rev changeset + COUNT (U.fid) AS fcount -- number of files by items + FROM (SELECT DISTINCT -- unique cset/file pairs + CI.cid AS cid, -- rev changeset + R.fid AS fid -- file of item in changeset + FROM csitem CI, changeset C, revision R + WHERE CI.iid = R.rid -- get rev of item in changeset + AND C.cid = CI.cid -- get changeset of item + AND C.type = 0 -- limit to rev csets + ) AS U + GROUP BY U.cid -- aggregate by csets, count files/cset + ) AS UU, + (SELECT V.cid AS cid, -- rev changeset + COUNT (V.iid) AS rcount -- number of items + FROM csitem V, changeset X + WHERE X.cid = V.cid -- get changeset of item + AND X.type = 0 -- limit to rev csets + GROUP BY V.cid -- aggregate by csets, count items/cset + ) AS VV + WHERE VV.cid = UU.cid -- sync #items/cset with #files/cset + AND UU.fcount < VV.rcount -- less files than items + -- => items belong to the same file. + ) + AND T.tid = C.type -- get readable changeset type + } + return + } + + proc TagChangesets {} { + # This code performs a number of paranoid checks of the + # database, searching for inconsistent changeset/revision + # information. + + upvar 1 n n ; # Counter for the checks (we print an id before + # the main label). + + # Find all tags which are not used by at least one changeset. + CheckTag \ + {All tags have to be used by least one changeset} \ + {is not used by a tag symbol changeset} { + -- Unused tags = All tags + -- - revisions used by tag changesets. + -- + -- Both sets can be computed easily, and subtracted + -- from each other. Then we can get the associated + -- file (name) for display. + + SELECT P.name, S.name + FROM project P, tag T, symbol S + WHERE T.tid IN (SELECT tid -- All tags + FROM tag + EXCEPT -- subtract + SELECT CI.iid -- tags used + FROM csitem CI, changeset C + WHERE C.cid = CI.cid -- by any tag + AND C.type = 1) -- changeset + AND S.sid = T.sid -- get symbol of tag + AND P.pid = S.pid -- get project of symbol + } + # Find all tags which are used by more than one changeset. + CheckRev \ + {All tags have to be used by at most one changeset} \ + {is used by multiple changesets} { + -- Principle of operation: Get all tag/changeset pairs + -- for all tag changesets, group by tag to aggregate + -- the changeset, counting them. From the resulting + -- tag/count table select those with more than one + -- user, and get their associated file (name) for + -- display. + + SELECT P.name, S.name + FROM tag T, project P, symbol S, + (SELECT CI.iid AS iid, -- item + count(CI.cid) AS count -- number of csets using item + FROM csitem CI, changeset C + WHERE C.type = 1 -- limit to tag csets + AND C.cid = CI.cid -- get items of cset + GROUP BY CI.iid -- aggregate by item, count csets/item + ) AS U + WHERE U.count > 1 -- find tag item used multiple times + AND T.tid = U.iid -- get tag of item + AND S.sid = T.sid -- get symbol of tag + AND P.pid = S.pid -- get project of symbol + } + if 0 { + # This check is disabled for the moment. Apparently tags + # can cross lines of development, at least if the involved + # LODs are the trunk, and the NTDB. That makes sense, as + # the NTDB revisions are initially logically a part of the + # trunk. The standard check below however does not capture + # this. When I manage to rephrase it to accept this type + # of cross-over it will be re-activated. + + # All tags have to agree on the LOD their changeset + # belongs to. In other words, all tags in a changeset have + # to refer to the same line of development. + # + # Instead of looking at all pairs of tags in all + # changesets we generate the distinct set of all LODs + # referenced by the tags of a changeset, look for those + # with cardinality > 1, and get the identifying + # information for the changesets found thusly. + CheckCS \ + {All tags in a changeset have to belong to the same LOD} \ + {: Its tags disagree about the LOD they belong to} { + SELECT T.name, C.cid + FROM changeset C, cstype T + WHERE C.cid IN (SELECT U.cid + FROM (SELECT DISTINCT CI.cid AS cid, T.lod AS lod + FROM csitem CI, changeset C, tag T + WHERE CI.iid = T.tid + AND C.cid = CI.cid + AND C.type = 1) AS U + GROUP BY U.cid HAVING COUNT(U.lod) > 1) + AND T.tid = C.type + } + } + # All tags have to agree on the project their changeset + # belongs to. In other words, all tags in a changeset have to + # refer to the same project. + # + # Instead of looking at all pairs of tags in all changesets we + # generate the distinct set of all projects referenced by the + # tags of a changeset, look for those with cardinality > 1, + # and get the identifying information for the changesets found + # thusly. + CheckCS \ + {All tags in a changeset have to belong to the same project} \ + {: Its tags disagree about the project they belong to} { + SELECT T.name, C.cid + FROM changeset C, cstype T + WHERE C.cid IN (SELECT U.cid + FROM (SELECT DISTINCT -- unique cset/proj pairs + CI.cid AS cid, -- tag cset + F.pid AS pid -- project of item in cset + FROM csitem CI, changeset C, tag T, file F + WHERE CI.iid = T.tid -- get tag of item in cset + AND C.cid = CI.cid -- get changeset of item + AND C.type = 1 -- limit to tag changesets + AND F.fid = T.fid -- get file of tag + ) AS U + GROUP BY U.cid -- aggregate by csets, count proj/cset + HAVING COUNT(U.pid) > 1 -- find csets with multiple projects + ) + AND T.tid = C.type -- get readable changeset type + } + # All tags in a single changeset have to belong to different + # files. Conversely: No two tags of a single file are allowed + # to be in the same changeset. + # + # Instead of looking at all pairs of tags in all changesets we + # generate the distinct set of all files referenced by the + # tags of a changeset, and look for those with cardinality < + # the cardinality of the set of tags, and get the identifying + # information for the changesets found thusly. + CheckCS \ + {All tags in a changeset have to belong to different files} \ + {: Its tags share files} { + SELECT T.name, C.cid + FROM changeset C, cstype T + WHERE C.cid IN (SELECT VV.cid + FROM (SELECT U.cid AS cid, -- changeset + COUNT (U.fid) AS fcount -- number of files by items + FROM (SELECT DISTINCT -- unique cset/file pairs + CI.cid AS cid, -- tag changeset + T.fid AS fid -- file of item in changeset + FROM csitem CI, changeset C, tag T + WHERE CI.iid = T.tid -- get tag of item in changeset + AND C.cid = CI.cid -- get changeset of item + AND C.type = 1 -- limit to tag changesets + ) AS U + GROUP BY U.cid -- aggregate by csets, count files/cset + ) AS UU, + (SELECT V.cid AS cid, -- changeset + COUNT (V.iid) AS rcount -- number of items in cset + FROM csitem V, changeset X + WHERE X.cid = V.cid -- get changeset of item + AND X.type = 1 -- limit to tag changesets + GROUP BY V.cid -- aggregate by csets, count items/cset + ) AS VV + WHERE VV.cid = UU.cid -- sync #items/cset with #files/cset + AND UU.fcount < VV.rcount -- less files than items + -- => items belong to the same file. + ) + AND T.tid = C.type -- get readable changeset type + } + return + } + + proc BranchChangesets {} { + # This code performs a number of paranoid checks of the + # database, searching for inconsistent changeset/revision + # information. + + upvar 1 n n ; # Counter for the checks (we print an id before + # the main label). + + # Find all branches which are not used by at least one + # changeset. + CheckBranch \ + {All branches have to be used by least one changeset} \ + {is not used by a branch symbol changeset} { + -- Unused branches = All branches + -- - branches used by branch changesets. + -- + -- Both sets can be computed easily, and subtracted + -- from each other. Then we can get the associated + -- file (name) for display. + + SELECT P.name, S.name + FROM project P, branch B, symbol S + WHERE B.bid IN (SELECT bid -- All branches + FROM branch + EXCEPT -- subtract + SELECT CI.iid -- branches used + FROM csitem CI, changeset C + WHERE C.cid = CI.cid -- by any branch + AND C.type = 2 -- changeset + ) + AND S.sid = B.sid -- get symbol of branch + AND P.pid = S.pid -- get project of symbol + } + # Find all branches which are used by more than one changeset. + CheckRev \ + {All branches have to be used by at most one changeset} \ + {is used by multiple changesets} { + -- Principle of operation: Get all branch/changeset + -- pairs for all branch changesets, group by tag to + -- aggregate the changeset, counting them. From the + -- resulting branch/count table select those with more + -- than one user, and get their associated file (name) + -- for display. + + SELECT P.name, S.name + FROM branch B, project P, symbol S, + (SELECT CI.iid AS iid, -- item + count(CI.cid) AS count -- number of csets for item + FROM csitem CI, changeset C + WHERE C.type = 2 -- limit to branch changesets, + AND C.cid = CI.cid -- get the items they contain, + GROUP BY CI.iid -- aggregate by items, count csets/item (x) + ) AS U + WHERE U.count > 1 -- find items used multiple times + AND B.bid = U.iid -- get the users (branch changesets) + AND S.sid = B.sid -- get symbol of branch + AND P.pid = S.pid -- get project of symbol + } + if 0 { + # This check has been disabled. When the converter was run + # on the Tcl CVS several branches tripped this + # constraint. One of them was a free-floating branch, and + # its handling has been fixed by now. The others however + # seem semi-legitimate, in the sense that they show + # inconsistencies in the CVS history the user is not + # really able to solve, but it might be possible to simply + # ignore them. + + # For example in Tcl we have a branch X with a prefered + # parent Y, except for a single file where the prefered + # parent seems to be created after its current parent, + # making re-parenting impossible. However we may be able + # to ignore this, it should only cause the branch to have + # more than one predecessor, and shifting it around in the + # commit order. The backend would still use the prefered + # parent for the attachment point in fossil. + + # So, for now I have decided to disable this and press + # forward. Of course, if we run into actual trouble we + # will have to go back here see what can be done to fix + # this. Even if only giving the user the instruction how + # to edit the CVS repository to remove the inconsistency. + + # All branches have to agree on the LOD their changeset + # belongs to. In other words, all branches in a changeset + # have to refer to the same line of development. + # + # Instead of looking at all pairs of branches in all + # changesets we generate the distinct set of all LODs + # referenced by the branches of a changeset, look for + # those with cardinality > 1, and get the identifying + # information for the changesets found thusly. + CheckCS \ + {All branches in a changeset have to belong to the same LOD} \ + {: Its branches disagree about the LOD they belong to} { + SELECT T.name, C.cid + FROM changeset C, cstype T + WHERE C.cid IN (SELECT U.cid + FROM (SELECT DISTINCT CI.cid AS cid, B.lod AS lod + FROM csitem CI, changeset C, branch B + WHERE CI.iid = B.bid + AND C.cid = CI.cid + AND C.type = 2) AS U + GROUP BY U.cid HAVING COUNT(U.lod) > 1) + AND T.tid = C.type + } + } + # All branches have to agree on the project their changeset + # belongs to. In other words, all branches in a changeset have + # to refer to the same project. + # + # Instead of looking at all pairs of branches in all + # changesets we generate the distinct set of all projects + # referenced by the branches of a changeset, look for those + # with cardinality > 1, and get the identifying information + # for the changesets found thusly. + CheckCS \ + {All branches in a changeset have to belong to the same project} \ + {: Its branches disagree about the project they belong to} { + SELECT T.name, C.cid + FROM changeset C, cstype T + WHERE C.cid IN (SELECT U.cid + FROM (SELECT DISTINCT -- Unique cset/proj pairs + CI.cid AS cid, -- Branch cset + F.pid AS pid -- Project of item in cset + FROM csitem CI, changeset C, branch B, file F + WHERE CI.iid = B.bid -- get branch of item in cset + AND C.cid = CI.cid -- get changeset of item + AND C.type = 2 -- limit to branch changesets + AND F.fid = B.fid -- get file of branch + ) AS U + GROUP BY U.cid -- aggregate by csets, count proj/cset + HAVING COUNT(U.pid) > 1 -- find cset with multiple projects + ) + AND T.tid = C.type -- get readable changeset type + } + # All branches in a single changeset have to belong to + # different files. Conversely: No two branches of a single + # file are allowed to be in the same changeset. + # + # Instead of looking at all pairs of branches in all + # changesets we generate the distinct set of all files + # referenced by the branches of a changeset, and look for + # those with cardinality < the cardinality of the set of + # branches, and get the identifying information for the + # changesets found thusly. + CheckCS \ + {All branches in a changeset have to belong to different files} \ + {: Its branches share files} { + SELECT T.name, C.cid + FROM changeset C, cstype T + WHERE C.cid IN (SELECT VV.cid + FROM (SELECT U.cid AS cid, -- changeset + COUNT (U.fid) AS fcount -- number of files by items + FROM (SELECT DISTINCT -- unique cset/file pairs + CI.cid AS cid, -- Branch changeset + B.fid AS fid -- File of item in changeset + FROM csitem CI, changeset C, branch B + WHERE CI.iid = B.bid -- get tag of item in changeset + AND C.cid = CI.cid -- get changeset of item + AND C.type = 2 -- limit to branch changesets + ) AS U + GROUP BY U.cid -- aggregate by csets, count files/cset + ) AS UU, + (SELECT V.cid AS cid, -- changeset + COUNT (V.iid) AS rcount -- number of items in cset + FROM csitem V, changeset X + WHERE X.cid = V.cid -- get changeset of item + AND X.type = 2 -- limit to branch changesets + GROUP BY V.cid -- aggregate by csets, count items/cset + ) AS VV + WHERE VV.cid = UU.cid -- sync #items/cset with #files/cset + AND UU.fcount < VV.rcount -- less files than items + -- => items belong to the same file. + ) + AND T.tid = C.type -- get readable changeset type + } + return + } + + proc ___UnusedChangesetChecks___ {} { + # This code performs a number of paranoid checks of the + # database, searching for inconsistent changeset/revision + # information. + + return ; # Disabled for now, bottlenecks ... + + upvar 1 n n ; # Counter for the checks (we print an id before + # the main label). + + # The next two checks are BOTTLENECKS. In essence we are + # checking each symbol changeset one by one. + + # TODO: Try to rephrase the checks to make more use of + # indices, set and stream operations. + + # All revisions used by tag symbol changesets have to have the + # changeset's tag associated with them. + CheckRevCS \ + {All revisions used by tag symbol changesets have to have the changeset's tag attached to them} \ + {does not have the tag of its symbol changeset @ attached to it} { + SELECT CT.name, C.cid, F.name, R.rev + FROM changeset C, cstype CT, revision R, file F, csitem CI, tag T + WHERE C.type = 1 -- symbol changesets only + AND C.src = T.sid -- tag only, linked by symbol id + AND C.cid = CI.cid -- changeset --> its revisions + AND R.rid = CI.iid -- look at the revisions + -- and look for the tag among the attached ones. + AND T.sid NOT IN (SELECT TB.sid + FROM tag TB + WHERE TB.rev = R.rid) + AND R.fid = F.fid -- get file of revision + } + + # All revisions used by branch symbol changesets have to have + # the changeset's branch associated with them. + + CheckRevCS \ + {All revisions used by branch symbol changesets have to have the changeset's branch attached to them} \ + {does not have the branch of its symbol changeset @ attached to it} { + SELECT CT.name, C.cid, F.name, R.rev, C.cid + FROM changeset C, cstype CT, revision R, file F, csitem CI, branch B + WHERE C.type = 1 -- symbol changesets only + AND C.src = B.sid -- branches only + AND C.cid = CI.cid -- changeset --> its revisions + AND R.rid = CI.iid -- look at the revisions + -- and look for the branch among the attached ones. + AND B.sid NOT IN (SELECT BB.sid + FROM branch BB + WHERE BB.root = R.rid) + AND R.fid = F.fid -- get file of revision + } + + # TODO + # The state has to contain at least one tag symbol changeset + # for all known tags. + + # TODO + # The state has to contain at least one branch symbol changeset + # for all known branches. + return + } + + + proc CheckRev {header label sql} { + upvar 1 n n + set ok 1 + foreach {fname revnr} [state run $sql] { + set ok 0 + trouble fatal "${revnr}::$fname $label" + } + log write 5 integrity {\[[format %02d [incr n]]\] [expr {$ok ? "Ok " : "Failed"}] ... $header} + return + } + + proc CheckTag {header label sql} { + upvar 1 n n + set ok 1 + foreach {pname sname} [state run $sql] { + set ok 0 + trouble fatal "<$pname tag '$sname'> $label" + } + log write 5 integrity {\[[format %02d [incr n]]\] [expr {$ok ? "Ok " : "Failed"}] ... $header} + return + } + + proc CheckBranch {header label sql} { + upvar 1 n n + set ok 1 + foreach {pname sname} [state run $sql] { + set ok 0 + trouble fatal "<$pname branch '$sname'> $label" + } + log write 5 integrity {\[[format %02d [incr n]]\] [expr {$ok ? "Ok " : "Failed"}] ... $header} + return + } + + proc CheckCS {header label sql} { + upvar 1 n n + set ok 1 + foreach {ctype cid} [state run $sql] { + set ok 0 + trouble fatal "<$ctype $cid> $label" + } + log write 5 integrity {\[[format %02d [incr n]]\] [expr {$ok ? "Ok " : "Failed"}] ... $header} + return + } + + proc CheckRevCS {header label sql} { + upvar 1 n n + set ok 1 + foreach {cstype csid fname revnr} [state run $sql] { + set ok 0 + set b "<$cstype $csid>" + trouble fatal "$fname <$revnr> [string map [list @ $b] $label]" + } + log write 5 integrity {\[[format %02d [incr n]]\] [expr {$ok ? "Ok " : "Failed"}] ... $header} + return + } + + # # ## ### ##### ######## ############# + ## Configuration + + pragma -hasinstances no ; # singleton + pragma -hastypeinfo no ; # no introspection + pragma -hastypedestroy no ; # immortal + + # # ## ### ##### ######## ############# +} + +namespace eval ::vc::fossil::import::cvs { + namespace export integrity + namespace eval integrity { + namespace import ::vc::fossil::import::cvs::state + namespace import ::vc::tools::trouble + namespace import ::vc::tools::log + log register integrity + } +} + +# # ## ### ##### ######## ############# ##################### +## Ready + +package provide vc::fossil::import::cvs::integrity 1.0 +return diff --git a/tools/cvs2fossil/lib/c2f_option.tcl b/tools/cvs2fossil/lib/c2f_option.tcl new file mode 100644 index 0000000..71fbbc8 --- /dev/null +++ b/tools/cvs2fossil/lib/c2f_option.tcl @@ -0,0 +1,284 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Option database, processes the command line. Note that not all of +## the option information is stored here. Parts are propagated to +## other pieces of the system and handled there, via option +## delegation + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime. +package require snit ; # OO system. +package require fileutil ; # Setting a tempdir. +package require vc::tools::trouble ; # Error reporting. +package require vc::tools::log ; # User feedback. +package require vc::tools::mem ; # Memory tracking. +package require vc::tools::misc ; # Misc. path reformatting. +package require vc::fossil::import::cvs::fossil ; # Fossil repository access +package require vc::fossil::import::cvs::pass ; # Pass management +package require vc::fossil::import::cvs::pass::collar ; # Pass I. +package require vc::fossil::import::cvs::repository ; # Repository management +package require vc::fossil::import::cvs::state ; # State storage +package require vc::fossil::import::cvs::integrity ; # State integrity checks. +package require vc::fossil::import::cvs::project::sym ; # Project level symbols +package require vc::fossil::import::cvs::cyclebreaker ; # Breaking dependency cycles. + +# # ## ### ##### ######## ############# ##################### +## + +snit::type ::vc::fossil::import::cvs::option { + # # ## ### ##### ######## ############# + ## Public API, Options. + + # --help, --help-passes, -h + # --version + # -p, --pass, --passes + # --ignore-conflicting-attics + # --convert-dotfiles + # --project + # -v, --verbose + # -q, --quiet + # --state (conversion status, ala config.cache) + # --trunk-only + # --exclude, --force-tag, --force-branch + # --batch + # --fossil PATH + + # -o, --output + # --dry-run + # --symbol-transform RE:XX + + # # ## ### ##### ######## ############# + ## Public API, Methods + + typemethod process {arguments} { + # Syntax of arguments: ?option ?value?...? /path/to/cvs/repository + + while {[IsOption arguments -> option]} { + switch -exact -- $option { + -h - + --help { PrintHelp ; exit 0 } + --help-passes { pass help ; exit 0 } + --version { PrintVersion ; exit 0 } + -p - + --pass - + --passes { pass select [Value arguments] } + --ignore-conflicting-attics { collar ignore_conflicting_attics } + --convert-dotfiles { collar accept_and_convert_dotfiles } + --project { repository add [Value arguments] } + -v - + --verbose { log verbose } + -q - + --quiet { log quiet } + --state { state usedb [Value arguments] } + --trunk-only { repository trunkonly! } + --exclude { project::sym exclude [Value arguments] } + --force-tag { project::sym forcetag [Value arguments] } + --force-branch { project::sym forcebranch [Value arguments] } + --batch { log noprogress } + --dots { cyclebreaker dotsto [Value arguments] } + --watch { cyclebreaker watch [Value arguments] } + --statesavequeriesto { state savequeriesto [Value arguments] } + --fossil { fossil setlocation [Value arguments] } + --memory-limit { mem::setlimit [Value arguments] } + --memory-track { mem::track } + -t - + --tempdir { fileutil::tempdir [Value arguments] } + default { + Usage $badoption$option\n$gethelp + } + } + } + + if {[llength $arguments] > 1} Usage + if {[llength $arguments] < 1} { Usage $nocvs } + repository base [striptrailingslash [lindex $arguments 0]] + + Validate + return + } + + # # ## ### ##### ######## ############# + ## Internal methods, printing information. + + proc PrintHelp {} { + global argv0 + trouble info "Usage: $argv0 $usage" + trouble info "" + trouble info " Information options" + trouble info "" + trouble info " -h, --help Print this message and exit with success" + trouble info " --help-passes Print list of passes and exit with success" + trouble info " --version Print version number of $argv0" + trouble info " -v, --verbose Increase application's verbosity" + trouble info " -q, --quiet Decrease application's verbosity" + trouble info " --batch Disable the progress feedback standard to" + trouble info " interactive use." + trouble info "" + trouble info " Conversion control options" + trouble info "" + trouble info " -p, --pass PASS Run only the specified conversion pass" + trouble info " -p, --passes ?START?:?END? Run only the passes START through END," + trouble info " inclusive." + trouble info "" + trouble info " Passes are specified by name." + trouble info "" + trouble info " --ignore-conflicting-attics" + trouble info " Prevent abort when conflicting archives" + trouble info " were found in both regular and Attic." + trouble info "" + trouble info " --convert-dotfiles" + trouble info " Prevent abort when dot-files were found," + trouble info " causing their conversion to nondot-form" + trouble info " instead." + trouble info "" + trouble info " --state PATH Save state to the specified file, and" + trouble info " load state of previous runs from it too." + trouble info "" + trouble info " --exclude ?PROJECT:?SYMBOL Exclude the named symbol from all or" + trouble info " just the specified project. Both project" + trouble info " and symbol names are glob patterns." + trouble info "" + trouble info " --force-tag ?PROJECT:?SYMBOL" + trouble info " Force the named symbol from all or just" + trouble info " the specified project to be converted as" + trouble info " tag. Both project and symbol names are" + trouble info " glob patterns." + trouble info "" + trouble info " --force-branch ?PROJECT:?SYMBOL" + trouble info " Force the named symbol from all or just" + trouble info " the specified project to be converted as" + trouble info " branch. Both project and symbol names" + trouble info " are glob patterns." + trouble info "" + trouble info " Output control options" + trouble info "" + trouble info " --fossil PATH Specify where to find the fossil execu-" + trouble info " table if cv2fossil could not find it in" + trouble info " the PATH." + trouble info "" + trouble info " --tempdir PATH, -t PATH Specify the path where temporary files" + trouble info " and directories shall go." + trouble info "" + trouble info " Debug options" + trouble info "" + trouble info " --dots PATH Write the changeset graphs before, after," + trouble info " and during breaking the of cycles to the" + trouble info " directory PATH, using GraphViz's dot format" + trouble info "" + trouble info " --memory-track Activate internal tracking of memory usage." + trouble info " Requires execution of cvs2fossil by a tclsh" + trouble info " which provides the \[memory\] command." + trouble info "" + trouble info " --memory-limit BYTES Like --memory-track, but additionally imposes" + trouble info " a limit on the maximual amount of memory the" + trouble info " application is allowed to use." + trouble info "" + + # --project, --cache + # ... + return + } + + proc PrintVersion {} { + global argv0 + set v [package require vc::fossil::import::cvs] + trouble info "$argv0 v$v" + return + } + + proc Usage {{text {}}} { + global argv0 + trouble fatal "Usage: $argv0 $usage" + if {$text ne ""} { trouble fatal "$text" } + exit 1 + } + + # # ## ### ##### ######## ############# + ## Internal methods, command line processing + + typevariable usage "?option ?value?...? cvs-repository-path" + typevariable nocvs " The cvs-repository-path is missing." + typevariable badoption " Bad option " + typevariable gethelp " Use --help to get help." + + proc IsOption {av _ ov} { + upvar 1 $av arguments $ov option + set candidate [lindex $arguments 0] + if {![string match -* $candidate]} {return 0} + set option $candidate + set arguments [lrange $arguments 1 end] + return 1 + } + + proc Value {av} { + upvar 1 $av arguments + set v [lindex $arguments 0] + set arguments [lrange $arguments 1 end] + return $v + } + + # # ## ### ##### ######## ############# + ## Internal methods, state validation + + proc Validate {} { + # Prevent in-depth validation if the options were already bad. + trouble abort? + + fossil validate + repository validate + state setup + + trouble abort? + return + } + + # # ## ### ##### ######## ############# + ## Configuration + + pragma -hasinstances no ; # singleton + pragma -hastypeinfo no ; # no introspection + pragma -hastypedestroy no ; # immortal + + # # ## ### ##### ######## ############# +} + +namespace eval ::vc::fossil::import::cvs { + namespace export option + namespace eval option { + namespace import ::vc::tools::misc::striptrailingslash + namespace import ::vc::fossil::import::cvs::fossil + namespace import ::vc::fossil::import::cvs::pass + namespace import ::vc::fossil::import::cvs::pass::collar + namespace import ::vc::fossil::import::cvs::cyclebreaker + namespace import ::vc::fossil::import::cvs::repository + namespace import ::vc::fossil::import::cvs::state + namespace import ::vc::fossil::import::cvs::integrity + namespace eval project { + namespace import ::vc::fossil::import::cvs::project::sym + } + namespace eval mem { + namespace import ::vc::tools::mem::setlimit + namespace import ::vc::tools::mem::track + } + namespace import ::vc::tools::trouble + namespace import ::vc::tools::log + } +} + +# # ## ### ##### ######## ############# ##################### +## Ready + +package provide vc::fossil::import::cvs::option 1.0 +return diff --git a/tools/cvs2fossil/lib/c2f_pass.tcl b/tools/cvs2fossil/lib/c2f_pass.tcl new file mode 100644 index 0000000..bd8b54a --- /dev/null +++ b/tools/cvs2fossil/lib/c2f_pass.tcl @@ -0,0 +1,243 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Pass manager. All passes register here, with code, description, and +## callbacks (... setup, run, finalize). Option processing and help +## query this manager to dynamically create the relevant texts. + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime. +package require snit ; # OO system. +package require vc::fossil::import::cvs::state ; # State storage +package require vc::fossil::import::cvs::integrity ; # State integrity checks. +package require vc::tools::misc ; # Text formatting +package require vc::tools::trouble ; # Error reporting. +package require vc::tools::log ; # User feedback. +package require struct::list ; # Portable lassign + +# # ## ### ##### ######## ############# ##################### +## + +snit::type ::vc::fossil::import::cvs::pass { + # # ## ### ##### ######## ############# + ## Public API, Methods (Setup, query) + + typemethod define {name description command} { + integrity assert { + ![info exists mydesc($name)] + } {Multiple definitions for pass code '$name'} + lappend mypasses $name + set mydesc($name) $description + set mycmd($name) $command + return + } + + typemethod help {} { + trouble info "" + trouble info "Conversion passes:" + trouble info "" + set n 0 + + set clen [max [struct::list map $mypasses {string length}]] + set cfmt %-${clen}s + set nfmt %[string length [llength $mypasses]]s + + foreach code $mypasses { + trouble info " [format $nfmt $n]: [format $cfmt $code] : $mydesc($code)" + incr n + } + trouble info "" + return + } + + # # ## ### ##### ######## ############# + ## Public API, Methods (Execution) + + typemethod select {passdef} { + set pl [split $passdef :] + if {[llength $pl] > 2} { + trouble fatal "Bad pass definition '$passdef'" + trouble fatal "Expected at most one ':'" + } elseif {[llength $pl] == 2} { + struct::list assign $pl start end + + if {($start eq "") && ($end eq "")} { + trouble fatal "Specify at least one of start- or end-pass" + set ok 0 + } else { + set ok 1 + Ok? $start start ok + Ok? $end end ok + } + + if {$ok} { + set mystart [Convert $start 0] + set myend [Convert $end [expr {[llength $mypasses] - 1}]] + if {$mystart > $myend} { + trouble fatal "Start pass is after end pass" + } + } + } elseif {[llength $pl] < 2} { + set start [lindex $pl 0] + Ok? $start "" __dummy__ 0 + set mystart [Id $start] + set myend $mystart + } + } + + typemethod run {} { + if {$mystart < 0} {set mystart 0} + if {$myend < 0} {set myend [expr {[llength $mypasses] - 1}]} + + set skipped [lrange $mypasses 0 [expr {$mystart - 1}]] + set run [lrange $mypasses $mystart $myend] + set defered [lrange $mypasses [expr {$myend + 1}] end] + + foreach p $skipped { + log write 0 pass "Skip $p" + Call $p load + } + foreach p $run { + log write 0 pass "Setup $p" + Call $p setup + } + foreach p $run { + log write 0 pass "Begin $p" + set secbegin [clock seconds] + Call $p run + set secstop [clock seconds] + log write 0 pass "Done $p" + Time $p [expr {$secstop - $secbegin}] + trouble abort? + } + foreach p $defered { + log write 0 pass "Defer $p" + Call $p discard + } + + state release + ShowTimes + return + } + + typemethod current {} { return $mycurrentpass } + + # # ## ### ##### ######## ############# + ## Internal methods + + proc Time {pass seconds} { + ::variable mytime + lappend mytime $pass $seconds + ShowTime $pass $seconds + return + } + + proc ShowTimes {} { + ::variable mytime + set total 0 + foreach {pass seconds} $mytime { + ShowTime $pass $seconds + incr total $seconds + } + ShowTime Total $total + return + } + + proc ShowTime {pass seconds} { + if {$seconds > 3600} { + set hr [expr {$seconds / 3600}] + set min [expr {$seconds % 3600}] + set sec [expr {$min % 60}] + set min [expr {$min / 60}] + + log write 0 pass "[format %8d $seconds] sec/$pass ([nsp $hr hour] [nsp $min minute] [nsp $sec second])" + } elseif {$seconds > 60} { + set min [expr {$seconds / 60}] + set sec [expr {$seconds % 60}] + + log write 0 pass "[format %8d $seconds] sec/$pass ([nsp $min minute] [nsp $sec second])" + } else { + log write 0 pass "[format %8d $seconds] sec/$pass" + } + return + } + + proc Ok? {code label ov {emptyok 1}} { + upvar 1 $ov ok + ::variable mydesc + if {$emptyok && ($code eq "")} return + if {[info exists mydesc($code)]} return + if {$label ne ""} {append label " "} + trouble fatal "Bad ${label}pass code $code" + set ok 0 + return + } + + proc Convert {code default} { + ::variable mypasses + return [expr {($code eq "") ? $default : [Id $code]}] + } + + proc Id {code} { + ::variable mypasses + return [lsearch -exact $mypasses $code] + } + + proc Call {code args} { + ::variable mycmd + set cmd $mycmd($code) + foreach a $args { lappend cmd $a } + eval $cmd + return + } + + # # ## ### ##### ######## ############# + ## Internal, state + + typevariable mypasses {} ; # List of registered passes (codes). + typevariable mydesc -array {} ; # Pass descriptions (one line). + typevariable mycmd -array {} ; # Pass callback command. + + typevariable mystart -1 + typevariable myend -1 + typevariable mytime {} ; # Timing data for each executed pass. + typevariable mycurrentpass {} ; # Pass currently running. + + # # ## ### ##### ######## ############# + ## Configuration + + pragma -hasinstances no ; # singleton + pragma -hastypeinfo no ; # no introspection + pragma -hastypedestroy no ; # immortal + + # # ## ### ##### ######## ############# +} + +namespace eval ::vc::fossil::import::cvs { + namespace export pass + namespace eval pass { + namespace import ::vc::fossil::import::cvs::state + namespace import ::vc::fossil::import::cvs::integrity + namespace import ::vc::tools::misc::* + namespace import ::vc::tools::trouble + namespace import ::vc::tools::log + log register pass + } +} + +# # ## ### ##### ######## ############# ##################### +## Ready + +package provide vc::fossil::import::cvs::pass 1.0 +return diff --git a/tools/cvs2fossil/lib/c2f_patopsort.tcl b/tools/cvs2fossil/lib/c2f_patopsort.tcl new file mode 100644 index 0000000..e0dd439 --- /dev/null +++ b/tools/cvs2fossil/lib/c2f_patopsort.tcl @@ -0,0 +1,210 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Pass XI. This pass goes over all changesets and sorts them +## topologically. It assumes that there are no cycles which could +## impede it, any remaining having been broken by the previous two +## passes, and aborts if that condition doesn't hold. + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime. +package require snit ; # OO system. +package require struct::list ; # Higher order list operations. +package require vc::tools::log ; # User feedback. +package require vc::fossil::import::cvs::cyclebreaker ; # Breaking dependency cycles. +package require vc::fossil::import::cvs::state ; # State storage. +package require vc::fossil::import::cvs::project::rev ; # Project level changesets + +# # ## ### ##### ######## ############# ##################### +## Register the pass with the management + +vc::fossil::import::cvs::pass define \ + AllTopologicalSort \ + {Topologically Sort All ChangeSets} \ + ::vc::fossil::import::cvs::pass::atopsort + +# # ## ### ##### ######## ############# ##################### +## + +snit::type ::vc::fossil::import::cvs::pass::atopsort { + # # ## ### ##### ######## ############# + ## Public API + + typemethod setup {} { + # Define the names and structure of the persistent state of + # this pass. + + state use revision + state use tag + state use branch + state use symbol + state use changeset + state use csitem + state use cssuccessor + state use csorder + + state extend cstimestamp { + -- Commit order of all changesets based on their + -- dependencies, plus a monotonically increasing + -- timestamp. + + cid INTEGER NOT NULL REFERENCES changeset, + pos INTEGER NOT NULL, + date INTEGER NOT NULL, + UNIQUE (cid), + UNIQUE (pos), + UNIQUE (date) + } + return + } + + typemethod load {} { + # Pass manager interface. Executed to load data computed by + # this pass into memory when this pass is skipped instead of + # executed. + return + } + + typemethod run {} { + # Pass manager interface. Executed to perform the + # functionality of the pass. + + set len [string length [project::rev num]] + set myatfmt %${len}s + incr len 12 + set mycsfmt %${len}s + + cyclebreaker savecmd [myproc SaveTimestamps] + + state transaction { + LoadSymbolChangesets + cyclebreaker run tsort-all [myproc Changesets] + } + return + } + + typemethod discard {} { + # Pass manager interface. Executed for all passes after the + # run passes, to remove all data of this pass from the state, + # as being out of date. + + state discard cstimestamp + return + } + + # # ## ### ##### ######## ############# + ## Internal methods + + proc Changesets {} { project::rev all } + + proc LoadSymbolChangesets {} { + # Consider use of 'project::rev sym' here. + set mysymchangesets [struct::list filter [project::rev all] [myproc IsBySymbol]] + return + } + + proc IsBySymbol {cset} { $cset bysymbol } + + proc SaveTimestamps {graph at cset} { + set cid [$cset id] + + set date [GetTime [lindex [$graph node get $cset timerange] 1] \ + [struct::set contains $mysymchangesets $cset] \ + message] + + log write 4 atopsort "Changeset @ [format $myatfmt $at]: [format $mycsfmt [$cset str]] '[$cset lod]' $message" + + state run { + INSERT INTO cstimestamp (cid, pos, date) + VALUES ($cid, $at, $date) + } + return + } + + proc GetTime {stamp expectchange mv} { + ::variable mylasttimestamp + upvar 1 $mv message + set message "" + if {$stamp > $mymaxtimestamp} { + # A timestamp in the future is believed to be bogus and + # shifted backwars in time to prevent it from forcing + # other timestamps to be pushed even further in the + # future. + + # From cvs2svn: Note that this is not nearly a complete + # solution to the bogus timestamp problem. A timestamp in + # the future still affects the ordering of changesets, and + # a changeset having such a timestamp will not be + # committed until all changesets with earlier timestamps + # have been committed, even if other changesets with even + # earlier timestamps depend on this one. + + incr mylasttimestamp + if {!$expectchange} { + set message " Timestamp [clock format $stamp] is in the future; shifted back to [clock format $mylasttimestamp] ([expr {$mylasttimestamp - $stamp}])" + } + } elseif {$stamp < ($mylasttimestamp)+1} { + incr mylasttimestamp + if {!$expectchange} { + set message " Timestamp [clock format $stamp] adjusted to [clock format $mylasttimestamp] (+[expr {$mylasttimestamp - $stamp}])" + } + } else { + set mylasttimestamp $stamp + } + return $mylasttimestamp + } + + typevariable myatfmt ; # Format for log output to gain better alignment of the various columns. + typevariable mycsfmt ; # Ditto for the changesets. + + typevariable mysymchangesets {} ; # Set of the symbol changesets. + typevariable mylasttimestamp 0 ; # Last delivered timestamp. + typevariable mymaxtimestamp + + typeconstructor { + # The maximum timestamp considered as reasonable is + # "now + 1 day". + set mymaxtimestamp [clock seconds] + incr mymaxtimestamp 86400 ; # 24h * 60min * 60sec + return + } + + # # ## ### ##### ######## ############# + ## Configuration + + pragma -hasinstances no ; # singleton + pragma -hastypeinfo no ; # no introspection + pragma -hastypedestroy no ; # immortal + + # # ## ### ##### ######## ############# +} + +namespace eval ::vc::fossil::import::cvs::pass { + namespace export atopsort + namespace eval atopsort { + namespace import ::vc::fossil::import::cvs::cyclebreaker + namespace import ::vc::fossil::import::cvs::state + namespace eval project { + namespace import ::vc::fossil::import::cvs::project::rev + } + namespace import ::vc::tools::log + log register atopsort + } +} + +# # ## ### ##### ######## ############# ##################### +## Ready + +package provide vc::fossil::import::cvs::pass::atopsort 1.0 +return diff --git a/tools/cvs2fossil/lib/c2f_pbreakacycle.tcl b/tools/cvs2fossil/lib/c2f_pbreakacycle.tcl new file mode 100644 index 0000000..59841c1 --- /dev/null +++ b/tools/cvs2fossil/lib/c2f_pbreakacycle.tcl @@ -0,0 +1,433 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Pass X. This is the final pass for breaking changeset dependency +## cycles. The previous breaker passes (7 and 9) broke cycles covering +## revision and symbol changesets, respectively. This pass now breaks +## any remaining cycles, each of which has to contain at least one +## revision and at least one symbol changeset. + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime. +package require snit ; # OO system. +package require struct::list ; # Higher order list operations. +package require struct::set ; # Set operations. +package require vc::tools::misc ; # Min, max. +package require vc::tools::log ; # User feedback. +package require vc::tools::trouble ; # Error reporting. +package require vc::fossil::import::cvs::repository ; # Repository management. +package require vc::fossil::import::cvs::cyclebreaker ; # Breaking dependency cycles. +package require vc::fossil::import::cvs::state ; # State storage. +package require vc::fossil::import::cvs::integrity ; # State integrity checks. +package require vc::fossil::import::cvs::project::rev ; # Project level changesets + +# # ## ### ##### ######## ############# ##################### +## Register the pass with the management + +vc::fossil::import::cvs::pass define \ + BreakAllCsetCycles \ + {Break Remaining ChangeSet Dependency Cycles} \ + ::vc::fossil::import::cvs::pass::breakacycle + +# # ## ### ##### ######## ############# ##################### +## + +snit::type ::vc::fossil::import::cvs::pass::breakacycle { + # # ## ### ##### ######## ############# + ## Public API + + typemethod setup {} { + # Define the names and structure of the persistent state of + # this pass. + + state use revision + state use tag + state use branch + state use symbol + state use changeset + state use csitem + state use cssuccessor + return + } + + typemethod load {} { + # Pass manager interface. Executed to load data computed by + # this pass into memory when this pass is skipped instead of + # executed. + return + } + + typemethod run {} { + # Pass manager interface. Executed to perform the + # functionality of the pass. + + set len [string length [project::rev num]] + set myatfmt %${len}s + incr len 12 + set mycsfmt %${len}s + + cyclebreaker precmd [myproc BreakBackward] + cyclebreaker savecmd [myproc KeepOrder] + + state transaction { + LoadCommitOrder + cyclebreaker run break-all [myproc Changesets] + } + + repository printcsetstatistics + integrity changesets + return + } + + typemethod discard {} { + # Pass manager interface. Executed for all passes after the + # run passes, to remove all data of this pass from the state, + # as being out of date. + return + } + + # # ## ### ##### ######## ############# + ## Internal methods + + proc Changesets {} { + log write 2 breakrcycle {Selecting all changesets} + return [project::rev all] + } + + proc LoadCommitOrder {} { + ::variable mycset + ::variable myrevisionchangesets + + log write 2 breakacycle {Loading revision commit order} + + set n 0 + state transaction { + state foreachrow { + SELECT cid, pos FROM csorder + } { + log progress 2 breakacycle $n {} + set cset [project::rev of $cid] + $cset setpos $pos + set mycset($pos) $cset + lappend myrevisionchangesets $cset + incr n + } + } + return + } + + # # ## ### ##### ######## ############# + + proc BreakBackward {graph} { + # We go over all branch changesets, i.e. the changesets + # created by the symbols which are translated as branches, and + # break any which are 'backward', which means that they have + # at least one incoming revision changeset which is committed + # after at least one of the outgoing revision changesets, per + # the order computed in pass 6. In "cvs2svn" this is called + # "retrograde". + + set n 0 + set max [llength [$graph nodes]] + foreach cset [$graph nodes] { + log progress 2 breakacycle $n $max ; incr n + if {![$cset isbranch]} continue + CheckAndBreakBackward $graph $cset + } + return + } + + proc CheckAndBreakBackward {graph cset} { + while {[IsBackward $graph $cset]} { + # Knowing that the branch changeset is backward we now + # look at the individual branches in the changeset and + # determine which of them are responsible for the + # overlap. This allows us to split them into two sets, one + # of non-overlapping branches, and of overlapping + # ones. Each set induces a new changeset, and the second + # one may still be backward and in need of further + # splitting. Hence the looping. + + # The border used for the split is the minimal commit + # position among the minimal sucessor commit positions for + # the branches in the changeset. We sort the file level + # items based on there they sit relative to the border + # into before and after the border. As the branches cannot + # be backward at file level thos before the border cannot + # generate a backward symbol changeset, however the + # branches after may constitute another backward branch + # with a new border. + + # limits : dict (revision -> list (max predecessor commit, min sucessor commit)) + + ComputeLimits $cset limits border + + log write 5 breakacycle "Breaking backward changeset [$cset str] using commit position $border as border" + + SplitItems $limits $border normalitems backwarditems + + set replacements [project::rev split $cset $normalitems $backwarditems] + cyclebreaker replace $graph $cset $replacements + + # At last we check that the normal frament is indeed not + # backward, and iterate over the possibly still backward + # second fragment. + + struct::list assign $replacements normal backward + integrity assert { + ![IsBackward $graph $normal] + } {The normal fragment is unexpectedly backward} + + set cset $backward + } + return + } + + proc IsBackward {dg cset} { + # A branch is "backward" if it has at least one incoming + # revision changeset which is committed after at least one of + # the outgoing revision changesets, per the order computed by + # pass 6. + + # Rephrased, the maximal commit position found among the + # incoming revision changesets is larger than the minimal + # commit position found among the outgoing revision + # changesets. Assuming that we have both incoming and outgoing + # revision changesets for the branch. + + # The helper "Positions" computes the set of commit positions + # for a set of changesets, which can be a mix of revision and + # symbol changesets. + + set predecessors [Positions [$dg nodes -in $cset]] + set successors [Positions [$dg nodes -out $cset]] + + return [expr { + [llength $predecessors] && + [llength $successors] && + ([max $predecessors] >= [min $successors]) + }] + } + + proc Positions {changesets} { + # To compute the set of commit positions from the set of + # changesets we first map each changeset to its position (*) + # and then filter out the invalid responses (the empty string) + # returned by the symbol changesets. + # + # (*) This data was loaded into memory earlir in the pass, by + # LoadCommitOrder. + + return [struct::list filter [struct::list map $changesets \ + [myproc ToPosition]] \ + [myproc ValidPosition]] + } + + proc ToPosition {cset} { $cset pos } + proc ValidPosition {pos} { expr {$pos ne ""} } + + proc ComputeLimits {cset lv bv} { + upvar 1 $lv thelimits $bv border + + # Individual branches may not have revision changesets which + # are their predecessors and/or successors, leaving the limits + # partially or completely undefined. To overcome this + # initialize boundaries for all items with proper defaults (-1 + # for max, {} for min, representing +infinity). + + array set maxpa {} + array set minsa {} + foreach item [$cset items] { + set maxpa($item) -1 + set minsa($item) {} + } + + # Get the limits from the database, for the items which + # actually have such, and merge the information with the + # defaults. + + struct::list assign [$cset limits] maxpdict minsdict + + array set maxpa $maxpdict + array set minsa $minsdict + + # Check that the ordering at the file level is correct. We + # cannot have backward ordering per branch, or something is + # wrong. + + foreach item [array names limits] { + set mins $minsa($item) + set maxp $maxp($item) + # Note that for the min successor position "" represents + # +infinity + integrity assert { + ($mins eq "") || ($maxp < $mins) + } {Item <$item> is backward at file level ($maxp >= $mins)} + } + + # Save the limits for the splitter, and compute the border at + # which to split as the minimum of all minimal successor + # positions. + + # Compute the border at which to split as the minimum of all + # minimal successor positions. By using the database info we + # automatically/implicitly filter out anything without a min + # successor. Further the data going into the comparison with + # the border is put together. + + set border [min [Values $minsdict]] + set thelimits [array get maxpa] + return + } + + proc Values {dict} { + set res {} + foreach {k v} $dict { lappend res $v } + return $res + } + + proc SplitItems {limits border nv bv} { + upvar 1 $nv normalitems $bv backwarditems + + set normalitems {} + set backwarditems {} + + foreach {item maxp} $limits { + if {$maxp >= $border} { + lappend backwarditems $item + } else { + lappend normalitems $item + } + } + + integrity assert {[llength $normalitems]} {Set of normal items is empty} + integrity assert {[llength $backwarditems]} {Set of backward items is empty} + return + } + + # # ## ### ##### ######## ############# + + proc KeepOrder {graph at cset} { + ::variable myatfmt + ::variable mycsfmt + + set cid [$cset id] + + log write 8 breakacycle "Changeset @ [format $myatfmt $at]: [format $mycsfmt [$cset str]] <<[FormatTR $graph $cset]>>" + + # We see here a mixture of symbol and revision changesets. + # The symbol changesets are ignored as irrelevant. + + if {[$cset pos] eq ""} return + + # For the revision changesets we are sure that they are + # consumed in the same order as generated by pass 7 + # (RevTopologicalSort). Per the code in cvs2svn. + + # This works if and only if none of the symbol changesets are + # "backwards", hence our breaking of the backward changesets + # first, in the pre-hook. + + # Note that tag changesets cannot be backward as they don't + # have successors at all. + + # An interesting thing IMHO, is that after breaking the + # backward symbol changesets we should not have any circles + # any longer. Each circle which would still be present has to + # involve a backward symbol, and we split them all, so there + # can't be a circle.. + + # Proof: + # Let us assume we that have a circle + # C: R1 -> ... -> Rx -> S -> Ry -> ... -> Rn -> R1 + # Let us further assume that the symbol changeset S in that + # circle is not backward. That means ORD(Rx) < ORD(Ry). The + # earlier topological sorting without symbols now forces this + # relationship through to be ORD(Rx) < ORD(R1) < ORD(Rx). We + # have reached an impossibility, a paradox. Our initial + # assumption of S not being backward cannot hold. + # + # Alternate, direct, reasoning: Without S the chain of + # dependencies is Ry -> .. -> R1 -> .. -> Rx, therefore + # ORD(Ry) < ORD(Rx) holds, and this means S is backward. + + struct::set exclude myrevisionchangesets $cset + + ::variable mylastpos + set new [$cset pos] + + if {$new != ($mylastpos + 1)} { + if {$mylastpos < 0} { + set old "<NONE>" + } else { + ::variable mycset + set old [$mycset($mylastpos) str]@$mylastpos + } + + #integrity assert 0 {Ordering of revision changesets violated, [$cset str]@$new is not immediately after $old} + log write 2 breakacycle {Ordering of revision changesets violated, [$cset str]@$new is not immediately after $old} + } + + set mylastpos $new + return + } + + proc FormatTR {graph cset} { + return [join [struct::list map [$graph node set $cset timerange] {clock format}] { -- }] + } + + typevariable mylastpos -1 ; # Position of last revision changeset saved. + typevariable myrevisionchangesets {} ; # Set of revision changesets + + typevariable myatfmt ; # Format for log output to gain better alignment of the various columns. + typevariable mycsfmt ; # Ditto for the changesets. + + # # ## ### ##### ######## ############# + + typevariable mycset -array {} ; # Map from commit positions to the + # changeset (object ref) at that + # position. + + # # ## ### ##### ######## ############# + ## Configuration + + pragma -hasinstances no ; # singleton + pragma -hastypeinfo no ; # no introspection + pragma -hastypedestroy no ; # immortal + + # # ## ### ##### ######## ############# +} + +namespace eval ::vc::fossil::import::cvs::pass { + namespace export breakacycle + namespace eval breakacycle { + namespace import ::vc::fossil::import::cvs::cyclebreaker + namespace import ::vc::fossil::import::cvs::repository + namespace import ::vc::fossil::import::cvs::state + namespace import ::vc::fossil::import::cvs::integrity + namespace eval project { + namespace import ::vc::fossil::import::cvs::project::rev + } + namespace import ::vc::tools::misc::* + namespace import ::vc::tools::trouble + namespace import ::vc::tools::log + log register breakacycle + } +} + +# # ## ### ##### ######## ############# ##################### +## Ready + +package provide vc::fossil::import::cvs::pass::breakacycle 1.0 +return diff --git a/tools/cvs2fossil/lib/c2f_pbreakrcycle.tcl b/tools/cvs2fossil/lib/c2f_pbreakrcycle.tcl new file mode 100644 index 0000000..474cd4a --- /dev/null +++ b/tools/cvs2fossil/lib/c2f_pbreakrcycle.tcl @@ -0,0 +1,126 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Pass VII. This pass goes over the set of revision based changesets +## and breaks all dependency cycles they may be in. We need a +## dependency tree. Identical to pass IX, except for the selection of +## the changesets. + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime. +package require snit ; # OO system. +package require struct::list ; # Higher order list operations. +package require vc::tools::log ; # User feedback. +package require vc::fossil::import::cvs::repository ; # Repository management. +package require vc::fossil::import::cvs::cyclebreaker ; # Breaking dependency cycles. +package require vc::fossil::import::cvs::state ; # State storage. +package require vc::fossil::import::cvs::integrity ; # State integrity checks. +package require vc::fossil::import::cvs::project::rev ; # Project level changesets + +# # ## ### ##### ######## ############# ##################### +## Register the pass with the management + +vc::fossil::import::cvs::pass define \ + BreakRevCsetCycles \ + {Break Revision ChangeSet Dependency Cycles} \ + ::vc::fossil::import::cvs::pass::breakrcycle + +# # ## ### ##### ######## ############# ##################### +## + +snit::type ::vc::fossil::import::cvs::pass::breakrcycle { + # # ## ### ##### ######## ############# + ## Public API + + typemethod setup {} { + # Define the names and structure of the persistent state of + # this pass. + + state use revision + state use symbol + state use changeset + state use csitem + state use cstype + state use cssuccessor + + return + } + + typemethod load {} { + # Pass manager interface. Executed to load data computed by + # this pass into memory when this pass is skipped instead of + # executed. + return + } + + typemethod run {} { + # Pass manager interface. Executed to perform the + # functionality of the pass. + + cyclebreaker breakcmd {::vc::fossil::import::cvs::cyclebreaker break} + + state transaction { + cyclebreaker run break-rev [myproc Changesets] + } + + repository printcsetstatistics + integrity changesets + return + } + + typemethod discard {} { + # Pass manager interface. Executed for all passes after the + # run passes, to remove all data of this pass from the state, + # as being out of date. + return + } + + # # ## ### ##### ######## ############# + ## Internal methods + + proc Changesets {} { + log write 2 breakrcycle {Selecting the revision changesets} + return [project::rev rev] + } + + # # ## ### ##### ######## ############# + ## Configuration + + pragma -hasinstances no ; # singleton + pragma -hastypeinfo no ; # no introspection + pragma -hastypedestroy no ; # immortal + + # # ## ### ##### ######## ############# +} + +namespace eval ::vc::fossil::import::cvs::pass { + namespace export breakrcycle + namespace eval breakrcycle { + namespace import ::vc::fossil::import::cvs::cyclebreaker + namespace import ::vc::fossil::import::cvs::repository + namespace import ::vc::fossil::import::cvs::state + namespace import ::vc::fossil::import::cvs::integrity + namespace eval project { + namespace import ::vc::fossil::import::cvs::project::rev + } + namespace import ::vc::tools::log + log register breakrcycle + } +} + +# # ## ### ##### ######## ############# ##################### +## Ready + +package provide vc::fossil::import::cvs::pass::breakrcycle 1.0 +return diff --git a/tools/cvs2fossil/lib/c2f_pbreakscycle.tcl b/tools/cvs2fossil/lib/c2f_pbreakscycle.tcl new file mode 100644 index 0000000..fb5ae5d --- /dev/null +++ b/tools/cvs2fossil/lib/c2f_pbreakscycle.tcl @@ -0,0 +1,126 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Pass IX. This pass goes over the set of symbol based changesets and +## breaks all dependency cycles they may be in. We need a dependency +## tree. Identical to pass VII, except for the selection of the +## changesets. + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime. +package require snit ; # OO system. +package require struct::list ; # Higher order list operations. +package require vc::tools::log ; # User feedback. +package require vc::fossil::import::cvs::cyclebreaker ; # Breaking dependency cycles. +package require vc::fossil::import::cvs::repository ; # Repository management. +package require vc::fossil::import::cvs::state ; # State storage. +package require vc::fossil::import::cvs::integrity ; # State integrity checks. +package require vc::fossil::import::cvs::project::rev ; # Project level changesets + +# # ## ### ##### ######## ############# ##################### +## Register the pass with the management + +vc::fossil::import::cvs::pass define \ + BreakSymCsetCycles \ + {Break Symbol ChangeSet Dependency Cycles} \ + ::vc::fossil::import::cvs::pass::breakscycle + +# # ## ### ##### ######## ############# ##################### +## + +snit::type ::vc::fossil::import::cvs::pass::breakscycle { + # # ## ### ##### ######## ############# + ## Public API + + typemethod setup {} { + # Define the names and structure of the persistent state of + # this pass. + + state use revision + state use tag + state use branch + state use symbol + state use changeset + state use csitem + state use cssuccessor + return + } + + typemethod load {} { + # Pass manager interface. Executed to load data computed by + # this pass into memory when this pass is skipped instead of + # executed. + return + } + + typemethod run {} { + # Pass manager interface. Executed to perform the + # functionality of the pass. + + cyclebreaker breakcmd {::vc::fossil::import::cvs::cyclebreaker break} + + state transaction { + cyclebreaker run break-sym [myproc Changesets] + } + + repository printcsetstatistics + integrity changesets + return + } + + typemethod discard {} { + # Pass manager interface. Executed for all passes after the + # run passes, to remove all data of this pass from the state, + # as being out of date. + return + } + + # # ## ### ##### ######## ############# + ## Internal methods + + proc Changesets {} { + log write 2 breakscycle {Selecting the symbol changesets} + return [project::rev sym] + } + + # # ## ### ##### ######## ############# + ## Configuration + + pragma -hasinstances no ; # singleton + pragma -hastypeinfo no ; # no introspection + pragma -hastypedestroy no ; # immortal + + # # ## ### ##### ######## ############# +} + +namespace eval ::vc::fossil::import::cvs::pass { + namespace export breakscycle + namespace eval breakscycle { + namespace import ::vc::fossil::import::cvs::cyclebreaker + namespace import ::vc::fossil::import::cvs::repository + namespace import ::vc::fossil::import::cvs::state + namespace import ::vc::fossil::import::cvs::integrity + namespace eval project { + namespace import ::vc::fossil::import::cvs::project::rev + } + namespace import ::vc::tools::log + log register breakscycle + } +} + +# # ## ### ##### ######## ############# ##################### +## Ready + +package provide vc::fossil::import::cvs::pass::breakscycle 1.0 +return diff --git a/tools/cvs2fossil/lib/c2f_pcollar.tcl b/tools/cvs2fossil/lib/c2f_pcollar.tcl new file mode 100644 index 0000000..4107444 --- /dev/null +++ b/tools/cvs2fossil/lib/c2f_pcollar.tcl @@ -0,0 +1,337 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007-2008 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Pass I. This pass scans the repository to import for RCS archives, +## and sorts and filters them into the declared projects, if any +## Without declared projects the whole repository is treated as a +## single project. + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime. +package require snit ; # OO system. +package require fileutil::traverse ; # Directory traversal. +package require fileutil ; # File & path utilities. +package require vc::tools::trouble ; # Error reporting. +package require vc::tools::log ; # User feedback. +package require vc::tools::misc ; # Local file utilities. +package require vc::fossil::import::cvs::pass ; # Pass management. +package require vc::fossil::import::cvs::repository ; # Repository management. +package require vc::fossil::import::cvs::state ; # State storage + +# # ## ### ##### ######## ############# ##################### +## Register the pass with the management + +vc::fossil::import::cvs::pass define \ + CollectAr \ + {Collect archives in repository} \ + ::vc::fossil::import::cvs::pass::collar + +# # ## ### ##### ######## ############# ##################### +## + +snit::type ::vc::fossil::import::cvs::pass::collar { + # # ## ### ##### ######## ############# + ## Public API + + typemethod setup {} { + # Define names and structure of the persistent state of this + # pass. + + # We deal with repository projects, and the rcs archive files + # in the projects. + + # For the first, projects, we keep their names, which are + # their paths relative to the base directory of the whole + # repository. These have to be globally unique, i.e. no two + # projects can have the same name. + + # For the files we keep their names, which are their paths + # relative to the base directory of the whole project! These + # have to be unique within a project, however globally this + # does not hold, a name may occur several times, in different + # projects. We further store the user visible file name + # associated with the rcs archive. + + # Both projects and files are identified by globally unique + # integer ids, automatically assigned by the database. + + state extend project { + pid INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE + } + state extend file { + fid INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + pid INTEGER NOT NULL REFERENCES project, -- project the file belongs to + name TEXT NOT NULL, + visible TEXT NOT NULL, + exec INTEGER NOT NULL, -- boolean, 'file executable'. + UNIQUE (pid, name) -- file names are unique within a project + } + return + } + + typemethod load {} { + # Pass manager interface. Executed for all passes before the + # run passes, to load all data of their pass from the state, + # as if it had been computed by the pass itself. + + state use project + state use file + + repository load + return + } + + typemethod run {} { + # Pass manager interface. Executed to perform the + # functionality of the pass. + + set rbase [repository base?] + foreach project [repository projects] { + set base [::file join $rbase [$project base]] + log write 1 collar "Scan $base" + + set traverse [fileutil::traverse %AUTO% $base \ + -prefilter [myproc FilterAtticSubdir $base]] + set n 0 + set r {} + + $traverse foreach path { + set rcs [fileutil::stripPath $base $path] + if {[IsCVSAdmin $rcs]} continue + if {![IsRCSArchive $path]} continue + + set usr [UserPath $rcs isattic] + + if {[CheckForAndReportPathConflicts $base $rcs $usr $isattic]} continue + if {[HandleDotFile $base $rcs usr $isattic]} continue + + log write 4 collar "Found $rcs" + $project addfile $rcs $usr [file executable $rcs] + + incr n + if {[log verbosity?] < 4} { + log progress 0 collar $n {} + } + } + + $traverse destroy + } + + repository printstatistics + repository persist + + log write 1 collar "Scan completed" + return + } + + typemethod discard {} { + # Pass manager interface. Executed for all passes after the + # run passes, to remove all data of this pass from the state, + # as being out of date. + + state discard project + state discard file + return + } + + typemethod ignore_conflicting_attics {} { + set myignore 1 + return + } + + typemethod accept_and_convert_dotfiles {} { + set myconvertdot 1 + return + } + + # # ## ### ##### ######## ############# + ## Internal methods + + typevariable myignore 0 ; # Flag. When set Attic files + # superceded by regular files + # ignored. + typevariable myconvertdot 0 ; # Flag. When set dotfiles do not + # cause rejection, but their names + # are converted to a dotless form + # ('dot-' prefix instead of '.'). + + proc FilterAtticSubdir {base path} { + # This command is used by the traverser to prevent it from + # scanning into subdirectories of an Attic. We get away with + # checking the immediate parent directory of the current path + # as our rejection means that deeper path do not occur. + + if {[file tail [file dirname $path]] eq "Attic"} { + set ad [fileutil::stripPath $base $path] + log write 1 collar "Directory $ad found in Attic, ignoring." + return 0 + } + return 1 + } + + proc IsRCSArchive {path} { + if {![string match *,v $path]} {return 0} + if {[fileutil::test $path fr msg]} {return 1} + trouble warn $msg + return 0 + } + + proc IsCVSAdmin {rcs} { + if { + [string match {CVSROOT/*} $rcs] || + [string match {.cvsignore*} [file tail $rcs]] + } { + log write 4 collar "Ignored $rcs, administrative archive" + return 1 + } + return 0 + } + + proc UserPath {rcs iav} { + upvar 1 $iav isattic + + # Derive the user-visible path from the rcs path. Meaning: + # Chop off the ",v" suffix, and remove a possible "Attic". + + set f [string range $rcs 0 end-2] + + if {"Attic" eq [lindex [file split $rcs] end-1]} { + + # The construction below ensures that Attic/X maps to X + # instead of ./X. Otherwise, Y/Attic/X maps to Y/X. + + set fx [file dirname [file dirname $f]] + set f [file tail $f] + if {$fx ne "."} { set f [file join $fx $f] } + + set isattic 1 + } else { + set isattic 0 + } + + return $f + } + + proc IsSuperceded {base rcs usr isattic} { + ::variable myignore + + if {!$isattic} {return 0} + if {![fileexists_cs $base/$usr,v]} {return 0} + + # We have a regular archive and an Attic archive refering to + # the same user visible file. Ignore the file in the Attic. + # + # By default this is a problem causing an abort after the pass + # has completed. The user can however force us to ignore it. + # In that case the warning is still printed, but will not + # induce an abort any longer. + + if {$myignore} { + log write 2 collar "Ignored $rcs, superceded archive" + } else { + trouble warn "Ignored $rcs, superceded archive" + } + return 1 + } + + # In the future we should move the activity below into the fossil + # backend, as the exact set of paths requiring translation, and + # how to translate them, depends entirely on the limitations + # imposed by the destination repository. + + proc HandleDotFile {base rcs usrvar isattic} { + ::variable myconvertdot + upvar 1 $usrvar usr + + set dedot [DeDot $usr] + if {$dedot eq $usr} { return 0 } + + # Ok, we now have established that the path has to be + # translated. Which as already happened as part of the check + # above. Left is to report the action, and to check if the new + # path collides with existing files and directories. + + if {!$myconvertdot} { + trouble warn "Ignored $rcs, is a dot-file" + return 1 + } + + log write 2 collar "Convert $rcs, is a dot-file" + set usr $dedot + + return [CheckForAndReportPathConflicts $base $rcs $usr $isattic] + } + + proc DeDot {path} { + set res {} + foreach segment [file split $path] { + lappend res [expr { + [string match {.*} $segment] + ? "dot-[string range $segment 1 end]" + : $segment + }] + } + return [eval [linsert $res 0 file join]] + #8.5: return [file join {*}$res] + } + + proc CheckForAndReportPathConflicts {base rcs usr isattic {intro {}}} { + if {[IsSuperceded $base $rcs $usr $isattic]} { return 1 } + + # XXX Checkme: not sure if this will still fail in the case + # where a directory does conflict with a file XXX + if { + [fileexists_cs $base/$usr] && + [fileisdir_cs $base/$usr] + } { + if {$intro ne {}} { + trouble fatal $intro + } + trouble fatal "Directory name conflicts with filename." + trouble fatal "Please remove or rename one of the following:" + trouble fatal " $base/$usr" + trouble fatal " $base/$rcs" + return 1 + } + return 0 + } + + # # ## ### ##### ######## ############# + ## Configuration + + pragma -hasinstances no ; # singleton + pragma -hastypeinfo no ; # no introspection + pragma -hastypedestroy no ; # immortal + + # # ## ### ##### ######## ############# +} + +namespace eval ::vc::fossil::import::cvs::pass { + namespace export collar + namespace eval collar { + namespace import ::vc::fossil::import::cvs::repository + namespace import ::vc::fossil::import::cvs::state + namespace import ::vc::tools::trouble + namespace import ::vc::tools::log + namespace import ::vc::tools::misc::file* + log register collar + } +} + +# # ## ### ##### ######## ############# ##################### +## Ready + +package provide vc::fossil::import::cvs::pass::collar 1.0 +return diff --git a/tools/cvs2fossil/lib/c2f_pcollrev.tcl b/tools/cvs2fossil/lib/c2f_pcollrev.tcl new file mode 100644 index 0000000..a3f6734 --- /dev/null +++ b/tools/cvs2fossil/lib/c2f_pcollrev.tcl @@ -0,0 +1,680 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007-2008 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Pass II. This pass parses the collected rcs archives and extracts +## all the information they contain (revisions, and symbols). + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime. +package require snit ; # OO system. +package require vc::tools::trouble ; # Error reporting. +package require vc::tools::log ; # User feedback. +package require vc::fossil::import::cvs::pass ; # Pass management. +package require vc::fossil::import::cvs::repository ; # Repository management. +package require vc::fossil::import::cvs::state ; # State storage. +package require vc::fossil::import::cvs::integrity ; # State integrity checks. +package require vc::fossil::import::cvs::project::sym ; # Project level symbols. +package require vc::fossil::import::cvs::file::rev ; # File level revisions. +package require vc::rcs::parser ; # Rcs archive data extraction. + +# # ## ### ##### ######## ############# ##################### +## Register the pass with the management + +vc::fossil::import::cvs::pass define \ + CollectRev \ + {Collect revisions and symbols} \ + ::vc::fossil::import::cvs::pass::collrev + +# # ## ### ##### ######## ############# ##################### +## + +snit::type ::vc::fossil::import::cvs::pass::collrev { + # # ## ### ##### ######## ############# + ## Public API + + typemethod setup {} { + # Define names and structure of the persistent state of this + # pass. + + state use project + state use file + + # We deal with per project and per file data, the first + # collated from the second. + + # Per file we have general information, ..., and then + # revisions and symbols. The latter can be further separated + # into tags and branches. At project level the per-file + # symbols information is merged. + + # File level ... + # Revisions, Branches, Tags + # + # Pseudo class hierarchy + # Tag <- Symbol <- Event + # Branch <- Symbol <- Event + # Revision <- Event + + state extend revision { + -- Revisions. Identified by a global numeric id each + -- belongs to a single file, identified by its id. It + -- further has a dotted revision number (DTN). + -- + -- Constraint: The dotted revision number is unique within + -- the file. See end of definition. + + rid INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + fid INTEGER NOT NULL REFERENCES file, -- File owning revision. + rev TEXT NOT NULL, -- Dotted Rev Number. + + -- All revisions belong to a line-of-development, + -- identified by a symbol (project level). During data + -- collection it was a file-level branch symbol. + -- + -- Constraint: All the LOD symbols are in the same project + -- as the file itself. This cannot be + -- expressed in CREATE TABLE syntax. + + lod INTEGER NOT NULL REFERENCES symbol, -- Line of development + + -- The revisions in a file are organized in a forest of + -- trees, with the main lines defined through the parent / + -- child references. A revision without a parent is the + -- root of a tree, and a revision without a child is a + -- leaf. + + -- Constraints: All revisions coupled through parent/child + -- refer to the same LOD symbol. The parent + -- of a child of X is X. The child of a + -- parent of X is X. + + parent INTEGER REFERENCES revision, + child INTEGER REFERENCES revision, + + -- The representation of a branch in a tree is the + -- exception to the three constraints above. + + -- The beginning of a branch is represented by a non-NULL + -- bparent of a revision. This revision B is the first on + -- the branch. Its parent P is the revision the branch is + -- rooted in, and it is not the child of P. B and P refer + -- to different LOD symbols. The bparent of B is also its + -- LOD, and the LOD of its children. + + bparent INTEGER REFERENCES symbol, + + -- Lastly we keep information is about non-trunk default + -- branches (NTDB) in the revisions. + + -- All revisions on the NTDB have 'isdefault' TRUE, + -- everyone else FALSE. The last revision X on the NTDB + -- which is still considered to be on the trunk as well + -- has a non-NULL 'dbchild' which refers to the root of + -- the trunk. The root also has a non-NULL dbparent + -- refering to X. + + isdefault INTEGER NOT NULL, + dbparent INTEGER REFERENCES revision, + dbchild INTEGER REFERENCES revision, + + -- The main payload of the revision are the date/time it + -- was entered, its state, operation (= type/class), text + -- content, and meta data (author, log message, branch, + -- project). The last is encoded as single id, see table + -- 'meta'. The date/time is given in seconds since the + -- epoch, for easy comparison. + + op INTEGER NOT NULL REFERENCES optype, + date INTEGER NOT NULL, + state TEXT NOT NULL, + mid INTEGER NOT NULL REFERENCES meta, + + UNIQUE (fid, rev) -- The DTN is unique within the revision's file. + } + + # Blobs contain the information needed to extract revisions + # from rcs archive files. As such each revision has an + # associated blob. However we can have blobs without + # revisions. This happens if a logically irrelevant revision + # is removed. We may however still need its blob to correctly + # expand other revisions, both its contents and for the + # ordering. + + state extend blob { + bid INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + rid INTEGER REFERENCES revision, + fid INTEGER NOT NULL REFERENCES file, -- File owning blob. + + -- The text content is an (offset,length) pair into the + -- rcs archive. For deltas we additionally refer to the + -- parent blob the delta is made against. + + coff INTEGER NOT NULL, + clen INTEGER NOT NULL, + pid INTEGER REFERENCES blob, + + UNIQUE (rid) + } { fid } + # Index on owning file to collect all blobs of a file when the + # time for its expansion comes. + + state extend optype { + oid INTEGER NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + UNIQUE(name) + } + state run { + INSERT INTO optype VALUES (-1,'delete'); -- The opcode names are the + INSERT INTO optype VALUES ( 0,'nothing'); -- fixed pieces, see myopstate + INSERT INTO optype VALUES ( 1,'add'); -- in file::rev. myopcode is + INSERT INTO optype VALUES ( 2,'change'); -- loaded from this. + } + + state extend revisionbranchchildren { + -- The non-primary children of a revision, as reachable + -- through a branch symbol, are listed here. This is + -- needed by pass 5 to break internal dependencies in a + -- changeset. + + rid INTEGER NOT NULL REFERENCES revision, + brid INTEGER NOT NULL REFERENCES revision, + UNIQUE(rid,brid) + } + + state extend tag { + tid INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + fid INTEGER NOT NULL REFERENCES file, -- File the item belongs to + lod INTEGER REFERENCES symbol, -- Line of development (NULL => Trunk) + sid INTEGER NOT NULL REFERENCES symbol, -- Symbol capturing the tag + + rev INTEGER NOT NULL REFERENCES revision -- The revision being tagged. + } { rev sid } + # Indices on: rev (revision successors) + # sid (tag predecessors, branch successors/predecessors) + + state extend branch { + bid INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + fid INTEGER NOT NULL REFERENCES file, -- File the item belongs to + lod INTEGER REFERENCES symbol, -- Line of development (NULL => Trunk) + sid INTEGER NOT NULL REFERENCES symbol, -- Symbol capturing the branch + + root INTEGER REFERENCES revision, -- Revision the branch sprouts from + first INTEGER REFERENCES revision, -- First revision committed to the branch + bra TEXT NOT NULL, -- branch number + pos INTEGER NOT NULL -- creation order in root. + + -- A branch can exist without root. It happens when the + -- only revision on trunk is the unnecessary dead one the + -- branch was sprouted from and it has commits. The branch + -- will exist to be the LOD of its revisions, nothing to + -- sprout from, the dead revision was removed, hence no + -- root. + } { root first sid } + # Indices on: root (revision successors) + # first (revision predecessors) + # sid (tag predecessors, branch successors/predecessors) + + # Project level ... + # pLineOfDevelopment, pSymbol, pBranch, pTag, pTrunk + # + # pTrunk <- pLineOfDevelopment + # pBranch <- pSymbol, pLineOfDevelopment + # pTag <- pSymbol, pLineOfDevelopment + + state extend symbol { + sid INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + pid INTEGER NOT NULL REFERENCES project, -- Project the symbol belongs to + name TEXT NOT NULL, + type INTEGER NOT NULL REFERENCES symtype, -- enum { excluded = 0, tag, branch, undefined } + + tag_count INTEGER NOT NULL, -- How often the symbol is used as tag. + branch_count INTEGER NOT NULL, -- How often the symbol is used as branch + commit_count INTEGER NOT NULL, -- How often a file was committed on the symbol + + UNIQUE (pid, name) -- Symbols are unique within the project + } + + state extend blocker { + -- For each symbol we save which other symbols are + -- blocking its removal (if the user asks for it). + + sid INTEGER NOT NULL REFERENCES symbol, -- + bid INTEGER NOT NULL REFERENCES symbol, -- Sprouted from sid, blocks it. + UNIQUE (sid, bid) + } + + state extend parent { + -- For each symbol we save which other symbols can act as + -- a possible parent in some file, and how often. + + sid INTEGER NOT NULL REFERENCES symbol, -- + pid INTEGER NOT NULL REFERENCES symbol, -- Possible parent of sid + n INTEGER NOT NULL, -- How often pid can act as parent. + UNIQUE (sid, pid) + } + + state extend symtype { + tid INTEGER NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + plural TEXT NOT NULL, + UNIQUE (name) + UNIQUE (plural) + } + state run { + INSERT INTO symtype VALUES (0,'excluded', 'excluded'); -- The ids are the fixed + INSERT INTO symtype VALUES (1,'tag', 'tags'); -- pieces, see project::sym, + INSERT INTO symtype VALUES (2,'branch', 'branches'); -- getsymtypes and associated + INSERT INTO symtype VALUES (3,'undefined','undefined'); -- typevariables. + } + + state extend meta { + -- Meta data of revisions. See revision.mid for the + -- reference. Many revisions can share meta data. This is + -- actually one of the criterions used to sort revisions + -- into changesets. + + mid INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + + -- Meta data belongs to a specific project, stronger, to a + -- branch in that project. It further has a log message, + -- and its author. This is unique with the project and + -- branch. + + pid INTEGER NOT NULL REFERENCES project, -- + bid INTEGER NOT NULL REFERENCES symbol, -- + aid INTEGER NOT NULL REFERENCES author, -- + cid INTEGER NOT NULL REFERENCES cmessage, -- + + UNIQUE (pid, bid, aid, cid) + + -- Constraints: The project of the meta data of a revision + -- X is the same as the project of X itself. + -- + -- ............ The branch of the meta data of a revision + -- X is the same as the line of development + -- of X itself. + } + + # Authors and commit messages are fully global, i.e. per + # repository. + + state extend author { + aid INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, -- Pool of the unique + name TEXT NOT NULL UNIQUE -- author names. + } + + state extend cmessage { + cid INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, -- Pool of the unique + text TEXT NOT NULL UNIQUE -- log messages + } + + project::sym getsymtypes + file::rev getopcodes + return + } + + typemethod load {} { + state use symbol + state use symtype + state use optype + + project::sym getsymtypes + file::rev getopcodes + repository loadsymbols + return + } + + typemethod run {} { + # Pass manager interface. Executed to perform the + # functionality of the pass. + + set rbase [repository base?] + foreach project [repository projects] { + set base [::file join $rbase [$project base]] + log write 1 collrev "Processing $base" + + foreach file [$project files] { + set path [$file path] + log write 2 collrev "Parsing $path" + if {[catch { + parser process [::file join $base $path] $file + } msg]} { + global errorCode + if {$errorCode eq "vc::rcs::parser"} { + trouble fatal "$path is not a valid RCS archive ($msg)" + } else { + global errorInfo + trouble internal $errorInfo + } + } else { + # We persist the core of the data collected about + # each file immediately after it has been parsed + # and wrangled into shape, and then drop it from + # memory. This is done to keep the amount of + # required memory within sensible limits. Without + # doing it this way we would easily gobble up 1G + # of RAM or more with all the objects (revisions + # and file-level symbols). + + $file persist + } + + $file drop + } + + $project purgeghostsymbols + } + + repository persistrev + repository printrevstatistics + integrity strict + + log write 1 collrev "Scan completed" + return + } + + typemethod discard {} { + # Pass manager interface. Executed for all passes after the + # run passes, to remove all data of this pass from the state, + # as being out of date. + + state discard revision + state discard tag + state discard branch + state discard symbol + state discard blocker + state discard parent + state discard symtype + state discard meta + state discard author + state discard cmessage + return + } + + # TODO: Move this code to the integrity module + proc Paranoia {} { + # This code performs a number of paranoid checks of the + # database, searching for inconsistent cross-references. + log write 4 collrev {Check database consistency} + + set n 0 ; # Counter for the checks (we print an id before the + # main label). + + # Find all revisions which disagree with their line of + # development about the project they are owned by. + Check \ + {Revisions and their LODs have to be in the same project} \ + {disagrees with its LOD about owning project} { + SELECT F.name, R.rev + FROM revision R, file F, symbol S + WHERE R.fid = F.fid -- Get file of revision + AND R.lod = S.sid -- Get symbol for revision's LOD + AND F.pid != S.pid -- but symbol is for a different project + ; + } + # Find all revisions which disgree with their meta data about + # the project they are owned by. + Check \ + {Revisions and their meta data have to be in the same project} \ + {disagrees with its meta data about owning project} { + SELECT F.name, R.rev + FROM revision R, file F, meta M + WHERE R.fid = F.fid -- Get file of revision + AND R.mid = M.mid -- Get meta data of revision + AND F.pid != M.pid -- but is for a different project + ; + } + # Find all revisions which disgree with their meta data about + # the branch/line of development they belong to. + Check \ + {Revisions and their meta data have to be in the same LOD} \ + {disagrees with its meta data about owning LOD} { + SELECT F.name, R.rev + FROM revision R, meta M, file F + WHERE R.mid = M.mid -- Get meta data of revision + AND R.lod != M.bid -- but is for a different LOD + AND R.fid = F.fid -- Get file of revision + ; + } + # Find all revisions with a primary child which disagrees + # about the file they belong to. + Check \ + {Revisions and their primary children have to be in the same file} \ + {disagrees with its primary child about the owning file} { + SELECT F.name, R.rev + FROM revision R, revision C, file F + WHERE R.fid = F.fid -- Get file of revision + AND R.child IS NOT NULL -- Restrict to non-leaf revisions + AND R.child = C.rid -- Get child (has to exist) + AND C.fid != R.fid -- Whic wrongly is in a different file + ; + } + + # Find all revisions with a branch parent symbol whose parent + # disagrees about the file they belong to. + Check \ + {Revisions and their branch children have to be in the same file} \ + {at the beginning of its branch and its parent disagree about the owning file} { + SELECT F.name, R.rev + FROM revision R, revision P, file F + WHERE R.fid = F.fid -- Get file of revision + AND R.bparent IS NOT NULL -- Restrict to first on branch + AND R.parent = P.rid -- Get out-of-branch parent + AND R.fid != P.fid -- Which wrongly is in a different file + ; + } + # Find all revisions with a non-NTDB child which disagrees + # about the file they belong to. + Check \ + {Revisions and their non-NTDB children have to be in the same file} \ + {disagrees with its non-NTDB child about the owning file} { + SELECT F.name, R.rev + FROM revision R, revision C, file F + WHERE R.fid = F.fid -- Get file of revision + AND R.dbchild IS NOT NULL -- Restrict to semi-last NTDB revision + AND R.dbchild = C.rid -- Got to associated trunk revision + AND C.fid != R.fid -- Which wrongly is in a different file + ; + } + # Find all revisions which have a primary child, but the child + # does not have them as parent. + Check \ + {Revisions have to be parents of their primary children} \ + {is not the parent of its primary child} { + SELECT F.name, R.rev + FROM revision R, revision C, file F + WHERE R.fid = F.fid -- Get file of revision + AND R.child IS NOT NULL -- Restrict to non-leaves + AND R.child = C.rid -- Get the child (has to exist) + AND C.parent != R.rid -- Which does not have us as its parent. + ; + } + # Find all revisions which have a primrary child, but the + # child has a branch parent symbol making them brach starters. + Check \ + {Primary children of revisions must not start branches} \ + {is parent of a primary child which is the beginning of a branch} { + SELECT F.name, R.rev + FROM revision R, revision C, file F + WHERE R.fid = F.fid -- Get file of revision + AND R.child IS NOT NULL -- Restrict to non-leaves + AND R.child = C.rid -- Get the child (has to exist) + AND C.bparent IS NOT NULL -- wrongly claiming to be first on branch + ; + } + # Find all revisions without branch parent symbol which have a + # parent, but the parent does not have them as primary child. + Check \ + {Revisions have to be primary children of their parents, if any} \ + {is not the child of its parent} { + SELECT F.name, R.rev + FROM revision R, revision P, file F + WHERE R.fid = F.fid + AND R.bparent IS NULL -- Get file of revision + AND R.parent IS NOT NULL -- Restrict to everything not first on a branch + AND R.parent = P.rid -- Get the parent (has to exist) + AND P.child != R.rid -- Which do not have us as their child + ; + } + # Find all revisions with a branch parent symbol which do not + # have a parent. + Check \ + {Branch starting revisions have to have a parent} \ + {at the beginning of its branch has no parent} { + SELECT F.name, R.rev + FROM revision R, file F + WHERE R.fid = F.fid -- Get file of revision + AND R.bparent IS NOT NULL -- Restrict to first on a branch + AND R.parent IS NULL -- But there is no out-of-branch parent + ; + } + # Find all revisions with a branch parent symbol whose parent + # has them as primary child. + Check \ + {Branch starting revisions must not be primary children of their parents} \ + {at the beginning of its branch is the primary child of its parent} { + SELECT F.name, R.rev + FROM revision R, revision P, file F + WHERE R.fid = F.fid -- Get file of revision + AND R.bparent IS NOT NULL -- Restrict to first on a branch + AND R.parent IS NOT NULL -- Which are not detached + AND R.parent = P.rid -- Get their non-branch parent + AND P.child = R.rid -- which improperly has them as primary child + ; + } + # Find all revisions with a non-NTDB child which are not on + # the NTDB. + Check \ + {NTDB to trunk transition has to begin on NTDB} \ + {has a non-NTDB child, yet is not on the NTDB} { + SELECT F.name, R.rev + FROM revision R, file F + WHERE R.fid = F.fid -- Get file of revision + AND R.dbchild IS NOT NULL -- Restrict to semi-last NTDB revision + AND NOT R.isdefault -- Improperly claiming to not be on NTDB + ; + } + # Find all revisions with a NTDB parent which are on the NTDB. + Check \ + {NTDB to trunk transition has to end on non-NTDB} \ + {has a NTDB parent, yet is on the NTDB} { + SELECT F.name, R.rev + FROM revision R, file F + WHERE R.fid = F.fid -- Get file of revision + AND R.dbparent IS NOT NULL -- Restrict to trunk roots with NTDB around + AND R.isdefault -- But root improperly claims to be on NTDB + ; + } + # Find all revisions with a child which disagrees about the + # line of development they belong to. + Check \ + {Revisions and their primary children have to be in the same LOD} \ + {and its primary child disagree about their LOD} { + SELECT F.name, R.rev + FROM revision R, revision C, file F + WHERE R.fid = F.fid -- Get file of revision + AND R.child IS NOT NULL -- Restrict to non-leaves + AND R.child = C.rid -- Get child (has to exist) + AND C.lod != R.lod -- which improperly uses a different LOD + ; + } + # Find all revisions with a non-NTDB child which agrees about + # the line of development they belong to. + Check \ + {NTDB and trunk revisions have to be in different LODs} \ + {on NTDB and its non-NTDB child wrongly agree about their LOD} { + SELECT F.name, R.rev + FROM revision R, revision C, file F + WHERE R.fid = F.fid -- Get file of revision + AND R.dbchild IS NOT NULL -- Restrict to semi-last NTDB revision + AND R.dbchild = C.rid -- Get associated trunk root revision + AND C.lod = R.lod -- Improperly uses the same LOD + ; + } + # Find all revisions with a branch parent symbol which is not + # their LOD. + Check \ + {Branch starting revisions have to have their LOD as branch parent symbol} \ + {at the beginning of its branch does not have the branch symbol as its LOD} { + SELECT F.name, R.rev + FROM revision R, file F + WHERE R.fid = F.fid -- Get file of revision + AND R.bparent IS NOT NULL -- Restrict to revisions first on a branch + AND R.lod != R.bparent -- and their branch is not their LOD + ; + } + # Find all revisions with a branch parent symbol whose parent + # is in the same line of development. + Check \ + {Revisions and their branch children have to be in different LODs} \ + {at the beginning of its branch and its parent wrongly agree about their LOD} { + SELECT F.name, R.rev + FROM revision R, revision P, file F + WHERE R.fid = F.fid -- Get file of revision + AND R.bparent IS NOT NULL -- Restrict to revisions first on a branch + AND R.parent = P.rid -- Get their non-branch parent + AND R.lod = P.lod -- Which improperly uses the same LOD + ; + } + return + } + + proc Check {header label sql} { + upvar 1 n n + set ok 1 + foreach {fname revnr} [state run $sql] { + set ok 0 + trouble fatal "$fname <$revnr> $label" + } + log write 5 collrev "\[[format %02d [incr n]]\] [expr {$ok ? "Ok " : "Failed"}] ... $header" + return + } + + # # ## ### ##### ######## ############# + ## Internal methods + + # # ## ### ##### ######## ############# + ## Configuration + + pragma -hasinstances no ; # singleton + pragma -hastypeinfo no ; # no introspection + pragma -hastypedestroy no ; # immortal + + # # ## ### ##### ######## ############# +} + +namespace eval ::vc::fossil::import::cvs::pass { + namespace export collrev + namespace eval collrev { + namespace import ::vc::rcs::parser + namespace import ::vc::fossil::import::cvs::repository + namespace import ::vc::fossil::import::cvs::state + namespace import ::vc::fossil::import::cvs::integrity + namespace eval project { + namespace import ::vc::fossil::import::cvs::project::sym + } + namespace eval file { + namespace import ::vc::fossil::import::cvs::file::rev + } + namespace import ::vc::tools::trouble + namespace import ::vc::tools::log + log register collrev + } +} + +# # ## ### ##### ######## ############# ##################### +## Ready + +package provide vc::fossil::import::cvs::pass::collrev 1.0 +return diff --git a/tools/cvs2fossil/lib/c2f_pcollsym.tcl b/tools/cvs2fossil/lib/c2f_pcollsym.tcl new file mode 100644 index 0000000..295027a --- /dev/null +++ b/tools/cvs2fossil/lib/c2f_pcollsym.tcl @@ -0,0 +1,318 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Pass III. This pass divides the symbols collected by the previous +## pass into branches, tags, and excludes. The latter are also +## partially deleted by this pass, not only marked. It is the next +## pass however, 'FilterSym', which performs the full deletion. + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime. +package require snit ; # OO system. +package require vc::tools::trouble ; # Error reporting. +package require vc::tools::log ; # User feedback. +package require vc::fossil::import::cvs::repository ; # Repository management. +package require vc::fossil::import::cvs::state ; # State storage. +package require vc::fossil::import::cvs::project::sym ; # Project level symbols + +# # ## ### ##### ######## ############# ##################### +## Register the pass with the management + +vc::fossil::import::cvs::pass define \ + CollateSymbols \ + {Collate symbols} \ + ::vc::fossil::import::cvs::pass::collsym + +# # ## ### ##### ######## ############# ##################### +## + +snit::type ::vc::fossil::import::cvs::pass::collsym { + # # ## ### ##### ######## ############# + ## Public API + + typemethod setup {} { + # Define names and structure of the persistent state of this + # pass. + + state use project + state use symbol + state use symtype + state use blocker + state use parent + + state extend preferedparent { + -- For each symbol the prefered parent. This describes the + -- tree of the found lines of development. Actually a + -- forest in case of multiple projects, with one tree per + -- project. + + sid INTEGER NOT NULL PRIMARY KEY REFERENCES symbol, + pid INTEGER NOT NULL REFERENCES symbol + } { pid } + # Index on: pid (branch successors) + return + } + + typemethod load {} { + # Pass manager interface. Executed to load data computed by + # this pass into memory when this pass is skipped instead of + # executed. + + # The results of this pass are fully in the persistent state, + # there is nothing to load for the next one. + return + } + + typemethod run {} { + # Pass manager interface. Executed to perform the + # functionality of the pass. + + state transaction { + repository determinesymboltypes + + project::sym printrulestatistics + project::sym printtypestatistics + } + + if {![trouble ?]} { + UnconvertedSymbols + BadSymbolTypes + BlockedExcludes + InvalidTags + } + + if {![trouble ?]} { + DropExcludedSymbolsFromReferences + DeterminePreferedParents + } + + log write 1 collsym "Collation completed" + return + } + + typemethod discard {} { + # Pass manager interface. Executed for all passes after the + # run passes, to remove all data of this pass from the state, + # as being out of date. + + state discard preferedparent + return + } + + # # ## ### ##### ######## ############# + ## Internal methods + + ## TODO: Move UnconvertedSymbols, BadSymbolTypes, BlockedIncludes, + ## TODO: InvalidTags to the integrity module? + + proc UnconvertedSymbols {} { + # Paranoia - Have we left symbols without conversion + # information (i.e. with type 'undefined') ? + + set undef [project::sym undef] + + state foreachrow { + SELECT P.name AS pname, S.name AS sname + FROM symbol S, project P + WHERE S.type = $undef -- Restrict to undefined symbols + AND P.pid = S.pid -- Get project for symbol + } { + trouble fatal "$pname : The symbol '$sname' was left undefined" + } + return + } + + proc BadSymbolTypes {} { + # Paranoia - Have we left symbols with bogus conversion + # information (type out of the valid range (excluded, branch, + # tag)) ? + + state foreachrow { + SELECT P.name AS pname, S.name AS sname + FROM symbol S, project P + WHERE S.type NOT IN (0,1,2) -- Restrict to symbols with bogus type codes + AND P.pid = S.pid -- Get project of symbol + } { + trouble fatal "$pname : The symbol '$sname' has no proper conversion type" + } + return + } + + proc BlockedExcludes {} { + # Paranoia - Have we scheduled symbols for exclusion without + # also excluding their dependent symbols ? + + set excl [project::sym excluded] + + state foreachrow { + SELECT P.name AS pname, S.name AS sname, SB.name AS bname + FROM symbol S, blocker B, symbol SB, project P + WHERE S.type = $excl -- Restrict to excluded symbols + AND S.sid = B.sid -- Get symbols blocking them + AND B.bid = SB.sid -- and + AND SB.type != $excl -- which are not excluded themselves + AND P.pid = S.pid -- Get project of symbol + } { + trouble fatal "$pname : The symbol '$sname' cannot be excluded as the unexcluded symbol '$bname' depends on it." + } + return + } + + proc InvalidTags {} { + # Paranoia - Have we scheduled symbols for conversion as tags + # which absolutely cannot be converted as tags due to commits + # made on them ? + + # In other words, this checks finds out if the user has asked + # nonsensical conversions of symbols, which should have been + # left to the heuristics, most specifically + # 'project::sym.HasCommits()'. + + set tag [project::sym tag] + + state foreachrow { + SELECT P.name AS pname, S.name AS sname + FROM project P, symbol S + WHERE S.type = $tag -- Restrict to tag symbols + AND S.commit_count > 0 -- which have revisions committed to them + AND P.pid = S.pid -- Get project of symbol + } { + trouble fatal "$pname : The symbol '$sname' cannot be forced to be converted as tag because it has commits." + } + return + } + + proc DropExcludedSymbolsFromReferences {} { + # The excluded symbols cann be used as blockers nor as + # possible parent for other symbols. We now drop the relevant + # entries to prevent them from causing confusion later on. + + set excl [project::sym excluded] + + state run { + DELETE FROM blocker + WHERE bid IN (SELECT sid + FROM symbol + WhERE type = $excl); -- Get excluded symbols + DELETE FROM parent + WHERE pid IN (SELECT sid + FROM symbol + WhERE type = $excl); -- Get excluded symbols + } + return + } + + proc DeterminePreferedParents {} { + array set prefered {} + + set excl [project::sym excluded] + + # Phase I: Pull the possible parents, using sorting to put the + # prefered parent of each symbol last among all + # candidates, allowing us get the prefered one by + # each candidate overwriting all previous + # selections. Note that we ignore excluded symbols, + # we do not care about their prefered parents and do + # not attempt to compute them. + + state foreachrow { + SELECT S.sid AS xs, + P.pid AS xp, + S.name AS sname, + SB.name AS pname, + PR.name AS prname, + P.n AS votes + FROM symbol S, parent P, symbol SB, project PR + WHERE S.type != $excl -- Restrict to wanted symbols + AND S.sid = P.sid -- Get possible parents of symbol + AND P.pid = SB.sid -- and + AND S.pid = PR.pid -- the project of the symbol + ORDER BY P.n ASC, P.pid DESC -- Sorting, see below + -- + -- Higher votes and smaller ids (= earlier branches) last + -- We simply keep the last possible parent for each + -- symbol. This parent will have the max number of votes + -- for its symbol and will be the earliest created branch + -- possible among all with many votes. + } { + log write 9 pcollsym "Voting $votes for Parent($sname) = $pname" + + set prefered($xs) [list $xp $sname $pname $prname] + } + + # Phase II: Write the found preferences back into the table + # this pass defined for it. + + foreach {s x} [array get prefered] { + struct::list assign $x p sname pname prname + state run { + INSERT INTO preferedparent (sid, pid) + VALUES ($s, $p); + } + + log write 3 pcollsym "$prname : '$sname's prefered parent is '$pname'" + } + + # Phase III: Check the result that all symbols except for + # trunks have a prefered parent. We also ignore + # excluded symbols, as we intentionally did not + # compute a prefered parent for them, see phase I. + + state foreachrow { + SELECT PR.name AS pname, S.name AS sname + FROM symbol S LEFT OUTER JOIN preferedparent P + ON S.sid = P.sid, -- From symbol to prefered parent + project PR + WHERE P.pid IS NULL -- restrict to symbols without a preference + AND S.type != $excl -- which are not excluded + AND S.name != ':trunk:' -- and are not a trunk + AND S.pid = PR.pid -- get project of symbol + } { + trouble fatal "$pname : '$sname' has no prefered parent." + } + + # The reverse, having prefered parents for unknown symbols + # cannot occur. + return + } + + # # ## ### ##### ######## ############# + ## Configuration + + pragma -hasinstances no ; # singleton + pragma -hastypeinfo no ; # no introspection + pragma -hastypedestroy no ; # immortal + + # # ## ### ##### ######## ############# +} + +namespace eval ::vc::fossil::import::cvs::pass { + namespace export collsym + namespace eval collsym { + namespace import ::vc::fossil::import::cvs::repository + namespace import ::vc::fossil::import::cvs::state + namespace eval project { + namespace import ::vc::fossil::import::cvs::project::sym + } + namespace import ::vc::tools::trouble + namespace import ::vc::tools::log + log register collsym + } +} + +# # ## ### ##### ######## ############# ##################### +## Ready + +package provide vc::fossil::import::cvs::pass::collsym 1.0 +return diff --git a/tools/cvs2fossil/lib/c2f_pcsetdeps.tcl b/tools/cvs2fossil/lib/c2f_pcsetdeps.tcl new file mode 100644 index 0000000..cef93d6 --- /dev/null +++ b/tools/cvs2fossil/lib/c2f_pcsetdeps.tcl @@ -0,0 +1,135 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Pass VI. This pass computes the dependencies between the changesets +## from the file level dependencies and stores them in the state for +## use by the cycle breaker and topological sorting passes. + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime. +package require snit ; # OO system. +package require vc::tools::misc ; # Text formatting. +package require vc::tools::log ; # User feedback. +package require vc::fossil::import::cvs::state ; # State storage. +package require vc::fossil::import::cvs::project::rev ; # Project level changesets + +# # ## ### ##### ######## ############# ##################### +## Register the pass with the management + +vc::fossil::import::cvs::pass define \ + CsetDeps \ + {Compute and cache ChangeSet Dependencies} \ + ::vc::fossil::import::cvs::pass::csetdeps + +# # ## ### ##### ######## ############# ##################### +## + +snit::type ::vc::fossil::import::cvs::pass::csetdeps { + # # ## ### ##### ######## ############# + ## Public API + + typemethod setup {} { + # Define the names and structure of the persistent state of + # this pass. + + state use project + state use file + state use revision + state use revisionbranchchildren + state use branch + state use tag + state use symbol + state use preferedparent + state use changeset + state use csitem + + # A table listing for each changeset the set of successor + # changesets. The predecessor information is implied. + + state extend cssuccessor { + cid INTEGER NOT NULL REFERENCES changeset, + nid INTEGER NOT NULL REFERENCES changeset, + UNIQUE (cid,nid) + } {cid nid} + # Index on both columns for fast forward and back retrieval. + return + } + + typemethod load {} { + # Pass manager interface. Executed to load data computed by + # this pass into memory when this pass is skipped instead of + # executed. + + state use cssuccessor + return + } + + typemethod run {} { + # Pass manager interface. Executed to perform the + # functionality of the pass. + + set n 0 + set max [llength [project::rev all]] + + foreach cset [project::rev all] { + log progress 2 csetdeps $n $max + # NOTE: Consider to commit only every N calls. + state transaction { + $cset determinesuccessors + } + incr n + } + return + } + + typemethod discard {} { + # Pass manager interface. Executed for all passes after the + # run passes, to remove all data of this pass from the state, + # as being out of date. + + state discard cssuccessor + return + } + + # # ## ### ##### ######## ############# + ## Internal methods + + # # ## ### ##### ######## ############# + ## Configuration + + pragma -hasinstances no ; # singleton + pragma -hastypeinfo no ; # no introspection + pragma -hastypedestroy no ; # immortal + + # # ## ### ##### ######## ############# +} + +namespace eval ::vc::fossil::import::cvs::pass { + namespace export csetdeps + namespace eval csetdeps { + namespace import ::vc::fossil::import::cvs::state + namespace eval project { + namespace import ::vc::fossil::import::cvs::project::rev + } + namespace import ::vc::tools::misc::* + namespace import ::vc::tools::log + log register csetdeps + } +} + +# # ## ### ##### ######## ############# ##################### +## Ready + +package provide vc::fossil::import::cvs::pass::csetdeps 1.0 +return diff --git a/tools/cvs2fossil/lib/c2f_pfiltersym.tcl b/tools/cvs2fossil/lib/c2f_pfiltersym.tcl new file mode 100644 index 0000000..9a11552 --- /dev/null +++ b/tools/cvs2fossil/lib/c2f_pfiltersym.tcl @@ -0,0 +1,598 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007-2008 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Pass IV. Coming after the symbol collation pass this pass now +## removes all revisions and symbols referencing any of the excluded +## symbols from the persistent database. + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime. +package require snit ; # OO system. +package require vc::tools::misc ; # Text formatting. +package require vc::tools::log ; # User feedback. +package require vc::fossil::import::cvs::repository ; # Repository management. +package require vc::fossil::import::cvs::state ; # State storage. +package require vc::fossil::import::cvs::integrity ; # State storage integrity checks. +package require vc::fossil::import::cvs::project::sym ; # Project level symbols + +# # ## ### ##### ######## ############# ##################### +## Register the pass with the management + +vc::fossil::import::cvs::pass define \ + FilterSymbols \ + {Filter symbols, remove all excluded pieces} \ + ::vc::fossil::import::cvs::pass::filtersym + +# # ## ### ##### ######## ############# ##################### +## + +snit::type ::vc::fossil::import::cvs::pass::filtersym { + # # ## ### ##### ######## ############# + ## Public API + + typemethod setup {} { + # Define names and structure of the persistent state of this + # pass. + + state use project + state use file + state use revision + state use revisionbranchchildren + state use branch + state use tag + state use symbol + state use blocker + state use parent + state use author + state use cmessage + state use preferedparent + + # NOTE: So far no pass coming after this makes us of this information. + state extend noop { + id INTEGER NOT NULL PRIMARY KEY, -- tag/branch reference + noop INTEGER NOT NULL + } + return + } + + typemethod load {} { + # Pass manager interface. Executed to load data computed by + # this pass into memory when this pass is skipped instead of + # executed. + + # The results of this pass are fully in the persistent state, + # there is nothing to load for the next one. + return + } + + typemethod run {} { + # Pass manager interface. Executed to perform the + # functionality of the pass. + + # The removal of excluded symbols and everything referencing + # to them is done completely in the database. + + state transaction { + FilterExcludedSymbols + MutateSymbols + AdjustParents + RefineSymbols + + PrintSymbolTree + repository printrevstatistics + + # Strict integrity enforces that all meta entries are in + # the same LOD as the revision using them. At this point + # this may not be true any longer. If a NTDB was excluded + # then all revisions it shared with the trunk were moved + # to the trunk LOD, however their meta entries will still + # refer to the now gone LOD symbol. This is fine however, + # it will not affect our ability to use the meta entries + # to distinguish and group revisions into changesets. It + # should be noted that we cannot simply switch the meta + # entries over to the trunk either, as that may cause the + # modified entries to violate the unique-ness constraint + # set on that table. + integrity metarelaxed + } + + log write 1 filtersym "Filtering completed" + return + } + + typemethod discard {} { + # Pass manager interface. Executed for all passes after the + # run passes, to remove all data of this pass from the state, + # as being out of date. + return + } + + # # ## ### ##### ######## ############# + ## Internal methods + + proc FilterExcludedSymbols {} { + log write 3 filtersym "Remove the excluded symbols and their users" + + # We pull all the excluded symbols together into a table for + # easy reference by the upcoming DELETE and other statements. + # ('x IN table' clauses). + + set excl [project::sym excluded] + + state run { + CREATE TEMPORARY TABLE excludedsymbols AS + SELECT sid + FROM symbol + WHERE type = $excl + } + + # First we have to handle the possibility of an excluded + # NTDB. This is a special special case there we have to + # regraft the revisions which are shared between the NTDB and + # Trunk onto the trunk, preventing their deletion later. We + # have code for that in 'file', however that operated on the + # in-memory revision objects, which we do not have here. We do + # the same now without object, by directly manipulating the + # links in the database. + + array set ntdb {} + array set link {} + + state foreachrow { + SELECT R.rid AS xid, + R.parent AS xparent, + R.dbchild AS transfer + FROM revision R, symbol S + WHERE R.lod = S.sid -- Get symbol of line-of-development of all revisions + AND S.sid IN excludedsymbols -- Restrict to the excluded symbols + AND R.isdefault -- Restrict to NTDB revisions + } { + set ntdb($xid) $xparent + if {$transfer eq ""} continue + set link($xid) $transfer + } + + foreach joint [array names link] { + # The joints are the highest NTDB revisions which are + # shared with their respective trunk. We disconnect from + # their NTDB children, and make them parents of their + # 'dbchild'. The associated 'dbparent' is squashed + # instead. All parents of the joints are moved to the + # trunk as well. + + set tjoint $link($joint) + set tlod [state one { + SELECT lod FROM revision WHERE rid = $tjoint + }] + + # Covnert db/parent/child into regular parent/child links. + state run { + UPDATE revision SET dbparent = NULL, parent = $joint WHERE rid = $tjoint ; + UPDATE revision SET dbchild = NULL, child = $tjoint WHERE rid = $joint ; + } + while {1} { + # Move the NTDB trunk revisions to trunk. + state run { + UPDATE revision SET lod = $tlod, isdefault = 0 WHERE rid = $joint + } + set last $joint + set joint $ntdb($joint) + if {![info exists ntdb($joint)]} break + } + + # Reached the NTDB basis in the trunk. Finalize the + # parent/child linkage and squash the branch parent symbol + # reference. + + state run { + UPDATE revision SET child = $last WHERE rid = $joint ; + UPDATE revision SET bparent = NULL WHERE rid = $last ; + } + } + + # Now that the special case is done we can simply kill all the + # revisions, tags, and branches referencing any of the + # excluded symbols in some way. This is easy as we do not have + # to select them again and again from the base tables any + # longer. + + state run { + CREATE TEMPORARY TABLE excludedrevisions AS + SELECT rid FROM revision WHERE lod IN excludedsymbols; + + DELETE FROM revision WHERE lod IN excludedsymbols; + DELETE FROM tag WHERE lod IN excludedsymbols; + DELETE FROM tag WHERE sid IN excludedsymbols; + DELETE FROM branch WHERE lod IN excludedsymbols; + DELETE FROM branch WHERE sid IN excludedsymbols; + + DELETE FROM revisionbranchchildren WHERE rid IN excludedrevisions; + DELETE FROM revisionbranchchildren WHERE brid IN excludedrevisions; + DELETE FROM blob WHERE rid IN excludedrevisions; + + DROP TABLE excludedrevisions; + DROP TABLE excludedsymbols; + } + return + } + + proc MutateSymbols {} { + # Next, now that we know which symbols are what we look for + # file level tags which are actually converted as branches + # (project level, and vice versa), and move them to the + # correct tables. + + # # ## ### ##### ######## ############# + + log write 3 filtersym "Mutate symbols, preparation" + + set branch [project::sym branch] + set tag [project::sym tag] + + set tagstomutate [state run { + SELECT T.tid, T.fid, T.lod, T.sid, T.rev + FROM tag T, symbol S + WHERE T.sid = S.sid + AND S.type = $branch + }] + + set branchestomutate [state run { + SELECT B.bid, B.fid, B.lod, B.sid, B.root, B.first, B.bra + FROM branch B, symbol S + WHERE B.sid = S.sid + AND S.type = $tag + }] + + set nt [expr {[llength $tagstomutate]/5}] + set nb [expr {[llength $branchestomutate]/7}] + + log write 4 filtersym "Changing [nsp $nt tag] into [nsp $nt branch branches]" + log write 4 filtersym "Changing [nsp $nb branch branches] into [nsp $nb tag]" + + # # ## ### ##### ######## ############# + + log write 3 filtersym "Mutate tags to branches" + + foreach {id fid lod sid rev} $tagstomutate { + state run { + DELETE FROM tag WHERE tid = $id ; + INSERT INTO branch (bid, fid, lod, sid, root, first, bra, pos) + VALUES ($id, $fid, $lod, $sid, $rev, NULL, '', -1); + } + } + + log write 3 filtersym "Ok." + + # # ## ### ##### ######## ############# + + log write 3 filtersym "Mutate branches to tags" + + foreach {id fid lod sid root first bra} $branchestomutate { + state run { + DELETE FROM branch WHERE bid = $id ; + INSERT INTO tag (tid, fid, lod, sid, rev) + VALUES ($id, $fid, $lod, $sid, $root); + } + } + + log write 3 filtersym "Ok." + + # # ## ### ##### ######## ############# + return + } + + # Adjust the parents of symbols to their preferred parents. + + # If a file level ymbol has a preferred parent that is different + # than its current parent, and if the preferred parent is an + # allowed parent of the symbol in this file, then we graft the + # aSymbol onto its preferred parent. + + proc AdjustParents {} { + log write 3 filtersym "Adjust parents, loading data in preparation" + + # We pull important maps once into memory so that we do quick + # hash lookup later when processing the graft candidates. + + # Tag/Branch names ... + array set sn [state run { SELECT T.tid, S.name FROM tag T, symbol S WHERE T.sid = S.sid }] + array set sn [state run { SELECT B.bid, S.name FROM branch B, symbol S WHERE B.sid = S.sid }] + # Symbol names ... + array set sx [state run { SELECT L.sid, L.name FROM symbol L }] + # Files and projects. + array set fpn {} + state foreachrow { + SELECT F.fid AS id, F.name AS fn, P.name AS pn + FROM file F, project P + WHERE F.pid = P.pid + } { set fpn($id) [list $fn $pn] } + + set tagstoadjust [state run { + SELECT T.tid, T.fid, T.lod, P.pid, S.name, R.rev, R.rid + FROM tag T, preferedparent P, symbol S, revision R + WHERE T.sid = P.sid -- For all tags, get left-hand of prefered parent via symbol + AND T.lod != P.pid -- Restrict to tags whose LOD is not their prefered parent + AND P.pid = S.sid -- Get symbol of prefered parent + AND S.name != ':trunk:' -- Exclude trunk parentage + AND T.rev = R.rid -- Get revision the tag is attached to. + }] + + set branchestoadjust [state run { + SELECT B.bid, B.fid, B.lod, B.pos, P.pid, S.name, NULL, NULL + FROM branch B, preferedparent P, symbol S + WHERE B.sid = P.sid -- For all branches, get left-hand of prefered parent via symbol + AND B.lod != P.pid -- Restrict to branches whose LOD is not their prefered parent + AND P.pid = S.sid -- Get symbol of prefered parent + AND S.name != ':trunk:' -- Exclude trunk parentage + AND B.root IS NULL -- Accept free-floating branch + UNION + SELECT B.bid, B.fid, B.lod, B.pos, P.pid, S.name, R.rev, R.rid + FROM branch B, preferedparent P, symbol S, revision R + WHERE B.sid = P.sid -- For all branches, get left-hand of prefered parent via symbol + AND B.lod != P.pid -- Restrict to branches whose LOD is not their prefered parent + AND P.pid = S.sid -- Get symbol of prefered parent + AND S.name != ':trunk:' -- Exclude trunk parentage + AND B.root = R.rid -- Get root revision of the branch + }] + + set tmax [expr {[llength $tagstoadjust] / 7}] + set bmax [expr {[llength $branchestoadjust] / 8}] + + log write 4 filtersym "Reparenting at most [nsp $tmax tag]" + log write 4 filtersym "Reparenting at most [nsp $bmax branch branches]" + + log write 3 filtersym "Adjust tag parents" + + # Find the tags whose current parent (lod) is not the prefered + # parent, the prefered parent is not the trunk, and the + # prefered parent is a possible parent per the tag's revision. + + set fmt %[string length $tmax]s + set mxs [format $fmt $tmax] + + set n 0 + foreach {id fid lod pid preferedname revnr rid} $tagstoadjust { + # BOTTLE-NECK ... + # + # The check if the candidate (pid) is truly viable is + # based on finding the branch as possible parent, and done + # now instead of as part of the already complex join. + # + # ... AND P.pid IN (SELECT B.sid + # FROM branch B + # WHERE B.root = R.rid) + + if {![state one { + SELECT COUNT(*) -- Count <=> Check existence. + FROM branch B + WHERE B.sid = $pid -- Restrict to branch for that symbol + AND B.root = $rid -- attached to that revision + }]} { + incr tmax -1 + set mxs [format $fmt $tmax] + continue + } + + # + # BOTTLE-NECK ... + + # The names for use in the log output are retrieved + # separately, to keep the join selecting the adjustable + # tags small, not burdened with the dereferencing of links + # to name. + + set tagname $sn($id) + set oldname $sx($lod) + struct::list assign $fpn($fid) fname prname + + # Do the grafting. + + log write 4 filtersym {\[[format $fmt $n]/$mxs\] $prname : Grafting tag '$tagname' on $fname/$revnr from '$oldname' onto '$preferedname'} + state run { UPDATE tag SET lod = $pid WHERE tid = $id } + incr n + } + + log write 3 filtersym "Reparented [nsp $n tag]" + + log write 3 filtersym "Adjust branch parents" + + # Find the branches whose current parent (lod) is not the + # prefered parent, the prefered parent is not the trunk, and + # the prefered parent is a possible parent per the branch's + # revision. + + set fmt %[string length $bmax]s + set mxs [format $fmt $bmax] + + set n 0 + foreach {id fid lod pos pid preferedname revnr rid} $branchestoadjust { + + # BOTTLE-NECK ... + # + # The check if the candidate (pid) is truly viable is + # based on the branch positions in the spawning revision, + # and done now instead of as part of the already complex + # join. + # + # ... AND P.pid IN (SELECT BX.sid + # FROM branch BX + # WHERE BX.root = R.rid + # AND BX.pos > B.pos) + + # Note: rid eq "" hear means that this is a free-floating + # branch, whose original root was removed as a unnecessary + # dead revision (See 'file::RemoveIrrelevantDeletions'). + # Such a branch can be regrafted without trouble and there + # is no need to look for the new parent in its + # non-existent root. + + if {($rid ne "") && ![state one { + SELECT COUNT(*) -- Count <=> Check existence. + FROM branch B + WHERE B.sid = $pid -- Look for branch by symbol + AND B.root = $rid -- Spawned by the given revision + AND B.pos > $pos -- And defined before branch to mutate + }]} { + incr bmax -1 + set mxs [format $fmt $bmax] + continue + } + + # + # BOTTLE-NECK ... + + # The names for use in the log output are retrieved + # separately, to keep the join selecting the adjustable + # tags small, not burdened with the dereferencing of links + # to name. + + set braname $sn($id) + set oldname $sx($lod) + struct::list assign $fpn($fid) fname prname + + # Do the grafting. + + log write 4 filtersym {\[[format $fmt $n]/$mxs\] $prname : Grafting branch '$braname' on $fname/$revnr from '$oldname' onto '$preferedname'} + state run { UPDATE branch SET lod = $pid WHERE bid = $id } + incr n + } + + log write 3 filtersym "Reparented [nsp $n branch branches]" + return + } + + proc RefineSymbols {} { + # Tags and branches are marked as normal/noop based on the op + # of their revision. + + log write 3 filtersym "Refine symbols (no-op or not?)" + + # Note: The noop information is not used anywhere. Consider + # disabling this code, and removing the data from the state. + + log write 4 filtersym " Regular tags" + state run { + INSERT INTO noop + SELECT T.tid, 0 + FROM tag T, revision R + WHERE T.rev = R.rid + AND R.op != 0 -- 0 == nothing + } + + log write 4 filtersym " No-op tags" + state run { + INSERT INTO noop + SELECT T.tid, 1 + FROM tag T, revision R + WHERE T.rev = R.rid + AND R.op = 0 -- nothing + } + + log write 4 filtersym " Regular branches" + state run { + INSERT INTO noop + SELECT B.bid, 0 + FROM branch B, revision R + WHERE B.root = R.rid + AND R.op != 0 -- nothing + } + + log write 4 filtersym " No-op branches" + state run { + INSERT INTO noop + SELECT B.bid, 1 + FROM branch B, revision R + WHERE B.root = R.rid + AND R.op = 0 -- nothing + } + return + } + + proc maxlen {v str} { + upvar 1 $v n + set l [string length $str] + if {$l <= $n} return + set n $l + return + } + + proc PrintSymbolTree {} { + if {![log visible? 9]} return + + array set sym {} + set n 0 + set t 0 + set c 0 + set p 0 + + state foreachrow { + SELECT S.name AS xs, + A.name AS stype, + S.commit_count AS cc, + P.name AS xp, + B.name AS ptype + FROM symbol S, preferedparent SP, symbol P, symtype A, symtype B + WHERE SP.sid = S.sid + AND P.sid = SP.pid + AND A.tid = S.type + AND B.tid = P.type + } { + lappend sym($xs) $xp $stype $ptype $cc + maxlen n $xs + maxlen t $stype + maxlen t $ptype + maxlen c $cc + maxlen p $xp + } + + foreach xs [lsort -dict [array names sym]] { + struct::list assign $sym($xs) xp stype ptype cc + log write 9 filtersym {Tree: [lj $t $stype] ([dj $c $cc]) [lj $n $xs] <-- [lj $t $ptype] $xp} + } + return + } + + proc lj {n s} { ::format %-${n}s $s } + proc dj {n s} { ::format %-${n}d $s } + + # # ## ### ##### ######## ############# + ## Configuration + + pragma -hasinstances no ; # singleton + pragma -hastypeinfo no ; # no introspection + pragma -hastypedestroy no ; # immortal + + # # ## ### ##### ######## ############# +} + +namespace eval ::vc::fossil::import::cvs::pass { + namespace export filtersym + namespace eval filtersym { + namespace import ::vc::fossil::import::cvs::repository + namespace import ::vc::fossil::import::cvs::state + namespace import ::vc::fossil::import::cvs::integrity + namespace eval project { + namespace import ::vc::fossil::import::cvs::project::sym + } + namespace import ::vc::tools::misc::nsp + namespace import ::vc::tools::log + log register filtersym + } +} + +# # ## ### ##### ######## ############# ##################### +## Ready + +package provide vc::fossil::import::cvs::pass::filtersym 1.0 +return diff --git a/tools/cvs2fossil/lib/c2f_pimportcsets.tcl b/tools/cvs2fossil/lib/c2f_pimportcsets.tcl new file mode 100644 index 0000000..d152674 --- /dev/null +++ b/tools/cvs2fossil/lib/c2f_pimportcsets.tcl @@ -0,0 +1,133 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2008 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Pass XIII. This is the second of the backend passes. It imports the +## changesets constructed by the previous passes into one or more +## fossil repositories, one per project. + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime. +package require snit ; # OO system. +package require vc::tools::log ; # User feedback. +package require vc::fossil::import::cvs::repository ; # Repository management. +package require vc::fossil::import::cvs::state ; # State storage. +package require vc::fossil::import::cvs::fossil ; # Access to fossil repositories. +package require vc::fossil::import::cvs::ristate ; # Import state (revisions) + +# # ## ### ##### ######## ############# ##################### +## Register the pass with the management + +vc::fossil::import::cvs::pass define \ + ImportCSets \ + {Import the changesets into fossil repositories} \ + ::vc::fossil::import::cvs::pass::importcsets + +# # ## ### ##### ######## ############# ##################### +## + +snit::type ::vc::fossil::import::cvs::pass::importcsets { + # # ## ### ##### ######## ############# + ## Public API + + typemethod setup {} { + # Define the names and structure of the persistent state of + # this pass. + + state use project + state use file + state use revision + state use meta + state use author + state use cmessage + state use symbol + state use space + state use revuuid + return + } + + typemethod load {} { + # Pass manager interface. Executed to load data computed by + # this pass into memory when this pass is skipped instead of + # executed. + return + } + + typemethod run {} { + # Pass manager interface. Executed to perform the + # functionality of the pass. + + foreach project [repository projects] { + log write 1 importcsets {Importing project "[$project base]"} + + set pid [$project id] + set fossil [fossil %AUTO%] + struct::list assign [state run { + SELECT repository, workspace + FROM space + WHERE pid = $pid + }] r w + $fossil load $r $w + + set rstate [ristate %AUTO%] + + state transaction { + # Layer II: Changesets + foreach {cset date} [$project changesetsinorder] { + $cset pushto $fossil $date $rstate + } + } + + $rstate destroy + $fossil destroy + } + return + } + + typemethod discard {} { + # Pass manager interface. Executed for all passes after the + # run passes, to remove all data of this pass from the state, + # as being out of date. + return + } + + # # ## ### ##### ######## ############# + ## Internal methods + + # # ## ### ##### ######## ############# + ## Configuration + + pragma -hasinstances no ; # singleton + pragma -hastypeinfo no ; # no introspection + pragma -hastypedestroy no ; # immortal + + # # ## ### ##### ######## ############# +} + +namespace eval ::vc::fossil::import::cvs::pass { + namespace export importcsets + namespace eval importcsets { + namespace import ::vc::fossil::import::cvs::repository + namespace import ::vc::fossil::import::cvs::state + namespace import ::vc::fossil::import::cvs::fossil + namespace import ::vc::fossil::import::cvs::ristate + namespace import ::vc::tools::log + log register importcsets + } +} + +# # ## ### ##### ######## ############# ##################### +## Ready + +package provide vc::fossil::import::cvs::pass::importcsets 1.0 +return diff --git a/tools/cvs2fossil/lib/c2f_pimportfiles.tcl b/tools/cvs2fossil/lib/c2f_pimportfiles.tcl new file mode 100644 index 0000000..90e8d8a --- /dev/null +++ b/tools/cvs2fossil/lib/c2f_pimportfiles.tcl @@ -0,0 +1,151 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007-2008 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Pass XII. This is the first of the backend passes. It imports the +## revisions of all files into one or more fossil repositories, one +## per project. + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime. +package require snit ; # OO system. +package require vc::tools::log ; # User feedback. +package require vc::fossil::import::cvs::repository ; # Repository management. +package require vc::fossil::import::cvs::state ; # State storage. +package require vc::fossil::import::cvs::fossil ; # Access to fossil repositories. + +# # ## ### ##### ######## ############# ##################### +## Register the pass with the management + +vc::fossil::import::cvs::pass define \ + ImportFiles \ + {Import the file revisions into fossil repositories} \ + ::vc::fossil::import::cvs::pass::importfiles + +# # ## ### ##### ######## ############# ##################### +## + +snit::type ::vc::fossil::import::cvs::pass::importfiles { + # # ## ### ##### ######## ############# + ## Public API + + typemethod setup {} { + # Define the names and structure of the persistent state of + # this pass. + + state use project + state use file + state use revision + state use meta + state use author + state use cmessage + state use symbol + + # Discard on setup. Do not discard on deferal. + state discard revuuid + state extend revuuid { + rid INTEGER NOT NULL REFERENCES revision UNIQUE, + uuid INTEGER NOT NULL -- fossil id of the revision + -- unique within the project + } + + # Remember the locations of the scratch data createdby this + # pass, for use by the next (importing changesets). + state discard space + state extend space { + pid INTEGER NOT NULL REFERENCES project, + repository TEXT NOT NULL, + workspace TEXT NOT NULL + } + return + } + + typemethod load {} { + # Pass manager interface. Executed to load data computed by + # this pass into memory when this pass is skipped instead of + # executed. + return + } + + typemethod run {} { + # Pass manager interface. Executed to perform the + # functionality of the pass. + + foreach project [repository projects] { + log write 1 importfiles {Importing project "[$project base]"} + + set pid [$project id] + set fossil [fossil %AUTO%] + $fossil initialize + + state transaction { + # Layer I: Files and their revisions + foreach file [$project files] { + $file pushto $fossil + } + + # Save the scratch locations, needed by the next pass. + struct::list assign [$fossil space] r w + state run { + DELETE FROM space + WHERE pid = $pid + ; + INSERT INTO space (pid, repository, workspace) + VALUES ($pid, $r, $w); + } + } + + $fossil destroy + } + return + } + + typemethod discard {} { + # Pass manager interface. Executed for all passes after the + # run passes, to remove all data of this pass from the state, + # as being out of date. + + # Not discarding revuuid/space here, allow us to keep the info + # for the next pass even if the revisions are recomputed. + return + } + + # # ## ### ##### ######## ############# + ## Internal methods + + # # ## ### ##### ######## ############# + ## Configuration + + pragma -hasinstances no ; # singleton + pragma -hastypeinfo no ; # no introspection + pragma -hastypedestroy no ; # immortal + + # # ## ### ##### ######## ############# +} + +namespace eval ::vc::fossil::import::cvs::pass { + namespace export importfiles + namespace eval importfiles { + namespace import ::vc::fossil::import::cvs::repository + namespace import ::vc::fossil::import::cvs::state + namespace import ::vc::fossil::import::cvs::fossil + namespace import ::vc::tools::log + log register importfiles + } +} + +# # ## ### ##### ######## ############# ##################### +## Ready + +package provide vc::fossil::import::cvs::pass::importfiles 1.0 +return diff --git a/tools/cvs2fossil/lib/c2f_pimportfinal.tcl b/tools/cvs2fossil/lib/c2f_pimportfinal.tcl new file mode 100644 index 0000000..98511fc --- /dev/null +++ b/tools/cvs2fossil/lib/c2f_pimportfinal.tcl @@ -0,0 +1,122 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2008 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Pass XIV. This is the third and last of the backend passes. It +## finalizes the fossil repositories we have constructed in the +## previous import passes. + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime. +package require snit ; # OO system. +package require vc::tools::log ; # User feedback. +package require vc::fossil::import::cvs::repository ; # Repository management. +package require vc::fossil::import::cvs::state ; # State storage. +package require vc::fossil::import::cvs::fossil ; # Access to fossil repositories. + +# # ## ### ##### ######## ############# ##################### +## Register the pass with the management + +vc::fossil::import::cvs::pass define \ + ImportFinal \ + {Finalize the import into fossil repositories} \ + ::vc::fossil::import::cvs::pass::importfinal + +# # ## ### ##### ######## ############# ##################### +## + +snit::type ::vc::fossil::import::cvs::pass::importfinal { + # # ## ### ##### ######## ############# + ## Public API + + typemethod setup {} { + # Define the names and structure of the persistent state of + # this pass. + + state use space + return + } + + typemethod load {} { + # Pass manager interface. Executed to load data computed by + # this pass into memory when this pass is skipped instead of + # executed. + return + } + + typemethod run {} { + # Pass manager interface. Executed to perform the + # functionality of the pass. + + foreach project [repository projects] { + log write 1 importfinal {Finalize project "[$project base]"} + + set pid [$project id] + set fossil [fossil %AUTO%] + struct::list assign [state run { + SELECT repository, workspace + FROM space + WHERE pid = $pid + }] r w + $fossil load $r $w + + # At last copy the temporary repository file to its final + # destination and release the associated memory. + + set destination [$project base] + if {$destination eq ""} { + set destination [file tail [repository base?]] + } + append destination .fsl + + $fossil finalize $destination ; # implies destroy + } + return + } + + typemethod discard {} { + # Pass manager interface. Executed for all passes after the + # run passes, to remove all data of this pass from the state, + # as being out of date. + return + } + + # # ## ### ##### ######## ############# + ## Internal methods + + # # ## ### ##### ######## ############# + ## Configuration + + pragma -hasinstances no ; # singleton + pragma -hastypeinfo no ; # no introspection + pragma -hastypedestroy no ; # immortal + + # # ## ### ##### ######## ############# +} + +namespace eval ::vc::fossil::import::cvs::pass { + namespace export importfinal + namespace eval importfinal { + namespace import ::vc::fossil::import::cvs::repository + namespace import ::vc::fossil::import::cvs::state + namespace import ::vc::fossil::import::cvs::fossil + namespace import ::vc::tools::log + log register importfinal + } +} + +# # ## ### ##### ######## ############# ##################### +## Ready + +package provide vc::fossil::import::cvs::pass::importfinal 1.0 +return diff --git a/tools/cvs2fossil/lib/c2f_pinitcsets.tcl b/tools/cvs2fossil/lib/c2f_pinitcsets.tcl new file mode 100644 index 0000000..cd4cafb --- /dev/null +++ b/tools/cvs2fossil/lib/c2f_pinitcsets.tcl @@ -0,0 +1,379 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007-2008 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Pass V. This pass creates the initial set of project level +## revisions, aka changesets. Later passes will refine them, puts them +## into proper order, set their dependencies, etc. + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime. +package require snit ; # OO system. +package require vc::tools::misc ; # Text formatting. +package require vc::tools::log ; # User feedback. +package require vc::tools::mem ; # Memory tracking. +package require vc::fossil::import::cvs::repository ; # Repository management. +package require vc::fossil::import::cvs::state ; # State storage. +package require vc::fossil::import::cvs::integrity ; # State integrity checks. +package require vc::fossil::import::cvs::project::rev ; # Project level changesets + +# # ## ### ##### ######## ############# ##################### +## Register the pass with the management + +vc::fossil::import::cvs::pass define \ + InitCsets \ + {Initialize ChangeSets} \ + ::vc::fossil::import::cvs::pass::initcsets + +# # ## ### ##### ######## ############# ##################### +## + +snit::type ::vc::fossil::import::cvs::pass::initcsets { + # # ## ### ##### ######## ############# + ## Public API + + typemethod setup {} { + # Define the names and structure of the persistent state of + # this pass. + + state use project + state use file + state use revision + state use revisionbranchchildren + state use branch + state use tag + state use symbol + state use meta + + # Data per changeset, namely the project it belongs to, how it + # was induced (revision or symbol), plus reference to the + # primary entry causing it (meta entry or symbol). An adjunct + # table translates the type id's into human readable labels. + + state extend changeset { + cid INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + pid INTEGER NOT NULL REFERENCES project, + type INTEGER NOT NULL REFERENCES cstype, + src INTEGER NOT NULL -- REFERENCES meta|symbol (type dependent) + } + state extend cstype { + tid INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + UNIQUE (name) + } + # Note: Keep the labels used here in sync with the names for + # singleton helper classes for 'project::rev'. They are + # the valid type names for changesets and also hardwired + # in some code. + state run { + INSERT INTO cstype VALUES (0,'rev'); + INSERT INTO cstype VALUES (1,'sym::tag'); + INSERT INTO cstype VALUES (2,'sym::branch'); + } + + # Map from changesets to the (file level) revisions, tags, or + # branches they contain. The pos'ition provides an order of + # the items within a changeset. They are unique within the + # changeset. The items are in principle unique, if we were + # looking only at relevant changesets. However as they come + # from disparate sources the same id may have different + # meaning, be in different changesets and so is formally not + # unique. So we can only say that it is unique within the + # changeset. The integrity module has stronger checks. + + state extend csitem { + cid INTEGER NOT NULL REFERENCES changeset, + pos INTEGER NOT NULL, + iid INTEGER NOT NULL, -- REFERENCES revision|tag|branch + UNIQUE (cid, pos), + UNIQUE (cid, iid) + } { iid } + # Index on: iid (successor/predecessor retrieval) + + project::rev getcstypes + return + } + + typemethod load {} { + # Pass manager interface. Executed to load data computed by + # this pass into memory when this pass is skipped instead of + # executed. + + state use changeset + state use csitem + state use cstype + + # Need the types first, the constructor used inside of the + # 'load' below uses them to assert the correctness of type + # names. + project::rev getcstypes + project::rev load ::vc::fossil::import::cvs::repository + project::rev loadcounter + return + } + + typemethod run {} { + # Pass manager interface. Executed to perform the + # functionality of the pass. + + state transaction { + CreateRevisionChangesets ; # Group file revisions into + # preliminary csets and split + # them based on internal + # conflicts. + CreateSymbolChangesets ; # Create csets for tags and + # branches. + } + + repository printcsetstatistics + integrity changesets + + # Load the changesets for use by the next passes. + project::rev load ::vc::fossil::import::cvs::repository + project::rev loadcounter + return + } + + typemethod discard {} { + # Pass manager interface. Executed for all passes after the + # run passes, to remove all data of this pass from the state, + # as being out of date. + + state discard changeset + state discard cstype + state discard csitem + return + } + + # # ## ### ##### ######## ############# + ## Internal methods + + proc CreateRevisionChangesets {} { + log write 3 initcsets {Create changesets based on revisions} + + # To get the initial of changesets we first group all file + # level revisions using the same meta data entry together. As + # the meta data encodes not only author and log message, but + # also line of development and project we can be sure that + # revisions in different project and lines of development are + # not grouped together. In contrast to cvs2svn we do __not__ + # use distance in time between revisions to break them + # apart. We have seen CVS repositories (from SF) where a + # single commit contained revisions several hours apart, + # likely due to trouble on the server hosting the repository. + + # We order the revisions here by time, this will help the + # later passes (avoids joins later to get at the ordering + # info). + + # The changesets made from these groups are immediately + # inspected for internal conflicts and any such are broken by + # splitting the problematic changeset into multiple + # fragments. The results are changesets which have no internal + # dependencies, only external ones. + + set n 0 + set nx 0 + + set lastmeta {} + set lastproject {} + set revisions {} + + # Note: We could have written this loop to create the csets + # early, extending them with all their revisions. This + # however would mean lots of (slow) method invokations + # on the csets. Doing it like this, late creation, means + # less such calls. None, but the creation itself. + + log write 14 initcsets meta_begin + mem::mark + state foreachrow { + SELECT M.mid AS xmid, + R.rid AS xrid, + M.pid AS xpid + FROM revision R, + meta M -- R ==> M, using PK index of M. + WHERE R.mid = M.mid + ORDER BY M.mid, R.date + } { + log write 14 initcsets meta_next + + if {$lastmeta != $xmid} { + if {[llength $revisions]} { + incr n + set p [repository projectof $lastproject] + log write 14 initcsets meta_cset_begin + mem::mark + set cset [project::rev %AUTO% $p rev $lastmeta $revisions] + log write 14 initcsets meta_cset_done + set spawned [$cset breakinternaldependencies nx] + $cset persist + $cset destroy + foreach cset $spawned { $cset persist ; $cset destroy } + mem::mark + set revisions {} + } + set lastmeta $xmid + set lastproject $xpid + } + lappend revisions $xrid + } + + if {[llength $revisions]} { + incr n + set p [repository projectof $lastproject] + log write 14 initcsets meta_cset_begin + mem::mark + set cset [project::rev %AUTO% $p rev $lastmeta $revisions] + log write 14 initcsets meta_cset_done + set spawned [$cset breakinternaldependencies nx] + $cset persist + $cset destroy + foreach cset $spawned { $cset persist ; $cset destroy } + mem::mark + } + + log write 14 initcsets meta_done + mem::mark + + log write 4 initcsets "Created and saved [nsp $n {revision changeset}]" + log write 4 initcsets "Created and saved [nsp $nx {additional revision changeset}]" + + mem::mark + log write 4 initcsets Ok. + return + } + + proc CreateSymbolChangesets {} { + log write 3 initcsets {Create changesets based on symbols} + mem::mark + + # Tags and branches induce changesets as well, containing the + # revisions they are attached to (tags), or spawned from + # (branches). + + set n 0 + + # First process the tags, then the branches. We know that + # their ids do not overlap with each other. + + set lastsymbol {} + set lastproject {} + set tags {} + + state foreachrow { + SELECT S.sid AS xsid, + T.tid AS xtid, + S.pid AS xpid + FROM tag T, + symbol S -- T ==> R/S, using PK indices of R, S. + WHERE T.sid = S.sid + ORDER BY S.sid, T.tid + } { + if {$lastsymbol != $xsid} { + if {[llength $tags]} { + incr n + set p [repository projectof $lastproject] + set cset [project::rev %AUTO% $p sym::tag $lastsymbol $tags] + set tags {} + $cset persist + $cset destroy + } + set lastsymbol $xsid + set lastproject $xpid + } + lappend tags $xtid + } + + if {[llength $tags]} { + incr n + set p [repository projectof $lastproject] + set cset [project::rev %AUTO% $p sym::tag $lastsymbol $tags] + $cset persist + $cset destroy + } + + set lastsymbol {} + set lasproject {} + set branches {} + + state foreachrow { + SELECT S.sid AS xsid, + B.bid AS xbid, + S.pid AS xpid + FROM branch B, + symbol S -- B ==> R/S, using PK indices of R, S. + WHERE B.sid = S.sid + ORDER BY S.sid, B.bid + } { + if {$lastsymbol != $xsid} { + if {[llength $branches]} { + incr n + set p [repository projectof $lastproject] + set cset [project::rev %AUTO% $p sym::branch $lastsymbol $branches] + set branches {} + $cset persist + $cset destroy + } + set lastsymbol $xsid + set lastproject $xpid + } + lappend branches $xbid + } + + if {[llength $branches]} { + incr n + set p [repository projectof $lastproject] + set cset [project::rev %AUTO% $p sym::branch $lastsymbol $branches] + $cset persist + $cset destroy + } + + log write 4 initcsets "Created and saved [nsp $n {symbol changeset}]" + mem::mark + return + } + + # # ## ### ##### ######## ############# + ## Configuration + + pragma -hasinstances no ; # singleton + pragma -hastypeinfo no ; # no introspection + pragma -hastypedestroy no ; # immortal + + # # ## ### ##### ######## ############# +} + +namespace eval ::vc::fossil::import::cvs::pass { + namespace export initcsets + namespace eval initcsets { + namespace import ::vc::fossil::import::cvs::repository + namespace import ::vc::fossil::import::cvs::state + namespace import ::vc::fossil::import::cvs::integrity + namespace eval project { + namespace import ::vc::fossil::import::cvs::project::rev + } + namespace eval mem { + namespace import ::vc::tools::mem::mark + } + namespace import ::vc::tools::misc::* + namespace import ::vc::tools::log + log register initcsets + } +} + +# # ## ### ##### ######## ############# ##################### +## Ready + +package provide vc::fossil::import::cvs::pass::initcsets 1.0 +return diff --git a/tools/cvs2fossil/lib/c2f_prev.tcl b/tools/cvs2fossil/lib/c2f_prev.tcl new file mode 100644 index 0000000..71d0d1d --- /dev/null +++ b/tools/cvs2fossil/lib/c2f_prev.tcl @@ -0,0 +1,1846 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007-2008 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Revisions per project, aka Changesets. These objects are first used +## in pass 5, which creates the initial set covering the repository. + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime. +package require snit ; # OO system. +package require struct::set ; # Set operations. +package require vc::tools::misc ; # Text formatting +package require vc::tools::trouble ; # Error reporting. +package require vc::tools::log ; # User feedback. +package require vc::fossil::import::cvs::state ; # State storage. +package require vc::fossil::import::cvs::integrity ; # State integrity checks. + +# # ## ### ##### ######## ############# ##################### +## + +snit::type ::vc::fossil::import::cvs::project::rev { + # # ## ### ##### ######## ############# + ## Public API + + constructor {project cstype srcid items {theid {}}} { + if {$theid ne ""} { + set myid $theid + } else { + set myid [incr mycounter] + } + + integrity assert { + [info exists mycstype($cstype)] + } {Bad changeset type '$cstype'.} + + set myproject $project + set mytype $cstype + set mytypeobj ::vc::fossil::import::cvs::project::rev::${cstype} + set mysrcid $srcid + set myitems $items + set mypos {} ; # Commit location is not known yet. + + foreach iid $items { lappend mytitems [list $cstype $iid] } + + # Keep track of the generated changesets and of the inverse + # mapping from items to them. + lappend mychangesets $self + lappend mytchangesets($cstype) $self + set myidmap($myid) $self + + MapItems $cstype $items + return + } + + destructor { + # We may be able to get rid of this entirely, at least for + # (de)construction and pass InitCSets. + + UnmapItems $mytype $myitems + unset myidmap($myid) + + set pos [lsearch -exact $mychangesets $self] + set mychangesets [lreplace $mychangesets $pos $pos] + set pos [lsearch -exact $mytchangesets($mytype) $self] + set mytchangesets($mytype) [lreplace $mytchangesets($mytype) $pos $pos] + return + } + + method str {} { + set str "<" + set detail "" + if {[$mytypeobj bysymbol]} { + set detail " '[state one { + SELECT S.name + FROM symbol S + WHERE S.sid = $mysrcid + }]'" + } + append str "$mytype ${myid}${detail}>" + return $str + } + + method lod {} { + return [$mytypeobj cs_lod $mysrcid $myitems] + } + + method id {} { return $myid } + method items {} { return $mytitems } + method data {} { return [list $myproject $mytype $mysrcid] } + + delegate method bysymbol to mytypeobj + delegate method byrevision to mytypeobj + delegate method isbranch to mytypeobj + delegate method istag to mytypeobj + + method setpos {p} { set mypos $p ; return } + method pos {} { return $mypos } + + method determinesuccessors {} { + # Pass 6 operation. Compute project-level dependencies from + # the file-level data and save it back to the state. This may + # be called during the cycle breaker passes as well, to adjust + # the successor information of changesets which are the + # predecessors of dropped changesets. For them we have to + # remove their existing information first before inserting the + # new data. + state run { + DELETE FROM cssuccessor WHERE cid = $myid; + } + set loop 0 + # TODO: Check other uses of cs_sucessors. + # TODO: Consider merging cs_sucessor's SELECT with the INSERT here. + foreach nid [$mytypeobj cs_successors $myitems] { + state run { + INSERT INTO cssuccessor (cid, nid) + VALUES ($myid,$nid) + } + if {$nid == $myid} { set loop 1 } + } + # Report after the complete structure has been saved. + if {$loop} { $self reportloop } + return + } + + # result = list (changeset) + method successors {} { + # Use the data saved by pass 6. + return [struct::list map [state run { + SELECT S.nid + FROM cssuccessor S + WHERE S.cid = $myid + }] [mytypemethod of]] + } + + # item -> list (item) + method nextmap {} { + $mytypeobj successors tmp $myitems + return [array get tmp] + } + + method breakinternaldependencies {cv} { + upvar 1 $cv counter + log write 14 csets {[$self str] BID} + vc::tools::mem::mark + + # This method inspects the changeset, looking for internal + # dependencies. Nothing is done if there are no such. + + # Otherwise the changeset is split into a set of fragments + # which have no internal dependencies, transforming the + # internal dependencies into external ones. The new changesets + # generated from the fragment information are added to the + # list of all changesets (by the caller). + + # The code checks only successor dependencies, as this auto- + # matically covers the predecessor dependencies as well (Any + # successor dependency a -> b is also a predecessor dependency + # b -> a). + + array set breaks {} + + set fragments [BreakDirectDependencies $myitems breaks] + + if {![llength $fragments]} { return {} } + + return [$self CreateFromFragments $fragments counter breaks] + } + + method persist {} { + set tid $mycstype($mytype) + set pid [$myproject id] + set pos 0 + + state transaction { + state run { + INSERT INTO changeset (cid, pid, type, src) + VALUES ($myid, $pid, $tid, $mysrcid); + } + + foreach iid $myitems { + state run { + INSERT INTO csitem (cid, pos, iid) + VALUES ($myid, $pos, $iid); + } + incr pos + } + } + return + } + + method timerange {} { return [$mytypeobj timerange $myitems] } + + method limits {} { + struct::list assign [$mytypeobj limits $myitems] maxp mins + return [list [TagItemDict $maxp $mytype] [TagItemDict $mins $mytype]] + } + + method drop {} { + log write 8 csets {Dropping $self = [$self str]} + + state transaction { + state run { + DELETE FROM changeset WHERE cid = $myid; + DELETE FROM csitem WHERE cid = $myid; + DELETE FROM cssuccessor WHERE cid = $myid; + } + } + + # Return the list of predecessors so that they can be adjusted. + return [struct::list map [state run { + SELECT cid + FROM cssuccessor + WHERE nid = $myid + }] [mytypemethod of]] + } + + method reportloop {{kill 1}} { + # We print the items which are producing the loop, and how. + + set hdr "Self-referential changeset [$self str] __________________" + set ftr [regsub -all {[^ ]} $hdr {_}] + + log write 0 csets $hdr + foreach {item nextitem} [$mytypeobj loops $myitems] { + # Create tagged items from the id and our type. + set item [list $mytype $item] + set nextitem [list $mytype $nextitem] + # Printable labels. + set i "<[$type itemstr $item]>" + set n "<[$type itemstr $nextitem]>" + set ncs $myitemmap($nextitem) + # Print + log write 0 csets {* $i --> $n --> cs [$ncs str]} + } + log write 0 csets $ftr + + if {!$kill} return + trouble internal "[$self str] depends on itself" + return + } + + method pushto {repository date rstate} { + # Generate and import the manifest for this changeset. + # + # Data needed: + # - Commit message (-- mysrcid -> repository meta) + # - User doing the commit (s.a.) + # + # - Timestamp of when committed (command argument) + # + # - The parent changeset, if any. If there is no parent fossil + # will use the empty base revision as parent. + # + # - List of the file revisions in the changeset. + + # We derive the lod information directly from the revisions of + # the changeset, as the branch part of the meta data (s.a.) is + # outdated since pass FilterSymbols. See the method 'run' in + # file "c2f_pfiltersym.tcl" for more commentary on this. + + set lodname [$self lod] + + log write 2 csets {Importing changeset [$self str] on $lodname} + + if {[$mytypeobj istag]} { + # Handle tags. They appear immediately after the revision + # they are attached to (*). We can assume that the + # workspace for the relevant line of development + # exists. We retrieve it, then the uuid of the last + # revision entered into it, then tag this revision. + + # (*) Immediately in terms of the relevant line of + # development. Revisions on other lines may come in + # between, but they do not matter to that. + + set lws [Getworkspace $rstate $lodname $myproject 0] + set uuid [lindex [$lws getid] 1] + + $repository tag $uuid [state one { + SELECT S.name + FROM symbol S + WHERE S.sid = $mysrcid + }] + + } elseif {[$mytypeobj isbranch]} { + + # Handle branches. They appear immediately after the + # revision they are spawned from (*). We can assume that + # the workspace for the relevant line of development + # exists. + + # We retrieve it, then the uuid of the last revision + # entered into it. That revision is tagged as the root of + # the branch (**). A new workspace for the branch is + # created as well, for the future revisions of the new + # line of development. + + # An exception is made of the non-trunk default branch, + # aka vendor branch. This lod has to have a workspace not + # inherited from anything else. It has no root either, so + # tagging is out as well. + + # (*) Immediately in terms of the relevant line of + # development. Revisions on other lines may come in + # between, but they do not matter to that. + + # (**) Tagging the parent revision of the branch as its + # root is done to let us know about the existence of + # the branch even if it has no revisions committed to + # it, and thus no regular branch tag anywhere else. + # The name of the tag is the name for the lod, with + # the suffix '-root' appended to it. + + # LOD is self symbol of branch, not parent + set lodname [state one { + SELECT S.name + FROM symbol S + WHERE S.sid = $mysrcid + }] + + if {![$rstate has :trunk:]} { + # No trunk implies default branch. Just create the + # proper workspace. + Getworkspace $rstate $lodname $myproject 1 + } else { + # Non-default branch. Need workspace, and tag parent + # revision. + + set lws [Getworkspace $rstate $lodname $myproject 0] + set uuid [lindex [$lws getid] 1] + + $repository tag $uuid ${lodname}-root + } + } else { + # Revision changeset. + + struct::list assign [$myproject getmeta $mysrcid] __ __ user message + + # Perform the import. As part of that we determine the + # parent we need, and convert the list of items in the + # changeset into uuids and printable data. + + struct::list assign [Getisdefault $myitems] \ + isdefault lastdefaultontrunk + + log write 8 csets {LOD '$lodname'} + log write 8 csets { def? $isdefault} + log write 8 csets { last? $lastdefaultontrunk} + + set lws [Getworkspace $rstate $lodname $myproject $isdefault] + $lws add [Getrevisioninfo $myitems] + + struct::list assign \ + [$repository importrevision [$self str] \ + $user $message $date \ + [lindex [$lws getid] 0] [$lws get]] \ + rid uuid + + if {[$lws ticks] == 1} { + # First commit on this line of development. Set our + # own name as a propagating tag. And if the LOD has a + # parent we have to prevent the propagation of that + # tag into this new line. + + set plws [$lws parent] + if {$plws ne ""} { + $repository branchcancel $uuid [$plws name] + } + $repository branchmark $uuid [$lws name] + } + + # Remember the imported changeset in the state, under our + # LOD. And if it is the last trunk changeset on the vendor + # branch then the revision is also the actual root of the + # :trunk:, so we remember it as such in the state. However + # if the trunk already exists then the changeset cannot be + # on it any more. This indicates weirdness in the setup of + # the vendor branch, but one we can work around. + + $lws defid [list $rid $uuid] + if {$lastdefaultontrunk} { + log write 2 csets {This cset is the last on the NTDB, set the trunk workspace up} + + if {[$rstate has :trunk:]} { + log write 2 csets {Multiple changesets declared to be the last trunk changeset on the vendor-branch} + } else { + $rstate new :trunk: [$lws name] + } + } + } + + log write 2 csets { } + log write 2 csets { } + return + } + + proc Getrevisioninfo {revisions} { + set theset ('[join $revisions {','}]') + set revisions {} + state foreachrow [subst -nocommands -nobackslashes { + SELECT U.uuid AS frid, + F.visible AS path, + F.name AS fname, + R.rev AS revnr, + R.op AS rop + FROM revision R, revuuid U, file F + WHERE R.rid IN $theset -- All specified revisions + AND U.rid = R.rid -- get fossil uuid of revision + AND F.fid = R.fid -- get file of revision + }] { + lappend revisions $frid $path $fname/$revnr $rop + } + return $revisions + } + + proc Getworkspace {rstate lodname project isdefault} { + + # The state object holds the workspace state of each known + # line-of-development (LOD), up to the last committed + # changeset belonging to that LOD. + + # (*) Standard handling if in-LOD changesets. If the LOD of + # the current changeset exists in the state (= has been + # committed to) then this it has the workspace we are + # looking for. + + if {[$rstate has $lodname]} { + return [$rstate get $lodname] + } + + # If the LOD is however not yet known, then the current + # changeset can be either of + # (a) root of a vendor branch, + # (b) root of the trunk LOD, or + # (c) the first changeset in a new LOD which was spawned from + # an existing LOD. + + # For both (a) and (b) we have to create a new workspace for + # the lod, and it doesn't inherit from anything. + + # One exception for (a). If we already have a :vendor: branch + # then multiple symbols were used for the vendor branch by + # different files. In that case the 'new' branch is made an + # alias of the :vendor:, effectively merging the symbols + # together. + + # Note that case (b) may never occur. See the variable + # 'lastdefaultontrunk' in the caller (method pushto). This + # flag can the generation of the workspace for the :trunk: LOD + # as well, making it inherit the state of the last + # trunk-changeset on the vendor-branch. + + if {$isdefault} { + if {![$rstate has ":vendor:"]} { + # Create the vendor branch if not present already. We + # use the actual name for the lod, and additional make + # it accessible under an internal name (:vendor:) so + # that we can merge to it later, should it become + # necessary. See the other branch below. + $rstate new $lodname + $rstate dup :vendor: <-- $lodname + } else { + # Merge the new symbol to the vendor branch + $rstate dup $lodname <-- :vendor: + } + return [$rstate get $lodname] + } + + if {$lodname eq ":trunk:"} { + return [$rstate new $lodname] + } + + # Case (c). We find the parent LOD of our LOD and let the new + # workspace inherit from the parent's workspace. + + set plodname [[[$project getsymbol $lodname] parent] name] + + log write 8 csets {pLOD '$plodname'} + + if {[$rstate has $plodname]} { + return [$rstate new $lodname $plodname] + } + + foreach k [lsort [$rstate names]] { + log write 8 csets { $k = [[$rstate get $k] getid]} + } + + trouble internal {Unable to determine changeset parent} + return + } + + proc Getisdefault {revisions} { + set theset ('[join $revisions {','}]') + + struct::list assign [state run [subst -nocommands -nobackslashes { + SELECT R.isdefault, R.dbchild + FROM revision R + WHERE R.rid IN $theset -- All specified revisions + LIMIT 1 + }]] def last + + # TODO/CHECK: look for changesets where isdefault/dbchild is + # ambigous. + + return [list $def [expr {$last ne ""}]] + } + + typemethod split {cset args} { + # As part of the creation of the new changesets specified in + # ARGS as sets of items, all subsets of CSET's item set, CSET + # will be dropped from all databases, in and out of memory, + # and then destroyed. + # + # Note: The item lists found in args are tagged items. They + # have to have the same type as the changeset, being subsets + # of its items. This is checked in Untag1. + + log write 8 csets {OLD: [lsort [$cset items]]} + ValidateFragments $cset $args + + # All checks pass, actually perform the split. + + struct::list assign [$cset data] project cstype cssrc + + set predecessors [$cset drop] + $cset destroy + + set newcsets {} + foreach fragmentitems $args { + log write 8 csets {MAKE: [lsort $fragmentitems]} + + set fragment [$type %AUTO% $project $cstype $cssrc \ + [Untag $fragmentitems $cstype]] + lappend newcsets $fragment + + $fragment persist + $fragment determinesuccessors + } + + # The predecessors have to recompute their successors, i.e. + # remove the dropped changeset and put one of the fragments + # into its place. + foreach p $predecessors { + $p determinesuccessors + } + + return $newcsets + } + + typemethod itemstr {item} { + struct::list assign $item itype iid + return [$itype str $iid] + } + + typemethod strlist {changesets} { + return [join [struct::list map $changesets [myproc ID]]] + } + + proc ID {cset} { $cset str } + + proc Untag {taggeditems cstype} { + return [struct::list map $taggeditems [myproc Untag1 $cstype]] + } + + proc Untag1 {cstype theitem} { + struct::list assign $theitem t i + integrity assert {$cstype eq $t} {Item $i's type is '$t', expected '$cstype'} + return $i + } + + proc TagItemDict {itemdict cstype} { + set res {} + foreach {i v} $itemdict { lappend res [list $cstype $i] $v } + return $res + } + + proc ValidateFragments {cset fragments} { + # Check the various integrity constraints for the fragments + # specifying how to split the changeset: + # + # * We must have two or more fragments, as splitting a + # changeset into one makes no sense. + # * No fragment may be empty. + # * All fragments have to be true subsets of the items in the + # changeset to split. The 'true' is implied because none are + # allowed to be empty, so each has to be smaller than the + # total. + # * The union of the fragments has to be the item set of the + # changeset. + # * The fragment must not overlap, i.e. their pairwise + # intersections have to be empty. + + set cover {} + foreach fragmentitems $fragments { + log write 8 csets {NEW: [lsort $fragmentitems]} + + integrity assert { + ![struct::set empty $fragmentitems] + } {changeset fragment is empty} + + integrity assert { + [struct::set subsetof $fragmentitems [$cset items]] + } {changeset fragment is not a subset} + struct::set add cover $fragmentitems + } + + integrity assert { + [struct::set equal $cover [$cset items]] + } {The fragments do not cover the original changeset} + + set i 1 + foreach fia $fragments { + foreach fib [lrange $fragments $i end] { + integrity assert { + [struct::set empty [struct::set intersect $fia $fib]] + } {The fragments <$fia> and <$fib> overlap} + } + incr i + } + + return + } + + # # ## ### ##### ######## ############# + ## State + + variable myid {} ; # Id of the cset for the persistent + # state. + variable myproject {} ; # Reference of the project object the + # changeset belongs to. + variable mytype {} ; # What the changeset is based on + # (revisions, tags, or branches). + # Values: See mycstype. Note that we + # have to keep the names of the helper + # singletons in sync with the contents + # of state table 'cstype', and various + # other places using them hardwired. + variable mytypeobj {} ; # Reference to the container for the + # type dependent code. Derived from + # mytype. + variable mysrcid {} ; # Id of the metadata or symbol the cset + # is based on. + variable myitems {} ; # List of the file level revisions, + # tags, or branches in the cset, as + # ids. Not tagged. + variable mytitems {} ; # As myitems, the tagged form. + variable mypos {} ; # Commit position of the changeset, if + # known. + + # # ## ### ##### ######## ############# + ## Internal methods + + typevariable mycounter 0 ; # Id counter for csets. Last id + # used. + typevariable mycstype -array {} ; # Map cstypes (names) to persistent + # ids. Note that we have to keep + # the names in the table 'cstype' + # in sync with the names of the + # helper singletons. + + typemethod inorder {projectid} { + # Return all changesets (object references) for the specified + # project, in the order given to them by the sort passes. Both + # the filtering by project and the sorting by time make the + # use of 'project::rev rev' impossible. + + set res {} + state foreachrow { + SELECT C.cid AS xcid, + T.date AS cdate + FROM changeset C, cstimestamp T + WHERE C.pid = $projectid -- limit to changesets in project + AND T.cid = C.cid -- get ordering information + ORDER BY T.date -- sort into commit order + } { + lappend res $myidmap($xcid) $cdate + } + return $res + } + + typemethod getcstypes {} { + state foreachrow { + SELECT tid, name FROM cstype; + } { set mycstype($name) $tid } + return + } + + typemethod load {repository} { + set n 0 + log write 2 csets {Loading the changesets} + state foreachrow { + SELECT C.cid AS id, + C.pid AS xpid, + CS.name AS cstype, + C.src AS srcid + FROM changeset C, cstype CS + WHERE C.type = CS.tid + ORDER BY C.cid + } { + log progress 2 csets $n {} + set r [$type %AUTO% [$repository projectof $xpid] $cstype $srcid [state run { + SELECT C.iid + FROM csitem C + WHERE C.cid = $id + ORDER BY C.pos + }] $id] + incr n + } + return + } + + typemethod loadcounter {} { + # Initialize the counter from the state + log write 2 csets {Loading changeset counter} + set mycounter [state one { SELECT MAX(cid) FROM changeset }] + return + } + + typemethod num {} { return $mycounter } + + # # ## ### ##### ######## ############# + + method CreateFromFragments {fragments cv bv} { + upvar 1 $cv counter $bv breaks + UnmapItems $mytype $myitems + + # Create changesets for the fragments, reusing the current one + # for the first fragment. We sort them in order to allow + # checking for gaps and nice messages. + + set newcsets {} + set fragments [lsort -index 0 -integer $fragments] + + #puts \t.[join [PRs $fragments] .\n\t.]. + + Border [lindex $fragments 0] firsts firste + + integrity assert { + $firsts == 0 + } {Bad fragment start @ $firsts, gap, or before beginning of the range} + + set laste $firste + foreach fragment [lrange $fragments 1 end] { + Border $fragment s e + integrity assert { + $laste == ($s - 1) + } {Bad fragment border <$laste | $s>, gap or overlap} + + set new [$type %AUTO% $myproject $mytype $mysrcid [lrange $myitems $s $e]] + lappend newcsets $new + incr counter + + log write 4 csets {Breaking [$self str ] @ $laste, new [$new str], cutting $breaks($laste)} + + set laste $e + } + + integrity assert { + $laste == ([llength $myitems]-1) + } {Bad fragment end @ $laste, gap, or beyond end of the range} + + # Put the first fragment into the current changeset, and + # update the in-memory index. We can simply (re)add the items + # because we cleared the previously existing information, see + # 'UnmapItems' above. Persistence does not matter here, none + # of the changesets has been saved to the persistent state + # yet. + + set myitems [lrange $myitems 0 $firste] + set mytitems [lrange $mytitems 0 $firste] + MapItems $mytype $myitems + return $newcsets + } + + # # ## ### ##### ######## ############# + + proc BreakDirectDependencies {theitems bv} { + upvar 1 mytypeobj mytypeobj self self $bv breaks + + # Array of dependencies (parent -> child). This is pulled from + # the state, and limited to successors within the changeset. + + array set dependencies {} + + $mytypeobj internalsuccessors dependencies $theitems + if {![array size dependencies]} { + return {} + } ; # Nothing to break. + + log write 5 csets ...[$self str]....................................................... + vc::tools::mem::mark + + return [BreakerCore $theitems dependencies breaks] + } + + proc BreakerCore {theitems dv bv} { + # Break a set of revisions into fragments which have no + # internal dependencies. + + # We perform all necessary splits in one go, instead of only + # one. The previous algorithm, adapted from cvs2svn, computed + # a lot of state which was thrown away and then computed again + # for each of the fragments. It should be easier to update and + # reuse that state. + + upvar 1 $dv dependencies $bv breaks + + # We have internal dependencies to break. We now iterate over + # all positions in the list (which is chronological, at least + # as far as the timestamps are correct and unique) and + # determine the best position for the break, by trying to + # break as many dependencies as possible in one go. When a + # break was found this is redone for the fragments coming and + # after, after upding the crossing information. + + # Data structures: + # Map: POS revision id -> position in list. + # CROSS position in list -> number of dependencies crossing it + # DEPC dependency -> positions it crosses + # List: RANGE Of the positions itself. + # Map: DELTA position in list -> time delta between its revision + # and the next, if any. + # A dependency is a single-element map parent -> child + + # InitializeBreakState initializes their contents after + # upvar'ing them from this scope. It uses the information in + # DEPENDENCIES to do so. + + InitializeBreakState $theitems + + set fragments {} + set new [list $range] + + # Instead of one list holding both processed and pending + # fragments we use two, one for the framents to process, one + # to hold the new fragments, and the latter is copied to the + # former when they run out. This keeps the list of pending + # fragments short without sacrificing speed by shifting stuff + # down. We especially drop the memory of fragments broken + # during processing after a short time, instead of letting it + # consume memory. + + while {[llength $new]} { + + set pending $new + set new {} + set at 0 + + while {$at < [llength $pending]} { + set current [lindex $pending $at] + + log write 6 csets {. . .. ... ..... ........ .............} + log write 6 csets {Scheduled [join [PRs [lrange $pending $at end]] { }]} + log write 6 csets {Considering [PR $current] \[$at/[llength $pending]\]} + + set best [FindBestBreak $current] + + if {$best < 0} { + # The inspected range has no internal + # dependencies. This is a complete fragment. + lappend fragments $current + + log write 6 csets "No breaks, final" + } else { + # Split the range and schedule the resulting + # fragments for further inspection. Remember the + # number of dependencies cut before we remove them + # from consideration, for documentation later. + + set breaks($best) $cross($best) + + log write 6 csets "Best break @ $best, cutting [nsp $cross($best) dependency dependencies]" + + # Note: The value of best is an abolute location + # in myitems. Use the start of current to make it + # an index absolute to current. + + set brel [expr {$best - [lindex $current 0]}] + set bnext $brel ; incr bnext + set fragbefore [lrange $current 0 $brel] + set fragafter [lrange $current $bnext end] + + log write 6 csets "New pieces [PR $fragbefore] [PR $fragafter]" + + integrity assert {[llength $fragbefore]} {Found zero-length fragment at the beginning} + integrity assert {[llength $fragafter]} {Found zero-length fragment at the end} + + lappend new $fragbefore $fragafter + CutAt $best + } + + incr at + } + } + + log write 6 csets ". . .. ... ..... ........ ............." + + return $fragments + } + + proc InitializeBreakState {revisions} { + upvar 1 pos pos cross cross range range depc depc delta delta \ + dependencies dependencies + + # First we create a map of positions to make it easier to + # determine whether a dependency crosses a particular index. + + log write 14 csets {IBS: #rev [llength $revisions]} + log write 14 csets {IBS: pos map, cross counter} + + array set pos {} + array set cross {} + array set depc {} + set range {} + set n 0 + foreach rev $revisions { + lappend range $n + set pos($rev) $n + set cross($n) 0 + incr n + } + + log write 14 csets {IBS: pos/[array size pos], cross/[array size cross]} + + # Secondly we count the crossings per position, by iterating + # over the recorded internal dependencies. + + # Note: If the timestamps are badly out of order it is + # possible to have a backward successor dependency, + # i.e. with start > end. We may have to swap the indices + # to ensure that the following loop runs correctly. + # + # Note 2: start == end is not possible. It indicates a + # self-dependency due to the uniqueness of positions, + # and that is something we have ruled out already, see + # 'rev internalsuccessors'. + + log write 14 csets {IBS: cross counter filling, pos/cross map} + + foreach {rid children} [array get dependencies] { + foreach child $children { + set dkey [list $rid $child] + set start $pos($rid) + set end $pos($child) + + if {$start > $end} { + set crosses [list $end [expr {$start-1}]] + while {$end < $start} { + incr cross($end) + incr end + } + } else { + set crosses [list $start [expr {$end-1}]] + while {$start < $end} { + incr cross($start) + incr start + } + } + set depc($dkey) $crosses + } + } + + log write 14 csets {IBS: pos/[array size pos], cross/[array size cross], depc/[array size depc] (for [llength $revisions])} + log write 14 csets {IBS: timestamps, deltas} + + InitializeDeltas $revisions + + log write 14 csets {IBS: delta [array size delta]} + return + } + + proc InitializeDeltas {revisions} { + upvar 1 delta delta + + # Pull the timestamps for all revisions in the changesets and + # compute their deltas for use by the break finder. + + array set delta {} + array set stamp {} + + set theset ('[join $revisions {','}]') + state foreachrow [subst -nocommands -nobackslashes { + SELECT R.rid AS xrid, R.date AS time + FROM revision R + WHERE R.rid IN $theset + }] { + set stamp($xrid) $time + } + + log write 14 csets {IBS: stamp [array size stamp]} + + set n 0 + foreach rid [lrange $revisions 0 end-1] rnext [lrange $revisions 1 end] { + set delta($n) [expr {$stamp($rnext) - $stamp($rid)}] + incr n + } + return + } + + proc FindBestBreak {range} { + upvar 1 cross cross delta delta + + # Determine the best break location in the given range of + # positions. First we look for the locations with the maximal + # number of crossings. If there are several we look for the + # shortest time interval among them. If we still have multiple + # possibilities after that we select the earliest location + # among these. + + # Note: If the maximal number of crossings is 0 then the range + # has no internal dependencies, and no break location at + # all. This possibility is signaled via result -1. + + # Note: A range of length 1 or less cannot have internal + # dependencies, as that needs at least two revisions in + # the range. + + if {[llength $range] < 2} { return -1 } + + set max -1 + set best {} + + foreach location $range { + set crossings $cross($location) + if {$crossings > $max} { + set max $crossings + set best [list $location] + continue + } elseif {$crossings == $max} { + lappend best $location + } + } + + if {$max == 0} { return -1 } + if {[llength $best] == 1} { return [lindex $best 0] } + + set locations $best + set best {} + set min -1 + + foreach location $locations { + set interval $delta($location) + if {($min < 0) || ($interval < $min)} { + set min $interval + set best [list $location] + } elseif {$interval == $min} { + lappend best $location + } + } + + if {[llength $best] == 1} { return [lindex $best 0] } + + return [lindex [lsort -integer -increasing $best] 0] + } + + proc CutAt {location} { + upvar 1 cross cross depc depc + + # It was decided to split the changeset at the given + # location. This cuts a number of dependencies. Here we update + # the cross information so that the break finder has accurate + # data when we look at the generated fragments. + + set six [log visible? 6] + + # Note: The loop below could be made faster by keeping a map + # from positions to the dependencies crossing. An extension of + # CROSS, i.e. list of dependencies, counter is implied. Takes + # a lot more memory however, and takes time to update here + # (The inner loop is not incr -1, but ldelete). + + foreach dep [array names depc] { + set range $depc($dep) + # Check all dependencies still known, take their range and + # see if the break location falls within. + + Border $range s e + if {$location < $s} continue ; # break before range, ignore + if {$location > $e} continue ; # break after range, ignore. + + # This dependency crosses the break location. We remove it + # from the crossings counters, and then also from the set + # of known dependencies, as we are done with it. + + Border $depc($dep) ds de + for {set loc $ds} {$loc <= $de} {incr loc} { + incr cross($loc) -1 + } + unset depc($dep) + + if {!$six} continue + + struct::list assign $dep parent child + log write 5 csets "Broke dependency [PD $parent] --> [PD $child]" + } + + return + } + + # Print identifying data for a revision (project, file, dotted rev + # number), for high verbosity log output. + # TODO: Replace with call to itemstr (list rev $id) + + proc PD {id} { + foreach {p f r} [state run { + SELECT P.name , F.name, R.rev + FROM revision R, file F, project P + WHERE R.rid = $id -- Find specified file revision + AND F.fid = R.fid -- Get file of the revision + AND P.pid = F.pid -- Get project of the file. + }] break + return "'$p : $f/$r'" + } + + # Printing one or more ranges, formatted, and only their border to + # keep the strings short. + + proc PRs {ranges} { + return [struct::list map $ranges [myproc PR]] + } + + proc PR {range} { + Border $range s e + return <${s}...${e}> + } + + proc Border {range sv ev} { + upvar 1 $sv s $ev e + set s [lindex $range 0] + set e [lindex $range end] + return + } + + # # ## ### ##### ######## ############# + + proc UnmapItems {thetype theitems} { + # (*) We clear out the associated part of the myitemmap + # in-memory index in preparation for new data, or as part of + # object destruction. A simple unset is enough, we have no + # symbol changesets at this time, and thus never more than one + # reference in the list. + + upvar 1 myitemmap myitemmap self self + foreach iid $theitems { + set key [list $thetype $iid] + unset myitemmap($key) + log write 8 csets {MAP- item <$key> $self = [$self str]} + } + return + } + + proc MapItems {thetype theitems} { + upvar 1 myitemmap myitemmap self self + + foreach iid $theitems { + set key [list $thetype $iid] + set myitemmap($key) $self + log write 8 csets {MAP+ item <$key> $self = [$self str]} + } + return + } + + # # ## ### ##### ######## ############# + + typevariable mychangesets {} ; # List of all known + # changesets. + + # List of all known changesets of a type. + typevariable mytchangesets -array { + sym::branch {} + sym::tag {} + rev {} + } + + typevariable myitemmap -array {} ; # Map from items (tagged) + # to the list of changesets + # containing it. Each item + # can be used by only one + # changeset. + typevariable myidmap -array {} ; # Map from changeset id to + # changeset. + + typemethod all {} { return $mychangesets } + typemethod of {cid} { return $myidmap($cid) } + typemethod ofitem {iid} { return $myitemmap($iid) } + + typemethod rev {} { return $mytchangesets(rev) } + typemethod sym {} { return [concat \ + ${mytchangesets(sym::branch)} \ + ${mytchangesets(sym::tag)}] } + + # # ## ### ##### ######## ############# + ## Configuration + + pragma -hastypeinfo no ; # no type introspection + pragma -hasinfo no ; # no object introspection + + # # ## ### ##### ######## ############# +} + +## +## NOTE: The successor and predecessor methods defined by the classes +## below are -- bottle necks --. Look for ways to make the SQL +## faster. +## + +# # ## ### ##### ######## ############# ##################### +## Helper singleton. Commands for revision changesets. + +snit::type ::vc::fossil::import::cvs::project::rev::rev { + typemethod byrevision {} { return 1 } + typemethod bysymbol {} { return 0 } + typemethod istag {} { return 0 } + typemethod isbranch {} { return 0 } + + typemethod str {revision} { + struct::list assign [state run { + SELECT R.rev, F.name, P.name + FROM revision R, file F, project P + WHERE R.rid = $revision -- Find specified file revision + AND F.fid = R.fid -- Get file of the revision + AND P.pid = F.pid -- Get project of the file. + }] revnr fname pname + return "$pname/${revnr}::$fname" + } + + # result = list (mintime, maxtime) + typemethod timerange {items} { + set theset ('[join $items {','}]') + return [state run [subst -nocommands -nobackslashes { + SELECT MIN(R.date), MAX(R.date) + FROM revision R + WHERE R.rid IN $theset -- Restrict to revisions of interest + }]] + } + + # var(dv) = dict (revision -> list (revision)) + typemethod internalsuccessors {dv revisions} { + upvar 1 $dv dependencies + set theset ('[join $revisions {','}]') + + log write 14 csets internalsuccessors + + # See 'successors' below for the main explanation of + # the various cases. This piece is special in that it + # restricts the successors we look for to the same set of + # revisions we start from. Sensible as we are looking for + # changeset internal dependencies. + + array set dep {} + + state foreachrow [subst -nocommands -nobackslashes { + -- (1) Primary child + SELECT R.rid AS xrid, R.child AS xchild + FROM revision R + WHERE R.rid IN $theset -- Restrict to revisions of interest + AND R.child IS NOT NULL -- Has primary child + AND R.child IN $theset -- Which is also of interest + UNION + -- (2) Secondary (branch) children + SELECT R.rid AS xrid, B.brid AS xchild + FROM revision R, revisionbranchchildren B + WHERE R.rid IN $theset -- Restrict to revisions of interest + AND R.rid = B.rid -- Select subset of branch children + AND B.brid IN $theset -- Which is also of interest + UNION + -- (4) Child of trunk root successor of last NTDB on trunk. + SELECT R.rid AS xrid, RA.child AS xchild + FROM revision R, revision RA + WHERE R.rid IN $theset -- Restrict to revisions of interest + AND R.isdefault -- Restrict to NTDB + AND R.dbchild IS NOT NULL -- and last NTDB belonging to trunk + AND RA.rid = R.dbchild -- Go directly to trunk root + AND RA.child IS NOT NULL -- Has primary child. + AND RA.child IN $theset -- Which is also of interest + }] { + # Consider moving this to the integrity module. + integrity assert {$xrid != $xchild} {Revision $xrid depends on itself.} + lappend dependencies($xrid) $xchild + set dep($xrid,$xchild) . + } + + # The sql statements above looks only for direct dependencies + # between revision in the changeset. However due to the + # vagaries of meta data it is possible for two revisions of + # the same file to end up in the same changeset, without a + # direct dependency between them. However we know that there + # has to be a an indirect dependency, be it through primary + # children, branch children, or a combination thereof. + + # We now fill in these pseudo-dependencies, if no such + # dependency exists already. The direction of the dependency + # is actually irrelevant for this. + + # NOTE: This is different from cvs2svn. Our spiritual ancestor + # does not use such pseudo-dependencies, however it uses a + # COMMIT_THRESHOLD, a time interval commits should fall. This + # will greatly reduces the risk of getting far separated + # revisions of the same file into one changeset. + + # We allow revisions to be far apart in time in the same + # changeset, but in turn need the pseudo-dependencies to + # handle this. + + log write 14 csets {internal [array size dep]} + log write 14 csets {collected [array size dependencies]} + log write 14 csets pseudo-internalsuccessors + + array set fids {} + state foreachrow [subst -nocommands -nobackslashes { + SELECT R.rid AS xrid, R.fid AS xfid + FROM revision R + WHERE R.rid IN $theset + }] { lappend fids($xfid) $xrid } + + set groups {} + foreach {fid rids} [array get fids] { + if {[llength $rids] < 2} continue + foreach a $rids { + foreach b $rids { + if {$a == $b} continue + if {[info exists dep($a,$b)]} continue + if {[info exists dep($b,$a)]} continue + lappend dependencies($a) $b + set dep($a,$b) . + set dep($b,$a) . + } + } + set n [llength $rids] + lappend groups [list $n [expr {($n*$n-$n)/2}]] + } + + log write 14 csets {pseudo [array size fids] ([lsort -index 0 -decreasing -integer $groups])} + log write 14 csets {internal [array size dep]} + log write 14 csets {collected [array size dependencies]} + log write 14 csets complete + return + } + + # result = 4-list (itemtype itemid nextitemtype nextitemid ...) + typemethod loops {revisions} { + # Note: Tags and branches cannot cause the loop. Their id's, + # being of a fundamentally different type than the revisions + # coming in cannot be in the set. + + set theset ('[join $revisions {','}]') + return [state run [subst -nocommands -nobackslashes { + -- (1) Primary child + SELECT R.rid, R.child + FROM revision R + WHERE R.rid IN $theset -- Restrict to revisions of interest + AND R.child IS NOT NULL -- Has primary child + AND R.child IN $theset -- Loop + -- + UNION + -- (2) Secondary (branch) children + SELECT R.rid, B.brid + FROM revision R, revisionbranchchildren B + WHERE R.rid IN $theset -- Restrict to revisions of interest + AND R.rid = B.rid -- Select subset of branch children + AND B.rid IN $theset -- Loop + -- + UNION + -- (4) Child of trunk root successor of last NTDB on trunk. + SELECT R.rid, RA.child + FROM revision R, revision RA + WHERE R.rid IN $theset -- Restrict to revisions of interest + AND R.isdefault -- Restrict to NTDB + AND R.dbchild IS NOT NULL -- and last NTDB belonging to trunk + AND RA.rid = R.dbchild -- Go directly to trunk root + AND RA.child IS NOT NULL -- Has primary child. + AND RA.child IN $theset -- Loop + }]] + } + + # var(dv) = dict (item -> list (item)), item = list (type id) + typemethod successors {dv revisions} { + upvar 1 $dv dependencies + set theset ('[join $revisions {','}]') + + # The following cases specify when a revision S is a successor + # of a revision R. Each of the cases translates into one of + # the branches of the SQL UNION coming below. + # + # (1) S can be a primary child of R, i.e. in the same LOD. R + # references S directly. R.child = S(.rid), if it exists. + # + # (2) S can be a secondary, i.e. branch, child of R. Here the + # link is made through the helper table + # REVISIONBRANCHCHILDREN. R.rid -> RBC.rid, RBC.brid = + # S(.rid) + # + # (3) Originally this use case defined the root of a detached + # NTDB as the successor of the trunk root. This leads to a + # bad tangle later on. With a detached NTDB the original + # trunk root revision was removed as irrelevant, allowing + # the nominal root to be later in time than the NTDB + # root. Now setting this dependency will be backward in + # time. REMOVED. + # + # (4) If R is the last of the NTDB revisions which belong to + # the trunk, then the primary child of the trunk root (the + # '1.2' revision) is a successor, if it exists. + + # Note that the branches spawned from the revisions, and the + # tags associated with them are successors as well. + + state foreachrow [subst -nocommands -nobackslashes { + -- (1) Primary child + SELECT R.rid AS xrid, R.child AS xchild + FROM revision R + WHERE R.rid IN $theset -- Restrict to revisions of interest + AND R.child IS NOT NULL -- Has primary child + UNION + -- (2) Secondary (branch) children + SELECT R.rid AS xrid, B.brid AS xchild + FROM revision R, revisionbranchchildren B + WHERE R.rid IN $theset -- Restrict to revisions of interest + AND R.rid = B.rid -- Select subset of branch children + UNION + -- (4) Child of trunk root successor of last NTDB on trunk. + SELECT R.rid AS xrid, RA.child AS xchild + FROM revision R, revision RA + WHERE R.rid IN $theset -- Restrict to revisions of interest + AND R.isdefault -- Restrict to NTDB + AND R.dbchild IS NOT NULL -- and last NTDB belonging to trunk + AND RA.rid = R.dbchild -- Go directly to trunk root + AND RA.child IS NOT NULL -- Has primary child. + }] { + # Consider moving this to the integrity module. + integrity assert {$xrid != $xchild} {Revision $xrid depends on itself.} + lappend dependencies([list rev $xrid]) [list rev $xchild] + } + state foreachrow [subst -nocommands -nobackslashes { + SELECT R.rid AS xrid, T.tid AS xchild + FROM revision R, tag T + WHERE R.rid IN $theset -- Restrict to revisions of interest + AND T.rev = R.rid -- Select tags attached to them + }] { + lappend dependencies([list rev $xrid]) [list sym::tag $xchild] + } + state foreachrow [subst -nocommands -nobackslashes { + SELECT R.rid AS xrid, B.bid AS xchild + FROM revision R, branch B + WHERE R.rid IN $theset -- Restrict to revisions of interest + AND B.root = R.rid -- Select branches attached to them + }] { + lappend dependencies([list rev $xrid]) [list sym::branch $xchild] + } + return + } + + # result = list (changeset-id) + typemethod cs_successors {revisions} { + # This is a variant of 'successors' which maps the low-level + # data directly to the associated changesets. I.e. instead + # millions of dependency pairs (in extreme cases (Example: Tcl + # CVS)) we return a very short and much more manageable list + # of changesets. + + set theset ('[join $revisions {','}]') + return [state run [subst -nocommands -nobackslashes { + -- (1) Primary child + SELECT C.cid + FROM revision R, csitem CI, changeset C + WHERE R.rid IN $theset -- Restrict to revisions of interest + AND R.child IS NOT NULL -- Has primary child + AND CI.iid = R.child -- Select all changesets + AND C.cid = CI.cid -- containing the primary child + AND C.type = 0 -- which are revision changesets + UNION + -- (2) Secondary (branch) children + SELECT C.cid + FROM revision R, revisionbranchchildren B, csitem CI, changeset C + WHERE R.rid IN $theset -- Restrict to revisions of interest + AND R.rid = B.rid -- Select subset of branch children + AND CI.iid = B.brid -- Select all changesets + AND C.cid = CI.cid -- containing the branch + AND C.type = 0 -- which are revision changesets + UNION + -- (4) Child of trunk root successor of last NTDB on trunk. + SELECT C.cid + FROM revision R, revision RA, csitem CI, changeset C + WHERE R.rid IN $theset -- Restrict to revisions of interest + AND R.isdefault -- Restrict to NTDB + AND R.dbchild IS NOT NULL -- and last NTDB belonging to trunk + AND RA.rid = R.dbchild -- Go directly to trunk root + AND RA.child IS NOT NULL -- Has primary child. + AND CI.iid = RA.child -- Select all changesets + AND C.cid = CI.cid -- containing the primary child + AND C.type = 0 -- which are revision changesets + UNION + SELECT C.cid + FROM revision R, tag T, csitem CI, changeset C + WHERE R.rid in $theset -- Restrict to revisions of interest + AND T.rev = R.rid -- Select tags attached to them + AND CI.iid = T.tid -- Select all changesets + AND C.cid = CI.cid -- containing the tags + AND C.type = 1 -- which are tag changesets + UNION + SELECT C.cid + FROM revision R, branch B, csitem CI, changeset C + WHERE R.rid in $theset -- Restrict to revisions of interest + AND B.root = R.rid -- Select branches attached to them + AND CI.iid = B.bid -- Select all changesets + AND C.cid = CI.cid -- containing the branches + AND C.type = 2 -- which are branch changesets + }]] + + # Regarding rev -> branch|tag, we could consider looking at + # the symbol of the branch|tag, its lod-symbol, and the + # revisions on that lod, but don't. Because it is not exact + # enough, the branch|tag would depend on revisions coming + # after its creation on the parental lod. + } + + # result = symbol name + typemethod cs_lod {metaid revisions} { + # Determines the name of the symbol which is the line of + # development for the revisions in a changeset. The + # information in the meta data referenced by the source metaid + # is out of date by the time we come here (since pass + # FilterSymbols), so it cannot be used. See the method 'run' + # in file "c2f_pfiltersym.tcl" for more commentary on this. + + set theset ('[join $revisions {','}]') + return [state run [subst -nocommands -nobackslashes { + SELECT + DISTINCT L.name + FROM revision R, symbol L + WHERE R.rid in $theset -- Restrict to revisions of interest + AND L.sid = R.lod -- Get lod symbol of revision + }]] + } +} + +# # ## ### ##### ######## ############# ##################### +## Helper singleton. Commands for tag symbol changesets. + +snit::type ::vc::fossil::import::cvs::project::rev::sym::tag { + typemethod byrevision {} { return 0 } + typemethod bysymbol {} { return 1 } + typemethod istag {} { return 1 } + typemethod isbranch {} { return 0 } + + typemethod str {tag} { + struct::list assign [state run { + SELECT S.name, F.name, P.name + FROM tag T, symbol S, file F, project P + WHERE T.tid = $tag -- Find specified tag + AND F.fid = T.fid -- Get file of tag + AND P.pid = F.pid -- Get project of file + AND S.sid = T.sid -- Get symbol of tag + }] sname fname pname + return "$pname/T'${sname}'::$fname" + } + + # result = list (mintime, maxtime) + typemethod timerange {tags} { + # The range is defined as the range of the revisions the tags + # are attached to. + + set theset ('[join $tags {','}]') + return [state run [subst -nocommands -nobackslashes { + SELECT MIN(R.date), MAX(R.date) + FROM tag T, revision R + WHERE T.tid IN $theset -- Restrict to tags of interest + AND R.rid = T.rev -- Select tag parent revisions + }]] + } + + # var(dv) = dict (item -> list (item)), item = list (type id) + typemethod successors {dv tags} { + # Tags have no successors. + return + } + + # result = 4-list (itemtype itemid nextitemtype nextitemid ...) + typemethod loops {tags} { + # Tags have no successors, therefore cannot cause loops + return {} + } + + # result = list (changeset-id) + typemethod cs_successors {tags} { + # Tags have no successors. + return + } + + # result = symbol name + typemethod cs_lod {sid tags} { + # Determines the name of the symbol which is the line of + # development for the tags in a changeset. Comes directly from + # the symbol which is the changeset's source and its prefered + # parent. + + return [state run { + SELECT P.name + FROM preferedparent SP, symbol P + WHERE SP.sid = $sid + AND P.sid = SP.pid + }] + } +} + +# # ## ### ##### ######## ############# ##################### +## Helper singleton. Commands for branch symbol changesets. + +snit::type ::vc::fossil::import::cvs::project::rev::sym::branch { + typemethod byrevision {} { return 0 } + typemethod bysymbol {} { return 1 } + typemethod istag {} { return 0 } + typemethod isbranch {} { return 1 } + + typemethod str {branch} { + struct::list assign [state run { + SELECT S.name, F.name, P.name + FROM branch B, symbol S, file F, project P + WHERE B.bid = $branch -- Find specified branch + AND F.fid = B.fid -- Get file of branch + AND P.pid = F.pid -- Get project of file + AND S.sid = B.sid -- Get symbol of branch + }] sname fname pname + return "$pname/B'${sname}'::$fname" + } + + # result = list (mintime, maxtime) + typemethod timerange {branches} { + # The range of a branch is defined as the range of the + # revisions the branches are spawned by. NOTE however that the + # branches associated with a detached NTDB will have no root + # spawning them, hence they have no real timerange any + # longer. By using 0 we put them in front of everything else, + # as they logically are. + + set theset ('[join $branches {','}]') + return [state run [subst -nocommands -nobackslashes { + SELECT IFNULL(MIN(R.date),0), IFNULL(MAX(R.date),0) + FROM branch B, revision R + WHERE B.bid IN $theset -- Restrict to branches of interest + AND R.rid = B.root -- Select branch parent revisions + }]] + } + + # result = 4-list (itemtype itemid nextitemtype nextitemid ...) + typemethod loops {branches} { + # Note: Revisions and tags cannot cause the loop. Being of a + # fundamentally different type they cannot be in the incoming + # set of ids. + + set theset ('[join $branches {','}]') + return [state run [subst -nocommands -nobackslashes { + SELECT B.bid, BX.bid + FROM branch B, preferedparent P, branch BX + WHERE B.bid IN $theset -- Restrict to branches of interest + AND B.sid = P.pid -- Get the prefered branches via + AND BX.sid = P.sid -- the branch symbols + AND BX.bid IN $theset -- Loop + }]] + } + + # var(dv) = dict (item -> list (item)), item = list (type id) + typemethod successors {dv branches} { + upvar 1 $dv dependencies + # The first revision committed on a branch, and all branches + # and tags which have it as their prefered parent are the + # successors of a branch. + + set theset ('[join $branches {','}]') + state foreachrow [subst -nocommands -nobackslashes { + SELECT B.bid AS xbid, R.rid AS xchild + FROM branch B, revision R + WHERE B.bid IN $theset -- Restrict to branches of interest + AND B.first = R.rid -- Get first revision on the branch + }] { + lappend dependencies([list sym::branch $xbid]) [list rev $xchild] + } + state foreachrow [subst -nocommands -nobackslashes { + SELECT B.bid AS xbid, BX.bid AS xchild + FROM branch B, preferedparent P, branch BX + WHERE B.bid IN $theset -- Restrict to branches of interest + AND B.sid = P.pid -- Get subordinate branches via the + AND BX.sid = P.sid -- prefered parents of their symbols + }] { + lappend dependencies([list sym::branch $xbid]) [list sym::branch $xchild] + } + state foreachrow [subst -nocommands -nobackslashes { + SELECT B.bid AS xbid, T.tid AS xchild + FROM branch B, preferedparent P, tag T + WHERE B.bid IN $theset -- Restrict to branches of interest + AND B.sid = P.pid -- Get subordinate tags via the + AND T.sid = P.sid -- prefered parents of their symbols + }] { + lappend dependencies([list sym::branch $xbid]) [list sym::tag $xchild] + } + return + } + + # result = list (changeset-id) + typemethod cs_successors {branches} { + # This is a variant of 'successors' which maps the low-level + # data directly to the associated changesets. I.e. instead + # millions of dependency pairs (in extreme cases (Example: Tcl + # CVS)) we return a very short and much more manageable list + # of changesets. + + set theset ('[join $branches {','}]') + return [state run [subst -nocommands -nobackslashes { + SELECT C.cid + FROM branch B, revision R, csitem CI, changeset C + WHERE B.bid IN $theset -- Restrict to branches of interest + AND B.first = R.rid -- Get first revision on the branch + AND CI.iid = R.rid -- Select all changesets + AND C.cid = CI.cid -- containing this revision + AND C.type = 0 -- which are revision changesets + UNION + SELECT C.cid + FROM branch B, preferedparent P, branch BX, csitem CI, changeset C + WHERE B.bid IN $theset -- Restrict to branches of interest + AND B.sid = P.pid -- Get subordinate branches via the + AND BX.sid = P.sid -- prefered parents of their symbols + AND CI.iid = BX.bid -- Select all changesets + AND C.cid = CI.cid -- containing the subordinate branches + AND C.type = 2 -- which are branch changesets + UNION + SELECT C.cid + FROM branch B, preferedparent P, tag T, csitem CI, changeset C + WHERE B.bid IN $theset -- Restrict to branches of interest + AND B.sid = P.pid -- Get subordinate tags via the + AND T.sid = P.sid -- prefered parents of their symbols + AND CI.iid = T.tid -- Select all changesets + AND C.cid = CI.cid -- containing the subordinate tags + AND C.type = 1 -- which are tag changesets + }]] + return + } + + # result = symbol name + typemethod cs_lod {sid branches} { + # Determines the name of the symbol which is the line of + # development for the branches in a changeset. Comes directly + # from the symbol which is the changeset's source and its + # prefered parent. + + return [state run { + SELECT P.name + FROM preferedparent SP, symbol P + WHERE SP.sid = $sid + AND P.sid = SP.pid + }] + } + + typemethod limits {branches} { + # Notes. This method exists only for branches. It is needed to + # get detailed information about a backward branch. It does + # not apply to tags, nor revisions. The queries can also + # restrict themselves to the revision sucessors/predecessors + # of branches, as only they have ordering data and thus can + # cause the backwardness. + + set theset ('[join $branches {','}]') + + set maxp [state run [subst -nocommands -nobackslashes { + -- maximal predecessor position per branch + SELECT B.bid, MAX (CO.pos) + FROM branch B, revision R, csitem CI, changeset C, csorder CO + WHERE B.bid IN $theset -- Restrict to branches of interest + AND B.root = R.rid -- Get branch root revisions + AND CI.iid = R.rid -- Get changesets containing the + AND C.cid = CI.cid -- root revisions, which are + AND C.type = 0 -- revision changesets + AND CO.cid = C.cid -- Get their topological ordering + GROUP BY B.bid + }]] + + set mins [state run [subst -nocommands -nobackslashes { + -- minimal successor position per branch + SELECT B.bid, MIN (CO.pos) + FROM branch B, revision R, csitem CI, changeset C, csorder CO + WHERE B.bid IN $theset -- Restrict to branches of interest + AND B.first = R.rid -- Get the first revisions on the branches + AND CI.iid = R.rid -- Get changesets containing the + AND C.cid = CI.cid -- first revisions, which are + AND C.type = 0 -- revision changesets + AND CO.cid = C.cid -- Get their topological ordering + GROUP BY B.bid + }]] + + return [list $maxp $mins] + } + + # # ## ### ##### ######## ############# + ## Configuration + + pragma -hasinstances no ; # singleton + pragma -hastypeinfo no ; # no introspection + pragma -hastypedestroy no ; # immortal +} + +# # ## ### ##### ######## ############# ##################### +## + +namespace eval ::vc::fossil::import::cvs::project { + namespace export rev + namespace eval rev { + namespace import ::vc::fossil::import::cvs::state + namespace import ::vc::fossil::import::cvs::integrity + namespace import ::vc::tools::misc::* + namespace import ::vc::tools::trouble + namespace import ::vc::tools::log + log register csets + + # Set up the helper singletons + namespace eval rev { + namespace import ::vc::fossil::import::cvs::state + namespace import ::vc::fossil::import::cvs::integrity + namespace import ::vc::tools::log + } + namespace eval sym::tag { + namespace import ::vc::fossil::import::cvs::state + namespace import ::vc::fossil::import::cvs::integrity + namespace import ::vc::tools::log + } + namespace eval sym::branch { + namespace import ::vc::fossil::import::cvs::state + namespace import ::vc::fossil::import::cvs::integrity + namespace import ::vc::tools::log + } + } +} + +# # ## ### ##### ######## ############# ##################### +## Ready + +package provide vc::fossil::import::cvs::project::rev 1.0 +return diff --git a/tools/cvs2fossil/lib/c2f_prevlink.tcl b/tools/cvs2fossil/lib/c2f_prevlink.tcl new file mode 100644 index 0000000..53be78c --- /dev/null +++ b/tools/cvs2fossil/lib/c2f_prevlink.tcl @@ -0,0 +1,240 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Helper class for the pass 6 cycle breaker. Each instance refers to +## three changesets A, B, and C, with A a predecessor of B, and B +## predecessor of C, and the whole part of a dependency cycle. + +## Instances analyse the file level dependencies which gave rise to +## the changeset dependencies of A, B, and C, with the results used by +## the cycle breaker algorithm to find a good location where to at +## least weaken and at best fully break the cycle. + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime. +package require snit ; # OO system. +package require vc::tools::misc ; # Text formatting +package require vc::tools::trouble ; # Error reporting. +package require vc::tools::log ; # User feedback. +package require vc::fossil::import::cvs::state ; # State storage. +package require vc::fossil::import::cvs::integrity ; # State integrity checks. +package require vc::fossil::import::cvs::project::rev ; # Project level changesets + +# # ## ### ##### ######## ############# ##################### +## + +snit::type ::vc::fossil::import::cvs::project::revlink { + # # ## ### ##### ######## ############# + ## Public API + + constructor {prev cset next} { + set myprev $prev + set mycset $cset + set mynext $next + + # We perform the bulk of the analysis during construction. The + # file revisions held by the changeset CSET can be sorted into + # four categories. + + # 1. Revisions whose predecessors are not in PREV, nor are + # their successors found in NEXT. These revisions do not + # count, as they did not induce any of the two dependencies + # under consideration. They can be ignored. + + # 2. Revisions which have predecessors in PREV and successors + # in NEXT. They are called 'passthrough' in cvs2svn. They + # induce both dependencies under consideration and are thus + # critical in the creation of the cycle. As such they are + # also unbreakable :( + + # 3. Revisions which have predecessor in PREVE, but no + # successors in NEXT. As such they induced the incoming + # dependency, but not the outgoing one. + + # 4. Revisions which have no predecessors in PREVE, but their + # successors are in NEXT. As such they induced the outgoing + # dependency, but not the incoming one. + + # If we have no passthrough revisions then splitting the + # changeset between categories 3 and 4, with category 1 going + # wherever, will break the cycle. If category 2 revisions are + # present we can still perform the split, this will however + # not break the cycle, only weaken it. + + # NOTE: This is the only remaining user of 'nextmap'. Look + # into the possibility of performing the relevant counting + # within the database. + + array set csetprevmap [Invert [$myprev nextmap]] + array set csetnextmap [$mycset nextmap] + + set prevrev [$myprev items] + set nextrev [$mynext items] + + foreach item [$mycset items] { + set rt [RT $item] + incr mycount($rt) + lappend mycategory($rt) $item + } + return + } + + # Result is TRUE if and only breaking myset will do some good. + method breakable {} { expr {$mycount(prev) || $mycount(next)} } + method passcount {} { return $mycount(pass) } + + method linkstomove {} { + # Return the number of revisions that would be moved should we + # split the changeset. + + set n [min2 $mycount(prev) $mycount(next)] + if {$n > 0 } { return $n } + return [max2 $mycount(prev) $mycount(next)] + } + + method betterthan {other} { + set sbreak [$self breakable] + set obreak [$other breakable] + + if {$sbreak && !$obreak} { return 1 } ; # self is better. + if {!$sbreak && $obreak} { return 0 } ; # self is worse. + + # Equality. Look at the counters. + # - Whichever has the lesser number of passthrough revisions + # is better, as more can be split off, weakening the cycle + # more. + # - Whichever has less links to move is better. + + set opass [$other passcount] + if {$mycount(pass) < $opass} { return 1 } ; # self is better. + if {$mycount(pass) > $opass} { return 0 } ; # self is worse. + + set smove [$self linkstomove] + set omove [$other linkstomove] + + if {$smove < $omove} { return 1 } ; # self is better. + + return 0 ; # Self is worse or equal, i.e. not better. + } + + method break {} { + integrity assert {[$self breakable]} {Changeset [$mycset str] is not breakable.} + + # One thing to choose when splitting CSET is where the + # revision in categories 1 and 2 (none and passthrough + # respectively) are moved to. This is done using the counters. + + if {!$mycount(prev)} { + # Nothing in category 3 => 1,2 go there, 4 the other. + set mycategory(prev) [concat $mycategory(none) $mycategory(pass)] + } elseif {!$mycount(next)} { + # Nothing in category 4 => 1,2 go there, 3 the other. + set mycategory(next) [concat $mycategory(none) $mycategory(pass)] + } elseif {$mycount(prev) < $mycount(next)} { + # Less predecessors than successors => 1,2 go to the + # sucessors. + set mycategory(next) [concat $mycategory(next) $mycategory(none) \ + $mycategory(pass)] + } else { + # Less successors than predecessors => 1,2 go to the + # predecessors. + set mycategory(next) [concat $mycategory(next) $mycategory(none) \ + $mycategory(pass)] + } + + # We now have the revisions for the two fragments to be in the + # (prev|next) elements of mycategory. + + return [project::rev split $mycset $mycategory(prev) $mycategory(next)] + } + + # # ## ### ##### ######## ############# + ## State + + variable myprev {} ; # Reference to predecessor changeset in the link. + variable mycset {} ; # Reference to the main changeset of the link. + variable mynext {} ; # Reference to the successor changeset in the link. + + # Counters for the revision categories. + variable mycount -array { + none 0 + prev 0 + next 0 + pass 0 + } + # Lists of revisions for the various categories + variable mycategory -array { + none {} + prev {} + next {} + pass {} + } + + # # ## ### ##### ######## ############# + ## Internal methods + + proc RT {r} { + upvar 1 csetprevmap csetprevmap csetnextmap csetnextmap prevrev prevrev nextrev nextrev + + set inc [expr {[info exists csetprevmap($r)] + ? [struct::set size [struct::set intersect $csetprevmap($r) $prevrev]] + : 0}] + set out [expr {[info exists csetnextmap($r)] + ? [struct::set size [struct::set intersect $csetnextmap($r) $nextrev]] + : 0}] + + if {$inc && $out} { return pass } + if {$inc} { return prev } + if {$out} { return next } + return none + } + + proc Invert {dict} { + array set tmp {} + foreach {k values} $dict { + foreach v $values { lappend tmp($v) $k } + } + return [array get tmp] + } + + # # ## ### ##### ######## ############# + ## Configuration + + pragma -hastypeinfo no ; # no type introspection + pragma -hasinfo no ; # no object introspection + pragma -simpledispatch yes ; # simple fast dispatch + + # # ## ### ##### ######## ############# +} + +namespace eval ::vc::fossil::import::cvs::project { + namespace export revlink + namespace eval revlink { + namespace import ::vc::fossil::import::cvs::state + namespace import ::vc::fossil::import::cvs::integrity + namespace import ::vc::tools::misc::* + namespace import ::vc::tools::trouble + namespace eval project { + namespace import ::vc::fossil::import::cvs::project::rev + } + namespace import ::vc::tools::log + log register csets + } +} + +# # ## ### ##### ######## ############# ##################### +## Ready + +package provide vc::fossil::import::cvs::project::revlink 1.0 +return diff --git a/tools/cvs2fossil/lib/c2f_project.tcl b/tools/cvs2fossil/lib/c2f_project.tcl new file mode 100644 index 0000000..88f515f --- /dev/null +++ b/tools/cvs2fossil/lib/c2f_project.tcl @@ -0,0 +1,233 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007-2008 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Project, part of a CVS repository. Multiple instances are possible. + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime. +package require snit ; # OO system. +package require vc::fossil::import::cvs::file ; # CVS archive file. +package require vc::fossil::import::cvs::state ; # State storage. +package require vc::fossil::import::cvs::project::rev ; # Changesets. +package require vc::fossil::import::cvs::project::sym ; # Per project symbols. +package require vc::fossil::import::cvs::project::trunk ; # Per project trunk, main lod +package require vc::tools::log ; # User feedback +package require struct::list ; # Advanced list operations.. + +# # ## ### ##### ######## ############# ##################### +## + +snit::type ::vc::fossil::import::cvs::project { + # # ## ### ##### ######## ############# + ## Public API + + constructor {path r} { + set mybase $path + set myrepository $r + set mytrunk [trunk %AUTO% $self] + set mysymbol([$mytrunk name]) $mytrunk + return + } + + method base {} { return $mybase } + method trunk {} { return $mytrunk } + + method fullpath {} { return [$myrepository base?]/$mybase } + + method printbase {} { + if {$mybase eq ""} {return <Repository>} + return $mybase + } + + method id {} { return $myid } + method setid {id} { set myid $id ; return } + + method addfile {rcs usr executable {fid {}}} { + set myfiles($rcs) [list $usr $executable $fid] + return + } + + method filenames {} { + return [lsort -dict [array names myfiles]] + } + + method files {} { + return [TheFiles] + } + + delegate method defauthor to myrepository + delegate method defcmessage to myrepository + delegate method trunkonly to myrepository + delegate method commitmessageof to myrepository + + method defmeta {bid aid cid} { + return [$myrepository defmeta $myid $bid $aid $cid] + } + + method getsymbol {name} { + if {![info exists mysymbol($name)]} { + set mysymbol($name) \ + [sym %AUTO% $name [$myrepository defsymbol $myid $name] $self] + } + return $mysymbol($name) + } + + method hassymbol {name} { + return [info exists mysymbol($name)] + } + + method purgeghostsymbols {} { + set changes 1 + while {$changes} { + set changes 0 + foreach {name symbol} [array get mysymbol] { + if {![$symbol isghost]} continue + log write 3 project "$mybase: Deleting ghost symbol '$name'" + $symbol destroy + unset mysymbol($name) + set changes 1 + } + } + return + } + + method determinesymboltypes {} { + foreach {name symbol} [array get mysymbol] { + $symbol determinetype + } + return + } + + # pass I persistence + method persist {} { + TheFiles ; # Force id assignment. + + state transaction { + # Project data first. Required so that we have its id + # ready for the files. + + state run { + INSERT INTO project (pid, name) + VALUES (NULL, $mybase); + } + set myid [state id] + + # Then all files, with proper backreference to their + # project. + + foreach rcs [lsort -dict [array names myfiles]] { + struct::list assign $myfiles($rcs) usr executable _fid_ + state run { + INSERT INTO file (fid, pid, name, visible, exec) + VALUES (NULL, $myid, $rcs, $usr, $executable); + } + $myfmap($rcs) setid [state id] + } + } + return + } + + # pass II persistence + method persistrev {} { + # Note: The per file information (incl. revisions and symbols) + # has already been saved and dropped. This was done + # immediately after processing it, i.e. as part of the main + # segment of the pass, to keep out use of memory under + # control. + # + # The repository level information has been saved as well too, + # just before saving the projects started. So now we just have + # to save the remaining project level parts to fix the + # left-over dangling references, which are the symbols. + + state transaction { + foreach {name symbol} [array get mysymbol] { + $symbol persistrev + } + } + return + } + + method changesetsinorder {} { + return [rev inorder $myid] + } + + delegate method getmeta to myrepository + + # # ## ### ##### ######## ############# + ## State + + variable mybase {} ; # Project directory. + variable myid {} ; # Project id in the persistent state. + variable mytrunk {} ; # Reference to the main line of + # development for the project. + variable myfiles -array {} ; # Maps the rcs archive paths to + # their user-visible files. + variable myfobj {} ; # File objects for the rcs archives + variable myfmap -array {} ; # Map rcs archive to their object. + variable myrepository {} ; # Repository the project belongs to. + variable mysymbol -array {} ; # Map symbol names to project-level + # symbol objects. + + # # ## ### ##### ######## ############# + ## Internal methods + + proc TheFiles {} { + upvar 1 myfiles myfiles myfobj myfobj self self myfmap myfmap + if {![llength $myfobj]} { + set myfobj [EmptyFiles myfiles] + } + return $myfobj + } + + proc EmptyFiles {fv} { + upvar 1 $fv myfiles self self myfmap myfmap + set res {} + foreach rcs [lsort -dict [array names myfiles]] { + struct::list assign $myfiles($rcs) f executable fid + set file [file %AUTO% $fid $rcs $f $executable $self] + lappend res $file + set myfmap($rcs) $file + } + return $res + } + + # # ## ### ##### ######## ############# + ## Configuration + + pragma -hastypeinfo no ; # no type introspection + pragma -hasinfo no ; # no object introspection + pragma -hastypemethods no ; # type is not relevant. + + # # ## ### ##### ######## ############# +} + +namespace eval ::vc::fossil::import::cvs { + namespace export project + namespace eval project { + namespace import ::vc::tools::log + namespace import ::vc::fossil::import::cvs::file + namespace import ::vc::fossil::import::cvs::state + # Import not required, already a child namespace. + # namespace import ::vc::fossil::import::cvs::project::sym + # Import not required, already a child namespace. + # namespace import ::vc::fossil::import::cvs::project::rev + } +} + +# # ## ### ##### ######## ############# ##################### +## Ready + +package provide vc::fossil::import::cvs::project 1.0 +return diff --git a/tools/cvs2fossil/lib/c2f_prtopsort.tcl b/tools/cvs2fossil/lib/c2f_prtopsort.tcl new file mode 100644 index 0000000..d001216 --- /dev/null +++ b/tools/cvs2fossil/lib/c2f_prtopsort.tcl @@ -0,0 +1,159 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Pass VIII. This pass goes over the set of revision based changesets +## and sorts them topologically. It assumes that there are no cycles +## which could impede it, any having been broken by the previous pass, +## and aborts if that condition doesn't hold. + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime. +package require snit ; # OO system. +package require struct::list ; # Higher order list operations. +package require vc::tools::log ; # User feedback. +package require vc::fossil::import::cvs::cyclebreaker ; # Breaking dependency cycles. +package require vc::fossil::import::cvs::state ; # State storage. +package require vc::fossil::import::cvs::project::rev ; # Project level changesets + +# # ## ### ##### ######## ############# ##################### +## Register the pass with the management + +vc::fossil::import::cvs::pass define \ + RevTopologicalSort \ + {Topologically Sort Revision ChangeSets} \ + ::vc::fossil::import::cvs::pass::rtopsort + +# # ## ### ##### ######## ############# ##################### +## + +snit::type ::vc::fossil::import::cvs::pass::rtopsort { + # # ## ### ##### ######## ############# + ## Public API + + typemethod setup {} { + # Define the names and structure of the persistent state of + # this pass. + + state use revision + state use symbol + state use changeset + state use csitem + state use cstype + state use cssuccessor + + state extend csorder { + -- Commit order of the revision changesets based on their + -- dependencies + + cid INTEGER NOT NULL REFERENCES changeset, + pos INTEGER NOT NULL, + UNIQUE (cid), + UNIQUE (pos) + } + return + } + + typemethod load {} { + # Pass manager interface. Executed to load data computed by + # this pass into memory when this pass is skipped instead of + # executed. + + state use changeset + project::rev loadcounter + return + } + + typemethod run {} { + # Pass manager interface. Executed to perform the + # functionality of the pass. + + set len [string length [project::rev num]] + set myatfmt %${len}s + incr len 12 + set mycsfmt %${len}s + + cyclebreaker savecmd [myproc SaveOrder] + + state transaction { + cyclebreaker run tsort-rev [myproc Changesets] + } + return + } + + typemethod discard {} { + # Pass manager interface. Executed for all passes after the + # run passes, to remove all data of this pass from the state, + # as being out of date. + + state discard csorder + return + } + + # # ## ### ##### ######## ############# + ## Internal methods + + proc Changesets {} { + log write 2 breakscycle {Selecting the revision changesets} + return [project::rev rev] + } + + proc SaveOrder {graph at cset} { + ::variable myatfmt + ::variable mycsfmt + + set cid [$cset id] + + log write 4 rtopsort "Changeset @ [format $myatfmt $at]: [format $mycsfmt [$cset str]] '[$cset lod]' <<[FormatTR $graph $cset]>>" + state run { + INSERT INTO csorder (cid, pos) + VALUES ($cid, $at) + } + return + } + + proc FormatTR {graph cset} { + return [join [struct::list map [$graph node set $cset timerange] {clock format}] { -- }] + } + + typevariable myatfmt ; # Format for log output to gain better alignment of the various columns. + typevariable mycsfmt ; # Ditto for the changesets. + + # # ## ### ##### ######## ############# + ## Configuration + + pragma -hasinstances no ; # singleton + pragma -hastypeinfo no ; # no introspection + pragma -hastypedestroy no ; # immortal + + # # ## ### ##### ######## ############# +} + +namespace eval ::vc::fossil::import::cvs::pass { + namespace export rtopsort + namespace eval rtopsort { + namespace import ::vc::fossil::import::cvs::cyclebreaker + namespace import ::vc::fossil::import::cvs::state + namespace eval project { + namespace import ::vc::fossil::import::cvs::project::rev + } + namespace import ::vc::tools::log + log register rtopsort + } +} + +# # ## ### ##### ######## ############# ##################### +## Ready + +package provide vc::fossil::import::cvs::pass::rtopsort 1.0 +return diff --git a/tools/cvs2fossil/lib/c2f_psym.tcl b/tools/cvs2fossil/lib/c2f_psym.tcl new file mode 100644 index 0000000..1f8397f --- /dev/null +++ b/tools/cvs2fossil/lib/c2f_psym.tcl @@ -0,0 +1,422 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007-2008 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Symbols (Tags, Branches) per project. + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime. +package require snit ; # OO system. +package require vc::tools::trouble ; # Error reporting. +package require vc::tools::log ; # User feedback. +package require vc::tools::misc ; # Text formatting. +package require vc::fossil::import::cvs::state ; # State storage. +package require struct::set ; # Set handling. + +# # ## ### ##### ######## ############# ##################### +## + +snit::type ::vc::fossil::import::cvs::project::sym { + # # ## ### ##### ######## ############# + ## Public API + + constructor {name id project} { + set myname $name + set myid $id + set myproject $project + + # Count total number of symbols. + incr mynum + return + } + + method name {} { return $myname } + method id {} { return $myid } + + method istrunk {} { return 0 } + + method parent {} { + return [$myproject getsymbol [state run { + SELECT S.name + FROM preferedparent P, symbol S + WHERE P.sid = $myid + AND S.sid = P.pid + }]] + } + + # # ## ### ##### ######## ############# + ## Symbol type + + method determinetype {} { + # This is done by a fixed heuristics, with guidance by the + # user in edge-cases. Contrary to cvs2svn which uses a big + # honking streagy class and rule objects. Keep it simple, we + # can expand later when we actually need all the complexity + # for configurability. + + # The following guidelines are applied: + # - Is usage unambigous ? + # - Was there ever a commit on the symbol ? + # - More used as tag, or more used as branch ? + # - At last, what has the user told us about it ? + # - Fail + + foreach rule { + UserConfig + Unambigous + HasCommits + VoteCounts + } { + set chosen [$self $rule] + if {$chosen eq $myundef} continue + $self MarkAs $rule $chosen + return + } + + # None of the above was able to decide which type to assign to + # the symbol. This is a fatal error preventing the execution + # of the passes after 'CollateSymbols'. + + incr myrulecount(Undecided_) + trouble fatal "Unable to decide how to convert symbol '$myname'" + return + } + + method markthetrunk {} { $self MarkAs IsTheTrunk $mybranch ; return } + + # # ## ### ##### ######## ############# + ## Symbol statistics + + method defcounts {tc bc cc} { + set mybranchcount $tc + set mytagcount $bc + set mycommitcount $cc + return + } + + method countasbranch {} { incr mybranchcount ; return } + method countastag {} { incr mytagcount ; return } + method countacommit {} { incr mycommitcount ; return } + + method blockedby {symbol} { + # Remember the symbol as preventing the removal of this + # symbol. Ot is a tag or branch that spawned from a revision + # on this symbol. + + struct::set include myblockers $symbol + return + } + + method possibleparent {symbol} { + log write 9 symbol "Possible parent ($myname) = [$symbol name]" + + if {[info exists mypparent($symbol)]} { + incr mypparent($symbol) + } else { + set mypparent($symbol) 1 + } + return + } + + method isghost {} { + # Checks if this symbol (as line of development) never + # existed. + + if {$mycommitcount > 0} { return 0 } + if {[llength $myblockers]} { return 0 } + if {[array size mypparent] > 0} { return 0 } + + return 1 + } + + # # ## ### ##### ######## ############# + + method persistrev {} { + set pid [$myproject id] + + state transaction { + state run { + INSERT INTO symbol ( sid, pid, name, type, tag_count, branch_count, commit_count) + VALUES ($myid, $pid, $myname, $myundef, $mytagcount, $mybranchcount, $mycommitcount); + } + foreach symbol $myblockers { + set bid [$symbol id] + state run { + INSERT INTO blocker (sid, bid) + VALUES ($myid, $bid); + } + } + foreach {symbol count} [array get mypparent] { + set pid [$symbol id] + state run { + INSERT INTO parent (sid, pid, n) + VALUES ($myid, $pid, $count); + } + } + } + return + } + + # # ## ### ##### ######## ############# + ## State + + variable myproject {} ; # Reference to the project object + # containing the symbol. + variable myname {} ; # The symbol's name + variable myid {} ; # Repository wide numeric id of the + # symbol. This implicitly encodes the + # project as well. + + variable mybranchcount 0 ; # Count how many uses as branch. + variable mytagcount 0 ; # Count how many uses as tag. + variable mycommitcount 0 ; # Count how many files did a commit on the symbol. + + variable myblockers {} ; # List (Set) of the symbols which block + # the exclusion of this symbol. + + variable mypparent -array {} ; # Maps from symbols to the number + # of files in which it could have + # been a parent of this symbol. + + variable mytype {} ; # The type chosen for the symbol to use in + # the conversion. + + # # ## ### ##### ######## ############# + + typemethod exclude {pattern} { + # Store the pattern in memory for use by the code doing type + # determination. + + lappend myexcludepattern [ProcessPattern $pattern exclusion] + return + } + + typemethod forcetag {pattern} { + # Store the pattern in memory for use by the code doing type + # determination. + + lappend myforcepattern [ProcessPattern $pattern force-tag] $mytag + return + } + + typemethod forcebranch {pattern} { + # Store the pattern in memory for use by the code doing type + # determination. + + lappend myforcepattern [ProcessPattern $pattern force-branch] $mybranch + return + } + + proc ProcessPattern {pattern label} { + if {[string match *:*:* $pattern]} { + # Bad syntax for the pattern, using multiple colons. + + trouble fatal "Bad $label pattern '$pattern'" + } elseif {![string match *:* $pattern]} { + # When only a symbol pattern is specified it applies to + # all projects. + + return [list * $pattern] + } else { + # Both project and symbol patterns are present, we split + # them apart now for storage and easier extraction later. + + return [split $pattern :] + } + } + + typevariable myexcludepattern {} ; # List of patterns specifying + # the symbols to exclude from + # conversion. Tags and/or + # branches. + + typevariable myforcepattern {} ; # List of patterns and types + # specifying which symbols to + # force to specific types. + + typemethod getsymtypes {} { + state foreachrow { + SELECT tid, name FROM symtype; + } { set mysymtype($tid) $name } + return + } + + # Keep the codes below in sync with 'pass::collrev/setup('symtype'). + typevariable myexcluded 0 ; # Code for symbols which are excluded. + typevariable mytag 1 ; # Code for symbols which are tags. + typevariable mybranch 2 ; # Code for symbols which are branches. + typevariable myundef 3 ; # Code for symbols of unknown type. + typevariable mysymtype -array {} ; # Map from type code to label for the log. + + typemethod undef {} { return $myundef } + typemethod excluded {} { return $myexcluded } + typemethod tag {} { return $mytag } + typemethod branch {} { return $mybranch } + + typemethod printrulestatistics {} { + log write 2 symbol "Rule usage statistics:" + + set fmt %[string length $mynum]s + set all 0 + + foreach key [lsort [array names myrulecount]] { + log write 2 symbol "* [format $fmt $myrulecount($key)] $key" + incr all $myrulecount($key) + } + + log write 2 symbol "= [format $fmt $all] total" + return + } + + # Statistics on how often each 'rule' was used to decide on the + # type of a symbol. + typevariable myrulecount -array { + HasCommits 0 + IsTheTrunk 0 + Unambigous 0 + Undecided_ 0 + UserConfig 0 + VoteCounts 0 + } + + typemethod printtypestatistics {} { + log write 2 symbol "Symbol type statistics:" + + set fmt %[string length $mynum]s + set all 0 + + state foreachrow { + SELECT T.name AS stype, + T.plural AS splural, + COUNT (s.sid) AS n + FROM symbol S, symtype T + WHERE S.type = T.tid + GROUP BY T.name + ORDER BY T.name + } { + log write 2 symbol "* [format $fmt $n] [sp $n $stype $splural]" + incr all $n + } + + log write 2 symbol "= [format $fmt $all] total" + return + } + + typevariable mynum 0 + + # # ## ### ##### ######## ############# + ## Internal methods + + method UserConfig {} { + set project [$myproject base] + + # First check if the user requested the exclusion of the + # symbol from conversion. + + foreach ex $myexcludepattern { + struct::list assign $ex pp sp + if {![string match $pp $project]} continue + if {![string match $sp $myname]} continue + return $myexcluded + } + + # If the symbol is not excluded further check if the user + # forces its conversion as a specific type. + + foreach {ex stype} $myforcepattern { + struct::list assign $ex pp sp + if {![string match $pp $project]} continue + if {![string match $sp $myname]} continue + return $stype + } + + # Nothing is forced, have the main system hand the symbol over + # to the regular heuristics. + + return $myundef + } + + method Unambigous {} { + # If a symbol is used unambiguously as a tag/branch, convert + # it as such. + + set istag [expr {$mytagcount > 0}] + set isbranch [expr {$mybranchcount > 0 || $mycommitcount > 0}] + + if {$istag && $isbranch} { return $myundef } + if {$istag} { return $mytag } + if {$isbranch} { return $mybranch } + + # Symbol was not used at all. + return $myundef + } + + method HasCommits {} { + # If there was ever a commit on the symbol, convert it as a + # branch. + + if {$mycommitcount > 0} { return $mybranch } + return $myundef + } + + method VoteCounts {} { + # Convert the symbol based on how often it was used as a + # branch/tag. Whichever happened more often determines how the + # symbol is converted. + + if {$mytagcount > $mybranchcount} { return $mytag } + if {$mytagcount < $mybranchcount} { return $mybranch } + return $myundef + } + + method MarkAs {label chosen} { + log write 3 symbol {\[$label\] Converting symbol '$myname' as $mysymtype($chosen)} + + set mytype $chosen + incr myrulecount($label) + + # This is stored directly into the database. + state run { + UPDATE symbol + SET type = $chosen + WHERE sid = $myid + } + return + } + + # # ## ### ##### ######## ############# + ## Configuration + + pragma -hastypeinfo no ; # no type introspection + pragma -hasinfo no ; # no object introspection + pragma -simpledispatch yes ; # simple fast dispatch + + # # ## ### ##### ######## ############# +} + +namespace eval ::vc::fossil::import::cvs::project { + namespace export sym + namespace eval sym { + namespace import ::vc::fossil::import::cvs::state + namespace import ::vc::tools::misc::* + namespace import ::vc::tools::trouble + namespace import ::vc::tools::log + log register symbol + } +} + +# # ## ### ##### ######## ############# ##################### +## Ready + +package provide vc::fossil::import::cvs::project::sym 1.0 +return diff --git a/tools/cvs2fossil/lib/c2f_ptrunk.tcl b/tools/cvs2fossil/lib/c2f_ptrunk.tcl new file mode 100644 index 0000000..eb9e837 --- /dev/null +++ b/tools/cvs2fossil/lib/c2f_ptrunk.tcl @@ -0,0 +1,89 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007-2008 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Trunk, the special main line of development in a project. + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime. +package require snit ; # OO system. + +# # ## ### ##### ######## ############# ##################### +## + +snit::type ::vc::fossil::import::cvs::project::trunk { + # # ## ### ##### ######## ############# + ## Public API + + constructor {project} { + set mysymbol [$project getsymbol $myname] + set myid [$mysymbol id] + return + } + + destructor { + $mysymbol destroy + } + + method name {} { return $myname } + method id {} { return $myid } + method istrunk {} { return 1 } + method symbol {} { return $self } + method parent {} { return $self } + + method forceid {id} { set myid $id ; return } + + method defcounts {tc bc cc} {} + + method countasbranch {} {} + method countastag {} {} + method countacommit {} {} + + method blockedby {symbol} {} + method possibleparent {symbol} {} + + method isghost {} { return 0 } + + delegate method persistrev to mysymbol + + method determinetype {} { $mysymbol markthetrunk } + + # # ## ### ##### ######## ############# + ## State + + typevariable myname :trunk: ; # Name shared by all trunk symbols. + variable myid {} ; # The trunk's symbol id. + variable mysymbol {} ; # The symbol underneath the trunk. + + # # ## ### ##### ######## ############# + ## Internal methods + + # # ## ### ##### ######## ############# + ## Configuration + + pragma -hastypeinfo no ; # no type introspection + pragma -hasinfo no ; # no object introspection + pragma -hastypemethods no ; # type is not relevant. + + # # ## ### ##### ######## ############# +} + +namespace eval ::vc::fossil::import::cvs::project { + namespace export trunk +} + +# # ## ### ##### ######## ############# ##################### +## Ready + +package provide vc::fossil::import::cvs::project::trunk 1.0 +return diff --git a/tools/cvs2fossil/lib/c2f_repository.tcl b/tools/cvs2fossil/lib/c2f_repository.tcl new file mode 100644 index 0000000..ada5926 --- /dev/null +++ b/tools/cvs2fossil/lib/c2f_repository.tcl @@ -0,0 +1,502 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007-2008 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Repository manager. Keeps projects and their files around. + +package provide vc::fossil::import::cvs::repository 1.0 + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime. +package require snit ; # OO system. +package require vc::tools::trouble ; # Error reporting. +package require vc::tools::log ; # User feedback. +package require vc::tools::misc ; # Text formatting. +package require vc::tools::id ; # Indexing and id generation. +package require vc::fossil::import::cvs::project ; # CVS projects. +package require vc::fossil::import::cvs::state ; # State storage. +package require struct::list ; # List operations. +package require fileutil ; # File operations. + +# # ## ### ##### ######## ############# ##################### +## + +snit::type ::vc::fossil::import::cvs::repository { + # # ## ### ##### ######## ############# + ## Public API + + typemethod base {path} { + # Could be checked, easier to defer to the overall validation. + set mybase $path + return + } + + typemethod add {path} { + # Most things cannot be checked immediately, as the base is + # not known while projects are added. We can and do check for + # uniqueness. We accept multiple occurences of a name, and + # treat them as a single project. + + if {[lsearch -exact $myprojpaths $path] >= 0} return + lappend myprojpaths $path + return + } + + typemethod trunkonly! {} { set mytrunkonly 1 ; return } + typemethod trunkonly {} { return $mytrunkonly } + + typemethod projects {} { + return [TheProjects] + } + + typemethod base? {} { return $mybase } + + typemethod validate {} { + if {![IsRepositoryBase $mybase msg]} { + trouble fatal $msg + # Without a good base directory checking any projects is + # wasted time, so we leave now. + return + } + foreach pp $myprojpaths { + if {![IsProjectBase $mybase/$pp $mybase/CVSROOT msg]} { + trouble fatal $msg + } + } + return + } + + typemethod defauthor {a} { $myauthor put $a } + typemethod defcmessage {cm} { $mycmsg put $cm } + typemethod defsymbol {pid name} { $mysymbol put [list $pid $name] } + typemethod defmeta {pid bid aid cid} { $mymeta put [list $pid $bid $aid $cid] } + + typemethod commitmessageof {mid} { + struct::list assign [$mymeta keyof $mid] pid bid aid cid + return [$mycmsg keyof $cid] + } + + typemethod getmeta {mid} { + struct::list assign [$mymeta keyof $mid] pid bid aid cid + return [list \ + $myprojmap($pid) \ + [$mysymbol keyof $bid] \ + [$myauthor keyof $aid] \ + [$mycmsg keyof $cid]] + } + + # pass I results + typemethod printstatistics {} { + set prlist [TheProjects] + set npr [llength $prlist] + + log write 2 repository "Statistics: Scanned [nsp $npr project]" + + if {$npr > 1} { + set bmax [max [struct::list map $prlist [myproc .BaseLength]]] + incr bmax 2 + set bfmt %-${bmax}s + + set nmax [max [struct::list map $prlist [myproc .NFileLength]]] + set nfmt %${nmax}s + } else { + set bfmt %s + set nfmt %s + } + + set keep {} + foreach p $prlist { + set nfiles [llength [$p filenames]] + set line "Statistics: Project [format $bfmt \"[$p printbase]\"] : [format $nfmt $nfiles] [sp $nfiles file]" + if {$nfiles < 1} { + append line ", dropped" + } else { + lappend keep $p + } + log write 2 repository $line + } + + if {![llength $keep]} { + trouble warn "Dropped all projects" + } elseif {$npr == [llength $keep]} { + log write 2 repository "Keeping all projects" + } else { + log write 2 repository "Keeping [nsp [llength $keep] project]" + trouble warn "Dropped [nsp [expr {$npr - [llength $keep]}] {empty project}]" + } + + # Keep reduced set of projects. + set projects $keep + return + } + + # pass I persistence + typemethod persist {} { + ::variable myprojmap + state transaction { + foreach p [TheProjects] { + $p persist + set myprojmap([$p id]) $p + } + } + return + } + + typemethod load {} { + state transaction { + state foreachrow { + SELECT pid, name FROM project ; + } { + set project [project %AUTO% $name $type] + + lappend myprojpaths $name + lappend myprojects $project + set myprojmap($pid) $project + $project setid $pid + } + state foreachrow { + SELECT fid, pid, name, visible, exec FROM file ; + } { + $myprojmap($pid) addfile $name $visible $exec $fid + } + } + return + } + + # pass II results + typemethod printrevstatistics {} { + log write 2 repository "Revision statistics" + # number of revisions, symbols, repository wide, and per project ... + + set rcount [state one { SELECT COUNT (*) FROM revision }] + set tcount [state one { SELECT COUNT (*) FROM tag }] + set bcount [state one { SELECT COUNT (*) FROM branch }] + set scount [state one { SELECT COUNT (*) FROM symbol }] + set acount [state one { SELECT COUNT (*) FROM author }] + set ccount [state one { SELECT COUNT (*) FROM cmessage }] + set fmt %[string length [max [list $rcount $tcount $bcount $scount $acount $ccount]]]s + + log write 2 repository "Statistics: [format $fmt $rcount] [sp $rcount revision]" + log write 2 repository "Statistics: [format $fmt $tcount] [sp $tcount tag]" + log write 2 repository "Statistics: [format $fmt $bcount] [sp $bcount branch branches]" + log write 2 repository "Statistics: [format $fmt $scount] [sp $scount symbol]" + log write 2 repository "Statistics: [format $fmt $acount] [sp $acount author]" + log write 2 repository "Statistics: [format $fmt $ccount] [sp $ccount {log message}]" + + set prlist [TheProjects] + set npr [llength $prlist] + + if {$npr > 1} { + set bmax [max [struct::list map $prlist [myproc .BaseLength]]] + incr bmax 2 + set bfmt %-${bmax}s + } else { + set bfmt %s + } + + foreach p $prlist { + set pid [$p id] + set prefix "Project [format $bfmt \"[$p printbase]\"]" + regsub -all {[^ ]} $prefix { } blanks + set sep " : " + + set rcount [state one { SELECT COUNT (*) FROM revision R, file F WHERE R.fid = F.fid AND F.pid = $pid }] + set tcount [state one { SELECT COUNT (*) FROM tag T, file F WHERE T.fid = F.fid AND F.pid = $pid }] + set bcount [state one { SELECT COUNT (*) FROM branch B, file F WHERE B.fid = F.fid AND F.pid = $pid }] + set scount [state one { SELECT COUNT (*) FROM symbol WHERE pid = $pid }] + set acount [state one { SELECT COUNT (*) FROM author WHERE aid IN (SELECT DISTINCT aid FROM meta WHERE pid = $pid) }] + set ccount [state one { SELECT COUNT (*) FROM cmessage WHERE cid IN (SELECT DISTINCT cid FROM meta WHERE pid = $pid) }] + + log write 2 repository "Statistics: $prefix$sep[format $fmt $rcount] [sp $rcount revision]" + log write 2 repository "Statistics: $blanks$sep[format $fmt $tcount] [sp $tcount tag]" + log write 2 repository "Statistics: $blanks$sep[format $fmt $bcount] [sp $bcount branch branches]" + log write 2 repository "Statistics: $blanks$sep[format $fmt $scount] [sp $scount symbol]" + log write 2 repository "Statistics: $blanks$sep[format $fmt $acount] [sp $acount author]" + log write 2 repository "Statistics: $blanks$sep[format $fmt $ccount] [sp $ccount {log message}]" + } + return + } + + # pass II persistence + typemethod persistrev {} { + state transaction { + SaveAuthors + SaveCommitMessages + # TODO: Save symbols of all projects (before the revisions + # in the projects, as they are referenced by the meta + # tuples) + SaveMeta + foreach p [TheProjects] { $p persistrev } + } + return + } + + typemethod loadsymbols {} { + state transaction { + # We load the symbol ids at large to have the mapping + # right from the beginning. + + state foreachrow { + SELECT sid, pid, name, tag_count AS tc, branch_count AS bc, commit_count AS cc + FROM symbol + } { + $mysymbol map $sid [list $pid $name] + set project $myprojmap($pid) + + set force [$project hassymbol $name] + set symbol [$project getsymbol $name] + + # Forcing happens only for the trunks. + if {$force} { $symbol forceid $sid } + + # Set the loaded counts. + $symbol defcounts $tc $bc $cc + + # Note: The type is neither retrieved nor set, for + # this is used to load the pass II data, which means + # that everything is 'undefined' at this point anyway. + + # future: $symbol load (blockers, and parents) + } + + # Beyond the symbols we also load the author, commit log, + # and meta information. + + state foreachrow { + SELECT aid, name AS aname FROM author + } { + $myauthor map $aid $aname + } + state foreachrow { + SELECT cid, text FROM cmessage + } { + $mycmsg map $cid $text + } + state foreachrow { + SELECT mid, pid, bid, aid, cid FROM meta + } { + $mymeta map $mid [list $pid $bid $aid $cid] + } + } + return + } + + typemethod determinesymboltypes {} { + foreach project [TheProjects] { + $project determinesymboltypes + } + return + } + + typemethod projectof {pid} { + return $myprojmap($pid) + } + + + # pass IV+ results + typemethod printcsetstatistics {} { + log write 2 repository "Changeset statistics" + # number of revisions, symbols, repository wide, and per project ... + + set ccount [state one { SELECT COUNT (*) FROM changeset }] + set rcount [state one { SELECT COUNT (*) FROM changeset WHERE type = 0 }] + set tcount [state one { SELECT COUNT (*) FROM changeset WHERE type = 1 }] + set bcount [state one { SELECT COUNT (*) FROM changeset WHERE type = 2 }] + set fmt %[string length [max [list $ccount $rcount $tcount $bcount]]]s + + log write 2 repository "Statistics: [format $fmt $ccount] [sp $ccount changeset]" + log write 2 repository "Statistics: [format $fmt $rcount] [sp $rcount {revision changeset}]" + log write 2 repository "Statistics: [format $fmt $tcount] [sp $tcount {tag symbol changeset}]" + log write 2 repository "Statistics: [format $fmt $bcount] [sp $bcount {branch symbol changeset}]" + + set prlist [TheProjects] + set npr [llength $prlist] + + if {$npr > 1} { + set bmax [max [struct::list map $prlist [myproc .BaseLength]]] + incr bmax 2 + set bfmt %-${bmax}s + } else { + set bfmt %s + } + + foreach p $prlist { + set pid [$p id] + set prefix "Project [format $bfmt \"[$p printbase]\"]" + regsub -all {[^ ]} $prefix { } blanks + set sep " : " + + set ccount [state one { SELECT COUNT (*) FROM changeset WHERE pid = $pid }] + set rcount [state one { SELECT COUNT (*) FROM changeset WHERE pid = $pid AND type = 0 }] + set tcount [state one { SELECT COUNT (*) FROM changeset WHERE pid = $pid AND type = 1 }] + set bcount [state one { SELECT COUNT (*) FROM changeset WHERE pid = $pid AND type = 2 }] + + log write 2 repository "Statistics: $prefix$sep[format $fmt $ccount] [sp $ccount changeset]" + log write 2 repository "Statistics: $blanks$sep[format $fmt $rcount] [sp $rcount {revision changeset}]" + log write 2 repository "Statistics: $blanks$sep[format $fmt $tcount] [sp $tcount {tag symbol changeset}]" + log write 2 repository "Statistics: $blanks$sep[format $fmt $bcount] [sp $bcount {branch symbol changeset}]" + } + return + } + + # # ## ### ##### ######## ############# + ## State + + typevariable mybase {} ; # Base path to CVS repository. + typevariable myprojpaths {} ; # List of paths to all declared + # projects, relative to mybase. + typevariable myprojects {} ; # List of objects for all + # declared projects. + typevariable myprojmap -array {} ; # Map from project ids to their + # objects. + typevariable myauthor {} ; # Names of all authors found, + # maps to their ids. + typevariable mycmsg {} ; # All commit messages found, + # maps to their ids. + typevariable mymeta {} ; # Maps all meta data tuples + # (project, branch, author, + # cmessage) to their ids. + typevariable mysymbol {} ; # Map symbols identified by + # project and name to their + # id. This information is not + # saved directly. + typevariable mytrunkonly 0 ; # Boolean flag. Set by option + # processing when the user + # requested a trunk-only import + + # # ## ### ##### ######## ############# + ## Internal methods + + typeconstructor { + set myauthor [vc::tools::id %AUTO%] + set mycmsg [vc::tools::id %AUTO%] + set mymeta [vc::tools::id %AUTO%] + set mysymbol [vc::tools::id %AUTO%] + return + } + + proc .BaseLength {p} { + return [string length [$p printbase]] + } + + proc .NFileLength {p} { + return [string length [llength [$p filenames]]] + } + + proc IsRepositoryBase {path mv} { + ::variable mybase + upvar 1 $mv msg + if {![fileutil::test $mybase edr msg {CVS Repository}]} {return 0} + if {![fileutil::test $mybase/CVSROOT edr msg {CVS Admin Directory}]} {return 0} + return 1 + } + + proc IsProjectBase {path admin mv} { + upvar 1 $mv msg + if {![fileutil::test $path edr msg Project]} {return 0} + if { + ($path eq $admin) || + [string match $admin/* $path] + } { + set msg "Administrative subdirectory $path cannot be a project" + return 0 + } + return 1 + } + + proc TheProjects {} { + upvar 1 type type + ::variable myprojects + ::variable myprojpaths + + if {![llength $myprojects]} { + set myprojects [EmptyProjects $myprojpaths] + } + return $myprojects + } + + proc EmptyProjects {projpaths} { + ::variable mybase + upvar 1 type type + set res {} + if {[llength $projpaths]} { + foreach pp $projpaths { + lappend res [project %AUTO% $pp $type] + } + } else { + # Base is the single project. + lappend res [project %AUTO% "" $type] + } + return $res + } + + proc SaveAuthors {} { + ::variable myauthor + foreach {name aid} [$myauthor get] { + state run { + INSERT INTO author ( aid, name) + VALUES ($aid, $name); + } + } + return + } + + proc SaveCommitMessages {} { + ::variable mycmsg + foreach {text cid} [$mycmsg get] { + state run { + INSERT INTO cmessage ( cid, text) + VALUES ($cid, $text); + } + } + return + } + + proc SaveMeta {} { + ::variable mymeta + foreach {key mid} [$mymeta get] { + struct::list assign $key pid bid aid cid + state run { + INSERT INTO meta ( mid, pid, bid, aid, cid) + VALUES ($mid, $pid, $bid, $aid, $cid); + } + } + return + } + + # # ## ### ##### ######## ############# + ## Configuration + + pragma -hasinstances no ; # singleton + pragma -hastypeinfo no ; # no introspection + pragma -hastypedestroy no ; # immortal + + # # ## ### ##### ######## ############# +} + +namespace eval ::vc::fossil::import::cvs { + namespace export repository + namespace eval repository { + namespace import ::vc::fossil::import::cvs::project + namespace import ::vc::fossil::import::cvs::state + namespace import ::vc::tools::misc::* + namespace import ::vc::tools::id + namespace import ::vc::tools::trouble + namespace import ::vc::tools::log + log register repository + } +} + +# # ## ### ##### ######## ############# ##################### +## Ready +return diff --git a/tools/cvs2fossil/lib/c2f_ristate.tcl b/tools/cvs2fossil/lib/c2f_ristate.tcl new file mode 100644 index 0000000..39defd8 --- /dev/null +++ b/tools/cvs2fossil/lib/c2f_ristate.tcl @@ -0,0 +1,113 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2008 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Track the state of revision import. Essentially maps lines of +## developments to their workspace state. + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime. +package require snit ; # OO system. +package require struct::list ; # List assignment +package require vc::fossil::import::cvs::wsstate ; # Workspace state +package require vc::fossil::import::cvs::integrity ; # State integrity checks. +package require vc::tools::log ; # User feedback. +package require vc::tools::trouble ; # Error reporting. + +# # ## ### ##### ######## ############# ##################### +## + +snit::type ::vc::fossil::import::cvs::ristate { + # # ## ### ##### ######## ############# + ## Public API + + constructor {} { + # Start with an empty state + return + } + + method new {lod {parentlod {}}} { + # Create a workspace for a line of development (LOD). If a + # parent LOD is specified let the new workspace inherit the + # current state of the parent. + + log write 8 ristate {Open workspace for LOD "$lod"} + + integrity assert { + ![info exists mystate($lod)] + } {Trying to override existing state for lod "$lod"} + + set wss [wsstate ${selfns}::%AUTO% $lod] + set mystate($lod) $wss + + if {$parentlod ne ""} { + log write 8 ristate {Inheriting from workspace for LOD "$parentlod"} + + integrity assert { + [info exists mystate($parentlod)] + } {Trying to inherit from undefined lod "$parentlod"} + + set pwss $mystate($parentlod) + + $wss defstate [$pwss getstate] + $wss defid [$pwss getid] + $wss defparent $pwss + } + + return $wss + } + + method get {lod} { return $mystate($lod) } + method has {lod} { return [info exists mystate($lod)] } + + method names {} { return [array names mystate] } + + method dup {dst _from_ src} { + log write 8 ristate {Duplicate workspace for LOD "$dst" from "$src"} + set mystate($dst) $mystate($src) + return + } + + # # ## ### ##### ######## ############# + ## State + + variable mystate -array {} ; # Map from lines of development + # (identified by name) to their + # workspace. + + # # ## ### ##### ######## ############# + ## Configuration + + pragma -hastypeinfo no ; # no type introspection + pragma -hasinfo no ; # no object introspection + pragma -hastypemethods no ; # type is not relevant. + + # # ## ### ##### ######## ############# +} + +namespace eval ::vc::fossil::import::cvs { + namespace export ristate + namespace eval ristate { + namespace import ::vc::fossil::import::cvs::wsstate + namespace import ::vc::fossil::import::cvs::integrity + namespace import ::vc::tools::trouble + namespace import ::vc::tools::log + log register ristate + } +} + +# # ## ### ##### ######## ############# ##################### +## Ready + +package provide vc::fossil::import::cvs::ristate 1.0 +return diff --git a/tools/cvs2fossil/lib/c2f_state.tcl b/tools/cvs2fossil/lib/c2f_state.tcl new file mode 100644 index 0000000..6be8d97 --- /dev/null +++ b/tools/cvs2fossil/lib/c2f_state.tcl @@ -0,0 +1,231 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## State manager. Maintains the sqlite database used by all the other +## parts of the system, especially the passes and their support code, +## to persist and restore their state across invokations. + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime. +package require snit ; # OO system. +package require fileutil ; # File operations. +package require sqlite3 ; # Database access. +package require vc::tools::trouble ; # Error reporting. +package require vc::tools::log ; # User feedback. + +# # ## ### ##### ######## ############# ##################### +## + +snit::type ::vc::fossil::import::cvs::state { + # # ## ### ##### ######## ############# + ## Public API + + typemethod usedb {path} { + # Immediate validation. There are are two possibilities to + # consider. The path exists or it doesn't. + + # In the first case it has to be a readable and writable file, + # and it has to be a proper sqlite database. Further checks + # regarding the required tables will be done later, by the + # passes, during their setup. + + # In the second case we have to be able to create the file, + # and check that. This is done by opening it, sqlite will then + # try to create it, and may fail. + + if {[::file exists $path]} { + if {![fileutil::test $path frw msg {cvs2fossil state}]} { + trouble fatal $msg + return + } + } + + if {[catch { + sqlite3 ${type}::TEMP $path + ${type}::TEMP eval {PRAGMA synchronous=OFF;} + } res]} { + trouble fatal $res + return + } + + # A previously defined state database is closed before + # committing to the new definition. We do not store the path + # itself, this ensures that the file is _not_ cleaned up after + # a run. + + set mystate ${type}::STATE + set mypath {} + + catch { $mystate close } + rename ${type}::TEMP $mystate + + log write 2 state "is $path" + return + } + + typemethod setup {} { + # If, and only if no state database was defined by the user + # then it is now the time to create our own using a tempfile. + + if {$mystate ne ""} return + + set mypath [fileutil::tempfile cvs2fossil_state_] + set mystate ${type}::STATE + sqlite3 $mystate $mypath + $mystate eval {PRAGMA synchronous=OFF;} + + log write 2 state "using $mypath" + return + } + + typemethod release {} { + log write 2 state release + ${type}::STATE close + if {$mypath eq ""} return + ::file delete $mypath + return + } + + # Declare a table needed for the storing of persistent state, and + # its structure. A possibly previously existing definition is + # dropped. To be used when a table is needed and not assumed to + # exist from previous passes. + + typemethod extend {name definition {indices {}}} { + log write 5 state "extend $name" + Save "extend $name ================================" + + $mystate transaction { + catch { $mystate eval "DROP TABLE $name" } + $mystate eval "CREATE TABLE $name ( $definition )" + + set id 0 + foreach columns $indices { + log write 5 state "index $name$id" + + $mystate eval "CREATE INDEX ${name}$id ON ${name} ( [join $columns ,] )" + incr id + } + } + return + } + + # Declare that a table is needed for reading from and/or storing + # to persistent state, and is assumed to already exist. A missing + # table is an internal error causing an immediate exit. + + typemethod use {name} { + log write 5 state "use $name" + Save "use $name ===================================" + + set found [llength [$mystate eval { + SELECT name + FROM sqlite_master + WHERE type = 'table' + AND name = $name + ; + }]] + + # No assert, would cause cycle in package dependencies + if {$found} return + trouble internal "The required table \"$name\" is not defined." + # Not reached + return + } + + typemethod discard {name} { + # Method for a user to remove outdated information from the + # persistent state, table by table. + + log write 5 state "discard $name" + + $mystate transaction { + catch { $mystate eval "DROP TABLE $name" } + } + return + } + + typemethod run {args} { + Save $args + return [uplevel 1 [linsert $args 0 $mystate eval]] + } + + typemethod foreachrow {sql script} { + Save $sql + uplevel 1 [list $mystate eval $sql $script] + return + } + + typemethod one {args} { + Save $args + return [uplevel 1 [linsert $args 0 $mystate onecolumn]] + } + + typemethod transaction {script} { + return [uplevel 1 [list $mystate transaction $script]] + } + + typemethod id {} { + return [$mystate last_insert_rowid] + } + + typemethod savequeriesto {path} { + set mysavepath $path + return + } + + # # ## ### ##### ######## ############# + + proc Save {text} { + ::variable mysavepath + if {$mysavepath eq ""} return + fileutil::appendToFile $mysavepath $text\n\n + return + } + + # # ## ### ##### ######## ############# + ## State + + typevariable mystate {} ; # Sqlite database (command) holding the converter state. + typevariable mypath {} ; # Path to the database, for cleanup of a temp database. + typevariable mysavepath {} ; # Path where to save queries for introspection. + + # # ## ### ##### ######## ############# + ## Internal methods + + + # # ## ### ##### ######## ############# + ## Configuration + + pragma -hasinstances no ; # singleton + pragma -hastypeinfo no ; # no introspection + pragma -hastypedestroy no ; # immortal + + # # ## ### ##### ######## ############# +} + +namespace eval ::vc::fossil::import::cvs { + namespace export state + namespace eval state { + namespace import ::vc::tools::trouble + namespace import ::vc::tools::log + log register state + } +} + +# # ## ### ##### ######## ############# ##################### +## Ready + +package provide vc::fossil::import::cvs::state 1.0 +return diff --git a/tools/cvs2fossil/lib/c2f_wsstate.tcl b/tools/cvs2fossil/lib/c2f_wsstate.tcl new file mode 100644 index 0000000..502d479 --- /dev/null +++ b/tools/cvs2fossil/lib/c2f_wsstate.tcl @@ -0,0 +1,136 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2008 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Track the state of a cvs workspace as changesets are committed to +## it. Nothing actually happens in the filesystem, this is completely +## virtual. + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime. +package require snit ; # OO system. +package require struct::list ; # List assignment +package require vc::tools::log ; # User feedback. + +# # ## ### ##### ######## ############# ##################### +## + +snit::type ::vc::fossil::import::cvs::wsstate { + # # ## ### ##### ######## ############# + ## Public API + + constructor {lod} { + # Start with an empty state + set myname $lod + set myticks 0 + set myparent {} + return + } + + method name {} { return $myname } + method ticks {} { return $myticks } + + method add {oprevisioninfo} { + # oprevisioninfo = list (rid path label op ...) /quadruples + + # Overwrite all changed files (identified by path) with the + # new revisions. This keeps all unchanged files. Files marked + # as dead are removed. + + foreach {rid path label rop} $oprevisioninfo { + log write 5 wss {$myop($rop) $label} + + if {$rop < 0} { + if {[catch { + unset mystate($path) + }]} { + log write 0 wss "Removed path \"$path\" is not known to the workspace" + } + } else { + set mystate($path) [list $rid $label] + } + } + + incr myticks + return + } + + method get {} { + set res {} + foreach path [lsort -dict [array names mystate]] { + struct::list assign $mystate($path) rid label + lappend res $rid $path $label + } + return $res + } + + method defid {id} { + set myid $id + return + } + + method getid {} { return $myid } + + method defstate {s} { array set mystate $s ; return } + method getstate {} { return [array get mystate] } + + method parent {} { return $myparent } + method defparent {parent} { + set myparent $parent + return + } + + # # ## ### ##### ######## ############# + ## State + + variable myname {} ; # Name of the LOD the workspace is + # for. + variable myid {} ; # Record id of the fossil manifest + # associated with the current state. + variable mystate -array {} ; # Map from paths to the recordid of + # the file revision behind it, and + # the associated label for logging. + variable myticks 0 ; # Number of 'add' operations + # performed on the state. + variable myparent {} ; # Reference to the parent workspace. + + typevariable myop -array { + -1 REM + 0 --- + 1 ADD + 2 CHG + } + + # # ## ### ##### ######## ############# + ## Configuration + + pragma -hastypeinfo no ; # no type introspection + pragma -hasinfo no ; # no object introspection + pragma -hastypemethods no ; # type is not relevant. + + # # ## ### ##### ######## ############# +} + +namespace eval ::vc::fossil::import::cvs { + namespace export wsstate + namespace eval wsstate { + namespace import ::vc::tools::log + log register wss + } +} + +# # ## ### ##### ######## ############# ##################### +## Ready + +package provide vc::fossil::import::cvs::wsstate 1.0 +return diff --git a/tools/cvs2fossil/lib/cvs2fossil.tcl b/tools/cvs2fossil/lib/cvs2fossil.tcl new file mode 100644 index 0000000..7bca492 --- /dev/null +++ b/tools/cvs2fossil/lib/cvs2fossil.tcl @@ -0,0 +1,89 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Main package of the cvs conversion/import facility. Loads the +## required pieces and controls their interaction. + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime. +package require snit ; # OO system + +# # ## ### ##### ######## ############# ##################### +## Passes. The order in which the various passes are loaded is +## important. It is the same order in which they will +## register, and then be run in. + +package require vc::fossil::import::cvs::pass::collar ; # Coll'ect Ar'chives. +package require vc::fossil::import::cvs::pass::collrev ; # Coll'ect Rev'isions. +package require vc::fossil::import::cvs::pass::collsym ; # Coll'ate Sym'bols +package require vc::fossil::import::cvs::pass::filtersym ; # Filter' Sym'bols + +# Note: cvs2svn's SortRevisionSummaryPass and SortSymbolSummaryPass +# are not implemented by us. They are irrelevant due to our use +# of a relational database proper for the persistent state, +# allowing us to sort the data on the fly as we need it. + +package require vc::fossil::import::cvs::pass::initcsets ; # Init'ialize C'hange'Sets +package require vc::fossil::import::cvs::pass::csetdeps ; # C'hange'Set Dep'endencies +package require vc::fossil::import::cvs::pass::breakrcycle ; # Break' R'evision Cycle's +package require vc::fossil::import::cvs::pass::rtopsort ; # R'evision Top'ological Sort' +package require vc::fossil::import::cvs::pass::breakscycle ; # Break' S'ymbol Cycle's +package require vc::fossil::import::cvs::pass::breakacycle ; # Break' A'll Cycle's +package require vc::fossil::import::cvs::pass::atopsort ; # A'll Top'ological Sort' +package require vc::fossil::import::cvs::pass::importfiles ; # Import' Files +package require vc::fossil::import::cvs::pass::importcsets ; # Import' Changesets +package require vc::fossil::import::cvs::pass::importfinal ; # Import' Finalization + +# # ## ### ##### ######## ############# ##################### +## Support for passes etc. + +package require vc::fossil::import::cvs::option ; # Cmd line parsing & database +package require vc::fossil::import::cvs::pass ; # Pass management +package require vc::tools::log ; # User feedback + +# # ## ### ##### ######## ############# ##################### +## + +snit::type ::vc::fossil::import::cvs { + # # ## ### ##### ######## ############# + ## Public API, Methods + + typemethod run {arguments} { + # Run a series of passes over the cvs repository to extract, + # filter, and order its historical information. Which passes + # are actually run is determined through the specified options + # and their defaults. + + option process $arguments + pass run + + vc::tools::log write 0 cvs2fossil Done + return + } + + # # ## ### ##### ######## ############# + ## Configuration + + pragma -hasinstances no ; # singleton + pragma -hastypeinfo no ; # no introspection + pragma -hastypedestroy no ; # immortal + + # # ## ### ##### ######## ############# +} + +# # ## ### ##### ######## ############# ##################### +## Ready + +package provide vc::fossil::import::cvs 1.0 +return diff --git a/tools/cvs2fossil/lib/dot.tcl b/tools/cvs2fossil/lib/dot.tcl new file mode 100644 index 0000000..0081c08 --- /dev/null +++ b/tools/cvs2fossil/lib/dot.tcl @@ -0,0 +1,108 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Utility package, export graph data to dot format for formatting +## with neato et. all + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime +package require snit ; # OO system. +package require fileutil ; # Helper commands. + +# # ## ### ##### ######## ############# ##################### +## + +snit::type ::vc::tools::dot { + # # ## ### ##### ######## ############# + ## Public API, Methods + + typemethod format {g name {subgraph {}}} { + lappend lines "digraph \"$name\" \{" + + if {![llength $subgraph]} { + set nodes [$g nodes] + set arcs [$g arcs] + } else { + set nodes $subgraph + set arcs [eval [linsert $subgraph 0 $g arcs -inner]] + } + + foreach n $nodes { + set style [Style $g node $n {label label shape shape fontcolor fontcolor}] + lappend lines "\"$n\" ${style};" + } + foreach a $arcs { + set style [Style $g arc $a {color color}] + lappend lines "\"[$g arc source $a]\" -> \"[$g arc target $a]\" ${style};" + } + + lappend lines "\}" + return [join $lines \n] + } + + typemethod write {g name file {subgraph {}}} { + fileutil::writeFile $file [$type format $g $name $subgraph] + return + } + + typemethod layout {format g name file} { + set f [fileutil::tempfile c2fdot_] + $type write $g $name $f + exec dot -T $format -o $file $f + ::file delete $f + return + } + + # # ## ### ##### ######## ############# + ## Internal, state + + proc Style {graph x y dict} { + set sep " " + set head " \[" + set tail "" + set style "" + foreach {gattr key} $dict { + if {![$graph $x keyexists $y $key]} continue + append style "$head$sep${gattr}=\"[$graph $x get $y $key]\"" + set sep ", " + set head "" + set tail " \]" + } + + append style ${tail} + return $style + } + + # # ## ### ##### ######## ############# + ## Internal, helper methods (formatting, dispatch) + + # # ## ### ##### ######## ############# + ## Configuration + + pragma -hasinstances no ; # singleton + pragma -hastypeinfo no ; # no introspection + pragma -hastypedestroy no ; # immortal + + # # ## ### ##### ######## ############# +} + +namespace eval ::vc::tools { + namespace export dot +} + +# ----------------------------------------------------------------------------- +# Ready + +package provide vc::tools::dot 1.0 +return diff --git a/tools/cvs2fossil/lib/id.tcl b/tools/cvs2fossil/lib/id.tcl new file mode 100644 index 0000000..331f9a6 --- /dev/null +++ b/tools/cvs2fossil/lib/id.tcl @@ -0,0 +1,64 @@ +# # ## ### ##### ######## ############# + +## A simple class for handling an in-memory index mapping from +## arbitrary strings to a small numeric id. Can be queried in reverse +## too, returning the string for the id. + +## Id's are starting from 1. + +# # ## ### ##### ######## ############# +## Requirements. + +package require Tcl ; # Runtime. +package require snit ; # OO runtime. + +# # ## ### ##### ######## ############# +## Implementation. + +snit::type ::vc::tools::id { + # # ## ### ##### ######## ############# + + constructor {} {} + + # # ## ### ##### ######## ############# + ## Public API. + ## - Put data into the index, incl. query for id of key. + ## - Lookup data for id. + + method put {key} { + if {[info exists mydata($key)]} { return $mydata($key) } + incr mycounter + + set mydata($key) $mycounter + set myinvert($mycounter) $key + + return $mycounter + } + + # Explicitly load the database with a mapping. + method map {id key} { + set mydata($key) $id + set myinvert($id) $key + } + + method keyof {id} { return $myinvert($id) } + method get {} { return [array get mydata] } + + # # ## ### ##### ######## ############# + ## Internal. State. + + variable mydata -array {} ; # Map data -> id + variable myinvert -array {} ; # Map id -> data + variable mycounter 0 ; # Counter for id generation. + + # # ## ### ##### ######## ############# +} + +namespace eval ::vc::tools { + namespace export id +} + +# # ## ### ##### ######## ############# +## Ready. + +package provide vc::tools::id 1.0 diff --git a/tools/cvs2fossil/lib/log.tcl b/tools/cvs2fossil/lib/log.tcl new file mode 100644 index 0000000..4eef620 --- /dev/null +++ b/tools/cvs2fossil/lib/log.tcl @@ -0,0 +1,184 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007-2008 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Utility package, basic user feedback + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime +package require snit ; # OO system. +package require vc::tools::mem ; # Memory tracking. + +# # ## ### ##### ######## ############# ##################### +## + +snit::type ::vc::tools::log { + # # ## ### ##### ######## ############# + ## Public API, Methods + + # Write the message 'text' to log, for the named 'system'. The + # message is written if and only if the message verbosity is less + # or equal the chosen verbosity. A message of verbosity 0 cannot + # be blocked. + + typemethod write {verbosity system text} { + if {$verbosity > $myloglevel} return + uplevel #0 [linsert $mylogcmd end write [System $system] \ + [uplevel 1 [list ::subst $text]]] + return + } + + # Similar to write, especially in the handling of the verbosity, + # to drive progress displays. It signals that for some long + # running operation we are at tick 'n' of at most 'max' ticks. An + # empty 'max' indicates an infinite progress display. + + typemethod progress {verbosity system n max} { + if {!$myprogress} return + if {$verbosity > $myloglevel} return + uplevel #0 [linsert $mylogcmd end progress [System $system] $n $max] + return + } + + typemethod visible? {verbosity} { + return [expr {$verbosity <= $myloglevel}] + } + + # # ## ### ##### ######## ############# + # Public API, Administrative methods + + # Set verbosity to the chosen 'level'. Only messages with a level + # less or equal to this one will be shown. + + typemethod verbosity {level} { + if {$level < 1} {set level 0} + set myloglevel $level + return + } + + typemethod verbose {} { + incr myloglevel + return + } + + typemethod noprogress {} { + set myprogress 0 + return + } + + typemethod quiet {} { + if {$myloglevel < 1} return + incr myloglevel -1 + return + } + + # Query the currently set verbosity. + + typemethod verbosity? {} { + return $myloglevel + } + + # Set the log callback handling the actual output of messages going + # through the package. + + typemethod command {cmdprefix} { + variable mylogcmd $cmdprefix + return + } + + # Register a system name, to enable tabular formatting. This is + # done by setting up a format specifier with a proper width. This + # is handled in the generation command, before the output callback + # is invoked. + + typemethod register {name} { + set nlen [string length $name] + if {$nlen < $mysyslen} return + set mysyslen $nlen + set mysysfmt %-${mysyslen}s + return + } + + # # ## ### ##### ######## ############# + ## Internal, state + + typevariable myloglevel 2 ; # Some verbosity, not too much + typevariable mylogcmd ::vc::tools::log::OUT ; # Standard output to stdout. + typevariable mysysfmt %s ; # Non-tabular formatting. + typevariable mysyslen 0 ; # Ditto. + typevariable myprogress 1 ; # Progress output is standard. + + # # ## ### ##### ######## ############# + ## Internal, helper methods (formatting, dispatch) + + proc System {s} { + ::variable mysysfmt + return [format $mysysfmt $s] + } + + # # ## ### ##### ######## ############# + ## Standard output callback, module internal + + # Dispatch to the handlers of the possible operations. + + proc OUT {op args} { + eval [linsert $args 0 ::vc::tools::log::OUT/$op] + return + } + + # Write handler. Each message is a line. + + proc OUT/write {system text} { + set m [mlog] + regsub -all {[^ ]} $m { } b + puts "$m$system [join [split $text \n] "\n$b$system "]" + mlimit + return + } + + # Progress handler. Uses \r to return to the beginning of the + # current line without advancing. + + proc OUT/progress {system n max} { + if {$max eq {}} { + puts -nonewline "$system $n\r" + } else { + puts -nonewline "$system [format %[string length $max]s $n]/$max\r" + } + flush stdout + return + } + + # # ## ### ##### ######## ############# + ## Configuration + + pragma -hasinstances no ; # singleton + pragma -hastypeinfo no ; # no introspection + pragma -hastypedestroy no ; # immortal + + # # ## ### ##### ######## ############# +} + +namespace eval ::vc::tools { + namespace export log + namespace eval log { + namespace import ::vc::tools::mem::mlog + namespace import ::vc::tools::mem::mlimit + } +} + +# ----------------------------------------------------------------------------- +# Ready + +package provide vc::tools::log 1.0 +return diff --git a/tools/cvs2fossil/lib/mem.tcl b/tools/cvs2fossil/lib/mem.tcl new file mode 100644 index 0000000..dae4d18 --- /dev/null +++ b/tools/cvs2fossil/lib/mem.tcl @@ -0,0 +1,139 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2008 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Utilities for memory tracking + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime +package require struct::list ; # List assignment + +# # ## ### ##### ######## ############# ##################### +## + +namespace eval ::vc::tools::mem { + # # ## ### ##### ######## ############# + ## Public API, Methods + + if {[llength [info commands memory]]} { + proc minfo {} { + # memory info reduced to the set of relevant numbers in the output + struct::list assign [split [memory info] \n] tm tf cpa cba mpa mba + struct::list assign $tm _ _ tm + struct::list assign $tf _ _ tf + struct::list assign $cpa _ _ _ cpa + struct::list assign $cba _ _ _ cba + struct::list assign $mpa _ _ _ mpa + struct::list assign $mba _ _ _ mba + return [list $tm $tf $cpa $cba $mpa $mba] + } + } else { + proc minfo {} {return {0 0 0 0 0 0}} + } + + proc mlog {} { + variable track ; if {!$track} { return {} } + + variable lcba + variable lmba + variable mid + + struct::list assign [minfo] _ _ _ cba _ mba + + set dc [expr $cba - $lcba] ; set lcba $cba + set dm [expr $mba - $lmba] ; set lmba $mba + + # projection: 1 2 3 4 5 6 7 6 8 10 + return "[F [incr mid]] | [F $cba] | [F $dc] | [F $mba] | [F $dm] |=| " + } + + proc mark {} { + variable track ; if {!$track} return + variable mid + variable lcba + variable lmark + set dm [expr {$lcba - $lmark}] + puts "[F $mid] | [F $lcba] | [F $dm] | [X %] | [X %] |@| [X %]" + set lmark $lcba + return + } + + proc F {n} { format %10d $n } + proc X {c} { string repeat $c 10 } + + proc mlimit {} { + variable track ; if {!$track} return ; # No checks if there is no memory tracking + variable limit ; if {!$limit} return ; # No checks if there is no memory limit set + + struct::list assign [minfo] _ _ _ cba _ _ + + # Nothing to do if we are still under the limit + if {$cba <= $limit} return + + # Notify user and kill the importer + puts "" + puts "\tMemory limit breached: $cba > $limit" + puts "" + exit 1 + } + + proc setlimit {thelimit} { + # Activate memory tracking, and set the limit. The specified + # limit is taken relative to the amount of memory allocated at + # the time of the call. + + variable limit + struct::list assign [minfo] _ _ _ cba _ _ + set limit [expr $cba + $thelimit] + + track + return + } + + proc notrack {} { + variable track 0 + return + } + + proc track {} { + variable track 1 + return + } + + # # ## ### ##### ######## ############# + + variable track 0 ; # Boolean flag. If set this module will track + # memory, inserting the relevant information + # whenever the application logs something. + variable limit 0 ; # The maximum amount of memory allowed to the + # application. This module will abort when + # 'current bytes allocated' goes over this + # value. + + variable lcba 0 ; # Last 'current bytes allocated' (cba) + variable lmba 0 ; # Last 'maximum bytes allocated' (mba) + variable mid 0 ; # Memory id, abstract time + variable lmark 0 ; # + + # # ## ### ##### ######## ############# +} + +namespace eval ::vc::tools::mem { + namespace export minfo mlog track notrack mlimit setlimit mark +} + +# ----------------------------------------------------------------------------- +# Ready + +package provide vc::tools::mem 1.0 +return diff --git a/tools/cvs2fossil/lib/misc.tcl b/tools/cvs2fossil/lib/misc.tcl new file mode 100644 index 0000000..aa75ab3 --- /dev/null +++ b/tools/cvs2fossil/lib/misc.tcl @@ -0,0 +1,144 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007-2008 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Utilities for various things: text formatting, max, ... + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime + +# # ## ### ##### ######## ############# ##################### +## + +namespace eval ::vc::tools::misc { + # # ## ### ##### ######## ############# + ## Public API, Methods + + # Choose singular vs plural forms of a word based on a number. + + proc sp {n singular {plural {}}} { + if {$n == 1} {return $singular} + if {$plural eq ""} {set plural ${singular}s} + return $plural + } + + # As above, with the number automatically put in front of the + # string. + + proc nsp {n singular {plural {}}} { + return "$n [sp $n $singular $plural]" + } + + # Find maximum/minimum in a list. + + proc max {list} { + set max -1 + foreach e $list { + if {$e < $max} continue + set max $e + } + return $max + } + + proc min {list} { + set min {} + foreach e $list { + if {$min == {}} { + set min $e + } elseif {$e > $min} continue + set min $e + } + return $min + } + + proc max2 {a b} { + if {$a > $b} { return $a } + return $b + } + + proc min2 {a b} { + if {$a < $b} { return $a } + return $b + } + + proc ldelete {lv item} { + upvar 1 $lv list + set pos [lsearch -exact $list $item] + if {$pos < 0} return + set list [lreplace $list $pos $pos] + return + } + + # Delete item from list by name + + proc striptrailingslash {path} { + # split and rejoin gets rid of a traling / character. + return [eval [linsert [file split $path] 0 ::file join]] + } + + # The windows filesystem is storing file-names case-sensitive, but + # matching is case-insensitive. That is a problem as without + # precaution the two files Attic/X,v and x,v may be mistakenly + # identified as the same file. A similar thing can happen for + # files and directories. To prevent such mistakes we need commands + # which do case-sensitive file matching even on systems which do + # not perform this natively. These are below. + + if {$tcl_platform(platform) eq "windows"} { + # We use glob to get the list of files (with proper case in + # the names) to perform our own, case-sensitive matching. WE + # use 8.5 features where possible, for clarity. + + if {[package vsatisfies [package present Tcl] 8.5]} { + proc fileexists_cs {path} { + set dir [::file dirname $path] + set file [::file tail $path] + return [expr {$file in [glob -nocomplain -tail -directory $dir *]}] + } + + proc fileisdir_cs {path} { + set dir [::file dirname $path] + set file [::file tail $path] + return [expr {$file in [glob -nocomplain -types d -tail -directory $dir *]}] + } + } else { + proc fileexists_cs {path} { + set dir [::file dirname $path] + set file [::file tail $path] + return [expr {[lsearch [glob -nocomplain -tail -directory $dir *] $file] >= 0}] + } + + proc fileisdir_cs {path} { + set dir [::file dirname $path] + set file [::file tail $path] + return [expr {[lsearch [glob -nocomplain -types d -tail -directory $dir *] $file] >= 0}] + } + } + } else { + proc fileexists_cs {path} { return [file exists $path] } + proc fileisdir_cs {path} { return [file isdirectory $path] } + } + + # # ## ### ##### ######## ############# +} + +namespace eval ::vc::tools::misc { + namespace export sp nsp max min max2 min2 ldelete + namespace export striptrailingslash fileexists_cs fileisdir_cs +} + +# ----------------------------------------------------------------------------- +# Ready + +package provide vc::tools::misc 1.0 +return diff --git a/tools/cvs2fossil/lib/pkgIndex.tcl b/tools/cvs2fossil/lib/pkgIndex.tcl new file mode 100644 index 0000000..11ebc4b --- /dev/null +++ b/tools/cvs2fossil/lib/pkgIndex.tcl @@ -0,0 +1,47 @@ +# # ## ### ##### ######## ############# ##################### +## Package management. +## Index of the local packages required by cvs2fossil +# # ## ### ##### ######## ############# ##################### +if {![package vsatisfies [package require Tcl] 8.4]} return +package ifneeded vc::fossil::import::cvs 1.0 [list source [file join $dir cvs2fossil.tcl]] +package ifneeded vc::fossil::import::cvs::blobstore 1.0 [list source [file join $dir c2f_blobstore.tcl]] +package ifneeded vc::fossil::import::cvs::file 1.0 [list source [file join $dir c2f_file.tcl]] +package ifneeded vc::fossil::import::cvs::file::rev 1.0 [list source [file join $dir c2f_frev.tcl]] +package ifneeded vc::fossil::import::cvs::file::sym 1.0 [list source [file join $dir c2f_fsym.tcl]] +package ifneeded vc::fossil::import::cvs::file::trunk 1.0 [list source [file join $dir c2f_ftrunk.tcl]] +package ifneeded vc::fossil::import::cvs::fossil 1.0 [list source [file join $dir c2f_fossil.tcl]] +package ifneeded vc::fossil::import::cvs::option 1.0 [list source [file join $dir c2f_option.tcl]] +package ifneeded vc::fossil::import::cvs::integrity 1.0 [list source [file join $dir c2f_integrity.tcl]] +package ifneeded vc::fossil::import::cvs::pass 1.0 [list source [file join $dir c2f_pass.tcl]] +package ifneeded vc::fossil::import::cvs::pass::collar 1.0 [list source [file join $dir c2f_pcollar.tcl]] +package ifneeded vc::fossil::import::cvs::pass::collrev 1.0 [list source [file join $dir c2f_pcollrev.tcl]] +package ifneeded vc::fossil::import::cvs::pass::collsym 1.0 [list source [file join $dir c2f_pcollsym.tcl]] +package ifneeded vc::fossil::import::cvs::pass::filtersym 1.0 [list source [file join $dir c2f_pfiltersym.tcl]] +package ifneeded vc::fossil::import::cvs::pass::initcsets 1.0 [list source [file join $dir c2f_pinitcsets.tcl]] +package ifneeded vc::fossil::import::cvs::pass::csetdeps 1.0 [list source [file join $dir c2f_pcsetdeps.tcl]] +package ifneeded vc::fossil::import::cvs::pass::breakrcycle 1.0 [list source [file join $dir c2f_pbreakrcycle.tcl]] +package ifneeded vc::fossil::import::cvs::pass::rtopsort 1.0 [list source [file join $dir c2f_prtopsort.tcl]] +package ifneeded vc::fossil::import::cvs::pass::breakscycle 1.0 [list source [file join $dir c2f_pbreakscycle.tcl]] +package ifneeded vc::fossil::import::cvs::pass::breakacycle 1.0 [list source [file join $dir c2f_pbreakacycle.tcl]] +package ifneeded vc::fossil::import::cvs::pass::atopsort 1.0 [list source [file join $dir c2f_patopsort.tcl]] +package ifneeded vc::fossil::import::cvs::pass::importfiles 1.0 [list source [file join $dir c2f_pimportfiles.tcl]] +package ifneeded vc::fossil::import::cvs::pass::importcsets 1.0 [list source [file join $dir c2f_pimportcsets.tcl]] +package ifneeded vc::fossil::import::cvs::pass::importfinal 1.0 [list source [file join $dir c2f_pimportfinal.tcl]] +package ifneeded vc::fossil::import::cvs::gtcore 1.0 [list source [file join $dir c2f_gtcore.tcl]] +package ifneeded vc::fossil::import::cvs::cyclebreaker 1.0 [list source [file join $dir c2f_cyclebreaker.tcl]] +package ifneeded vc::fossil::import::cvs::project 1.0 [list source [file join $dir c2f_project.tcl]] +package ifneeded vc::fossil::import::cvs::project::rev 1.0 [list source [file join $dir c2f_prev.tcl]] +package ifneeded vc::fossil::import::cvs::project::revlink 1.0 [list source [file join $dir c2f_prevlink.tcl]] +package ifneeded vc::fossil::import::cvs::project::sym 1.0 [list source [file join $dir c2f_psym.tcl]] +package ifneeded vc::fossil::import::cvs::project::trunk 1.0 [list source [file join $dir c2f_ptrunk.tcl]] +package ifneeded vc::fossil::import::cvs::repository 1.0 [list source [file join $dir c2f_repository.tcl]] +package ifneeded vc::fossil::import::cvs::ristate 1.0 [list source [file join $dir c2f_ristate.tcl]] +package ifneeded vc::fossil::import::cvs::state 1.0 [list source [file join $dir c2f_state.tcl]] +package ifneeded vc::fossil::import::cvs::wsstate 1.0 [list source [file join $dir c2f_wsstate.tcl]] +package ifneeded vc::rcs::parser 1.0 [list source [file join $dir rcsparser.tcl]] +package ifneeded vc::tools::dot 1.0 [list source [file join $dir dot.tcl]] +package ifneeded vc::tools::id 1.0 [list source [file join $dir id.tcl]] +package ifneeded vc::tools::log 1.0 [list source [file join $dir log.tcl]] +package ifneeded vc::tools::misc 1.0 [list source [file join $dir misc.tcl]] +package ifneeded vc::tools::mem 1.0 [list source [file join $dir mem.tcl]] +package ifneeded vc::tools::trouble 1.0 [list source [file join $dir trouble.tcl]] diff --git a/tools/cvs2fossil/lib/rcsparser.tcl b/tools/cvs2fossil/lib/rcsparser.tcl new file mode 100644 index 0000000..d530b36 --- /dev/null +++ b/tools/cvs2fossil/lib/rcsparser.tcl @@ -0,0 +1,485 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +# A tool package, provides a parser for RCS archive files. This parser +# is implemented via recursive descent. It is not only given a file to +# process, but also a 'sink', an object it calls out to at important +# places of the parsing process to either signal an event and/or +# convey gathered information to it. The sink is responsible for the +# actual processing of the data in whatever way it desires. + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime. +package require snit ; # OO system. +package require fileutil ; # File utilities. +package require vc::tools::log ; # User feedback. +package require struct::list ; # Advanced list ops. + +# # ## ### ##### ######## ############# ##################### +## + +snit::type ::vc::rcs::parser { + # # ## ### ##### ######## ############# + ## Public API + + typemethod process {path sink} { + Initialize $path $sink + Call begin + Admin ; Deltas ; Description ; DeltaTexts + Call done + return + } + + # # ## ### ##### ######## ############# + ## Internal methods, recursive descent, syntactical processing + + proc Admin {} { + Head ; PrincipalBranch ; Access ; Symbols + Locks ; Strictness ; FileComment ; Expand + Call admindone + return + } + + # # ## ### ##### ######## ############# + + proc Head {} { + RequiredLiteral head + RequiredNumber -> head + Semicolon + Call sethead $head + return + } + + proc PrincipalBranch {} { + if {![OptionalLiteral branch]} return + RequiredNumber -> branch + Semicolon + Call setprincipalbranch $branch + return + } + + proc Access {} { + RequiredLiteral access ; + Semicolon + return + } + + proc Symbols {} { + RequiredLiteral symbols + while {[Ident -> symbol]} { + if { + *$} $symbol] || + [string match */ $symbol] + } { + Rewind + Bad {symbol name} + } + RequiredNumber -> rev + Call deftag $symbol $rev + } + Semicolon + return + } + + proc Locks {} { + # Not saving locks. + RequiredLiteral locks + while {[Ident -> symbol]} { + RequiredNumber -> l + } + Semicolon + return + } + + proc Strictness {} { + # Not saving strictness + if {![OptionalLiteral strict]} return + Semicolon + return + } + + proc FileComment {} { + if {![OptionalLiteral comment]} return + if {![OptionalString -> c]} return + Semicolon + Call setcomment $c + return + } + + proc Expand {} { + # Not saving expanded keywords + if {![OptionalLiteral expand]} return + if {![OptionalString -> dummy]} return + Semicolon + return + } + + # # ## ### ##### ######## ############# + + proc Deltas {} { + set ok [OptionalNumber -> rev] + while {$ok} { + Date -> d + Author -> a + State -> s + Branches -> b + NextRev -> n + Call def $rev $d $a $s $n $b + + # Check if this is followed by a revision number or the + # literal 'desc'. If neither we consume whatever is there + # until the next semicolon, as it has to be a 'new + # phrase'. Otherwise, for a revision number we loop back + # and consume that revision, and lastly for 'desc' we stop + # completely as this signals the end of the revision tree + # and the beginning of the deltas. + + while {1} { + set ok [OptionalNumber -> rev] + if {$ok} break + + if {[LiteralPeek desc]} { + set ok 0 + break + } + + Anything -> dummy + Semicolon + } + } + Call defdone + return + } + + # # ## ### ##### ######## ############# + + proc Date {_ dv} { + upvar 1 $dv d + RequiredLiteral date + RequiredNumber -> d + Semicolon + + struct::list assign [split $d .] year month day hour min sec + if {$year < 100} {incr year 1900} + set d [clock scan "${year}-${month}-${day} ${hour}:${min}:${sec}"] + return + } + + proc Author {_ av} { + upvar 1 $av a + RequiredLiteral author + Anything -> a + Semicolon + return + } + + proc State {_ sv} { + upvar 1 $sv s + RequiredLiteral state + Anything -> s + Semicolon + return + } + + proc Branches {_ bv} { + upvar 1 $bv b + RequiredLiteral branches + Anything -> b + Semicolon + return + } + + proc NextRev {_ nv} { + upvar 1 $nv n + RequiredLiteral next + Anything -> n + Semicolon + return + } + + # # ## ### ##### ######## ############# + + proc Description {} { + upvar 1 data data res res + RequiredLiteral desc + RequiredString -> d + Call setdesc $d + return + } + + # # ## ### ##### ######## ############# + + proc DeltaTexts {} { + while {[OptionalNumber -> rev]} { + RequiredLiteral log + RequiredString -> cmsg + if {[regexp {[\000-\010\013\014\016-\037]} $cmsg]} { + #Rewind + #Bad "log message for $rev contains at least one control character" + } + + RequiredLiteral text + RequiredStringRange -> delta + Call extend $rev $cmsg $delta + } + return + } + + # # ## ### ##### ######## ############# + ## Internal methods, lexiographical processing + + proc Semicolon {} { + ::variable mydata + ::variable mypos + + set ok [regexp -start $mypos -indices -- {\A\s*;\s*} $mydata match] + if {!$ok} { Expected ';' } + + SkipOver match + return + } + + proc RequiredLiteral {name} { + ::variable mydata + ::variable mypos + + set pattern "\\A\\s*$name\\s*" + set ok [regexp -start $mypos -indices -- $pattern $mydata match] + if {!$ok} { Expected '$name' } + + SkipOver match + return + } + + proc OptionalLiteral {name} { + ::variable mydata + ::variable mypos + + set pattern "\\A\\s*$name\\s*" + set ok [regexp -start $mypos -indices -- $pattern $mydata match] + if {!$ok} { return 0 } + + SkipOver match + return 1 + } + + proc LiteralPeek {name} { + ::variable mydata + ::variable mypos + + set pattern "\\A\\s*$name\\s*" + set ok [regexp -start $mypos -indices -- $pattern $mydata match] + if {!$ok} { return 0 } + + # NO - SkipOver match - Only looking ahead here. + return 1 + } + + proc RequiredNumber {_ v} { + upvar 1 $v value + ::variable mydata + ::variable mypos + + set pattern {\A\s*((\d|\.)+)\s*} + set ok [regexp -start $mypos -indices -- $pattern $mydata match v] + if {!$ok} { Expected id } + + Extract $v -> value + SkipOver match + return + } + + proc OptionalNumber {_ v} { + upvar 1 $v value + ::variable mydata + ::variable mypos + + set pattern {\A\s*((\d|\.)+)\s*} + set ok [regexp -start $mypos -indices -- $pattern $mydata match v] + if {!$ok} { return 0 } + + Extract $v -> value + SkipOver match + return 1 + } + + proc RequiredString {_ v} { + upvar 1 $v value + ::variable mydata + ::variable mypos + + set ok [regexp -start $mypos -indices -- {\A\s*@(([^@]*(@@)*)*)@\s*} $mydata match v] + if {!$ok} { Expected string } + + Extract $v -> value + set value [string map {@@ @} $value] + SkipOver match + return + } + + proc RequiredStringRange {_ v} { + upvar 1 $v value + ::variable mydata + ::variable mypos + + set ok [regexp -start $mypos -indices -- {\A\s*@(([^@]*(@@)*)*)@\s*} $mydata match value] + if {!$ok} { Expected string } + + SkipOver match + return + } + + proc OptionalString {_ v} { + upvar 1 $v value + ::variable mydata + ::variable mypos + + set ok [regexp -start $mypos -indices -- {\A\s*@(([^@]*(@@)*)*)@\s*} $mydata match v] + if {!$ok} { return 0 } + + Extract $v -> value + set value [string map {@@ @} $value] + SkipOver match + return 1 + } + + proc Ident {_ v} { + upvar 1 $v value + ::variable mydata + ::variable mypos + + set ok [regexp -start $mypos -indices -- {\A\s*;\s*} $mydata] + if {$ok} { return 0 } + + set ok [regexp -start $mypos -indices -- {\A\s*([^:]*)\s*:\s*} $mydata match v] + if {!$ok} { return 0 } + + Extract $v -> value + SkipOver match + return 1 + } + + proc Anything {_ v} { + upvar 1 $v value + ::variable mydata + ::variable mypos + + regexp -start $mypos -indices -- {\A\s*([^;]*)\s*} $mydata match v + + Extract $v -> value + SkipOver match + return + } + + # # ## ### ##### ######## ############# + ## Internal methods, input handling + + proc Extract {range _ v} { + upvar 1 $v value + ::variable mydata + struct::list assign $range s e + set value [string range $mydata $s $e] + return + } + + proc SkipOver {mv} { + # Note: The indices are absolute!, not relative to the start + # location. + upvar 1 $mv match + ::variable mypos + ::variable mysize + ::variable mylastpos + + struct::list assign $match s e + #puts "<$s $e> [info level -1]" + + set mylastpos $mypos + set mypos $e + incr mypos + + log progress 2 rcs $mypos $mysize + #puts $mypos/$mysize + return + } + + proc Rewind {} { + ::variable mypos + ::variable mylastpos + + set mypos $mylastpos + return + } + + proc Expected {x} { + ::variable mydata + ::variable mypos + set e $mypos ; incr e 30 + return -code error -errorcode vc::rcs::parser \ + "Expected $x @ '[string range $mydata $mypos $e]...'" + } + + proc Bad {x} { + ::variable mydata + ::variable mypos + set e $mypos ; incr e 30 + return -code error -errorcode vc::rcs::parser \ + "Bad $x @ '[string range $mydata $mypos $e]...'" + } + + # # ## ### ##### ######## ############# + ## Setup, callbacks. + + proc Initialize {path sink} { + ::variable mypos 0 + ::variable mydata [fileutil::cat -translation binary $path] + ::variable mysize [file size $path] + ::variable mysink $sink + return + } + + proc Call {args} { + ::variable mysink + set cmd $mysink + foreach a $args { lappend cmd $a } + eval $cmd + return + } + + # # ## ### ##### ######## ############# + ## Configuration + + typevariable mydata {} ; # Rcs archive contents to process + typevariable mysize 0 ; # Length of contents + typevariable mysink {} ; # Sink to report to + + pragma -hasinstances no ; # singleton + pragma -hastypeinfo no ; # no introspection + pragma -hastypedestroy no ; # immortal + + # # ## ### ##### ######## ############# +} + +namespace eval ::vc::rcs { + namespace export parser + namespace eval parser { + namespace import ::vc::tools::log + log register rcs + } +} + +# # ## ### ##### ######## ############# ##################### +## Ready + +package provide vc::rcs::parser 1.0 +return diff --git a/tools/cvs2fossil/lib/trouble.tcl b/tools/cvs2fossil/lib/trouble.tcl new file mode 100644 index 0000000..dcc2af9 --- /dev/null +++ b/tools/cvs2fossil/lib/trouble.tcl @@ -0,0 +1,124 @@ +## -*- tcl -*- +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Utility package, error reporting on top of the log package. + +# # ## ### ##### ######## ############# ##################### +## Requirements + +package require Tcl 8.4 ; # Required runtime. +package require vc::tools::log ; # Basic log generation. +package require snit ; # OO system. + +# # ## ### ##### ######## ############# ##################### +## + +snit::type ::vc::tools::trouble { + # # ## ### ##### ######## ############# + ## Public API, Methods + + typemethod internal {text} { + foreach line [split $text \n] { $type fatal "INTERNAL ERROR! $line" } + exit 1 + } + + typemethod fatal {text} { + lappend myfatal $text + return + } + + typemethod warn {text} { + lappend mywarn $text + log write 0 trouble $text + return + } + + typemethod info {text} { + lappend myinfo $text + return + } + + typemethod show {} { + foreach m $myinfo { log write 0 "" $m } + foreach m $mywarn { log write 0 warning $m } + foreach m $myfatal { log write 0 fatal $m } + return + } + + typemethod ? {} { + return [expr { + [llength $myinfo] || + [llength $mywarn] || + [llength $myfatal] + }] + } + + typemethod abort? {} { + if { + ![llength $myinfo] && + ![llength $mywarn] && + ![llength $myfatal] + } return + + # Frame the pending messages to make them more clear as the + # cause of the abort. + + set myinfo [linsert $myinfo 0 "" "Encountered problems." ""] + lappend myfatal "Stopped due to problems." + + # We have error messages to print, so stop now. + exit 1 + } + + # # ## ### ##### ######## ############# + ## Internal, state + + typevariable myinfo {} + typevariable mywarn {} + typevariable myfatal {} + + # # ## ### ##### ######## ############# + ## Configuration + + pragma -hasinstances no ; # singleton + pragma -hastypeinfo no ; # no introspection + pragma -hastypedestroy no ; # immortal + + # # ## ### ##### ######## ############# +} + +# # ## ### ##### ######## ############# ##################### +## Internal. Special. Set up a hook into the application exit, to show +## the remembered messages, before passing through the regular command. + +rename ::exit ::vc::tools::trouble::EXIT +proc ::exit {{status 0}} { + ::vc::tools::trouble show + ::vc::tools::trouble::EXIT $status + # Not reached. + return +} + +namespace eval ::vc::tools { + namespace eval trouble {namespace import ::vc::tools::log } + trouble::log register "" + trouble::log register fatal + trouble::log register trouble + trouble::log register warning + namespace export trouble +} + +# # ## ### ##### ######## ############# ##################### +## Ready + +package provide vc::tools::trouble 1.0 +return diff --git a/tools/cvs2fossil/lodtree b/tools/cvs2fossil/lodtree new file mode 100644 index 0000000..c4ee959 --- /dev/null +++ b/tools/cvs2fossil/lodtree @@ -0,0 +1,88 @@ +#!/bin/sh +## -*- tcl -*- \ +exec tclsh "$0" ${1+"$@"} + +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Command line application to extract the tree of branches (lines of +## development) from a state database and show it graphically. The +## code uses GraphViz's 'dot' to do the layouting and conversion into +## an image. + +# # ## ### ##### ######## ############# ##################### +## Requirements, extended package management for local packages. + +lappend auto_path [file join [file dirname [info script]] lib] + +package require Tcl 8.4 ; # Required runtime. +package require struct::graph ; # Graph handling. +package require struct::list ; # Higher order list ops. +package require vc::fossil::import::cvs::state ; # State storage. +package require vc::tools::dot ; # Graph export to DOT. + +namespace import ::vc::fossil::import::cvs::state +namespace import ::vc::tools::dot + +# Process the command line. Get the database to access. + +state use [lindex $argv 0] +state reading symbol +state reading parent + +# Get the data of all symbols in the state as a list for iteration, +# and as array for random access of neighbouring symbols. + +foreach {sid name} [set symbols [state run { SELECT sid, name FROM symbol }]] { + set sym($sid) [list $name] +} +foreach {sid lod} [state run { SELECT sid, lod FROM tag }] { + catch {unset sym($sid)} +} +foreach {sid lod} [state run { SELECT sid, lod FROM branch }] { + lappend sym($sid) $lod $sym($lod) diamond Branch +} + +# Start the graph + +struct::graph dg + +# Convert the symbols into nodes of the graph, and use node attributes +# to highlight various pieces of interest for the dot conversion. +# Label => Symbol name. + +foreach sid [array names sym] { + dg node insert $sid + struct::list assign $sym($sid) name lod lodname shape what + if {$shape eq ""} { set shape circle } + if {$what ne ""} { append what " " } + dg node set $sid label "$what$name" + dg node set $sid shape $shape +} + +# Go through the symbols a second time, now set up the arcs based on +# their parent choices. Use arc attributes to highlight interesting +# things (...). + +foreach sid [array names sym] { + struct::list assign $sym($sid) name lod lodname shape + if {$lod eq ""} continue ; # Root has no parent. + dg arc insert $sid $lod +} + +# Convert the graph to dot, then run the layouter and convert to png, +# at last show the image. + +vc::tools::dot layout png dg SymbolTree st.png +exec display st.png +file delete st.png +exit + diff --git a/tools/cvs2fossil/showg b/tools/cvs2fossil/showg new file mode 100644 index 0000000..e119aa8 --- /dev/null +++ b/tools/cvs2fossil/showg @@ -0,0 +1,6 @@ +#!/bin/sh + +dot -Tpng -o $$.png $1 +display $$.png +rm $$.png +exit diff --git a/tools/cvs2fossil/symboltree b/tools/cvs2fossil/symboltree new file mode 100644 index 0000000..5545473 --- /dev/null +++ b/tools/cvs2fossil/symboltree @@ -0,0 +1,88 @@ +#!/bin/sh +## -*- tcl -*- \ +exec tclsh "$0" ${1+"$@"} + +# # ## ### ##### ######## ############# ##################### +## Copyright (c) 2007 Andreas Kupries. +# +# This software is licensed as described in the file LICENSE, which +# you should have received as part of this distribution. +# +# This software consists of voluntary contributions made by many +# individuals. For exact contribution history, see the revision +# history and logs, available at http://fossil-scm.hwaci.com/fossil +# # ## ### ##### ######## ############# ##################### + +## Command line application to extract the tree of symbols (tags and +## branches) from a state database and show it graphically. The code +## uses GraphViz's 'dot' to do the layouting and conversion into an +## image. + +# # ## ### ##### ######## ############# ##################### +## Requirements, extended package management for local packages. + +lappend auto_path [file join [file dirname [info script]] lib] + +package require Tcl 8.4 ; # Required runtime. +package require struct::graph ; # Graph handling. +package require struct::list ; # Higher order list ops. +package require vc::fossil::import::cvs::state ; # State storage. +package require vc::tools::dot ; # Graph export to DOT. + +namespace import ::vc::fossil::import::cvs::state +namespace import ::vc::tools::dot + +# Process the command line. Get the database to access. + +state use [lindex $argv 0] +state reading symbol +state reading parent + +# Get the data of all symbols in the state as a list for iteration, +# and as array for random access of neighbouring symbols. + +foreach {sid name} [set symbols [state run { SELECT sid, name FROM symbol }]] { + set sym($sid) [list $name] +} +foreach {sid lod} [state run { SELECT sid, lod FROM tag }] { + lappend sym($sid) $lod $sym($lod) box Tag +} +foreach {sid lod} [state run { SELECT sid, lod FROM branch }] { + lappend sym($sid) $lod $sym($lod) diamond Branch +} + +# Start the graph + +struct::graph dg + +# Convert the symbols into nodes of the graph, and use node attributes +# to highlight various pieces of interest for the dot conversion. +# Label => Symbol name. + +foreach sid [array names sym] { + dg node insert $sid + struct::list assign $sym($sid) name lod lodname shape what + if {$shape eq ""} { set shape circle } + if {$what ne ""} { append what " " } + dg node set $sid label "$what$name" + dg node set $sid shape $shape +} + +# Go through the symbols a second time, now set up the arcs based on +# their parent choices. Use arc attributes to highlight interesting +# things (...). + +foreach sid [array names sym] { + struct::list assign $sym($sid) name lod lodname shape + if {$lod eq ""} continue ; # Root has no parent. + dg arc insert $sid $lod +} + +# Convert the graph to dot, then run the layouter and convert to png, +# at last show the image. + +vc::tools::dot layout png dg SymbolTree st.png +exec display st.png +file delete st.png +exit + diff --git a/tools/decode-email.c b/tools/decode-email.c new file mode 100644 index 0000000..8945394 --- /dev/null +++ b/tools/decode-email.c @@ -0,0 +1,163 @@ +/* +** This program reads a raw email file and attempts to decode it into +** a more human-readable format. The following decodings are done: +** +** (1) Header values are prefixed by "| " at the left margin. +** +** (2) Content-Transfer-Encoding is recognized and the content is +** decoded for display. +*/ +#define _GNU_SOURCE +#include <stdio.h> +#include <string.h> +#include <ctype.h> + +#define BINARY 0 +#define BASE64 1 +#define QUOTED 2 + +static int decode_hex(char c){ + if( c>='0' && c<='9' ) return c - '0'; + if( c>='A' && c<='F' ) return c - 'A' + 10; + if( c>='a' && c<='f' ) return c - 'a' + 10; + return -1; +} + +static void convert_file(const char *zFilename, FILE *in){ + int inHdr = 1; + int n; + int nBoundary; + int decodeType = 0; + int textMimetype = 1; + char *zB; + char zBoundary[200]; + char zLine[5000]; + char zOut[5000]; + while( fgets(zLine, sizeof(zLine), in) ){ + if( !inHdr + && zLine[0]=='-' + && zLine[1]=='-' + && strncmp(zLine+2,zBoundary,nBoundary)==0 + ){ + printf("|----------------- end of body section ---------|\n"); + inHdr = 1; + } + if( !inHdr ){ + if( textMimetype && decodeType==BASE64 ){ + int ii, jj, c, x, y; + int bits = 0; + for(ii=jj=0; (c = zLine[ii])!=0; ii++){ + if( c>='A' && c<='Z' ){ + x = c - 'A'; + }else if( c>='a' && c<='z' ){ + x = c - 'a' + 26; + }else if( c>='0' && c<='9' ){ + x = c - '0' + 52; + }else if( c=='+' ){ + x = 62; + }else if( c=='/' ){ + x = 63; + }else if( c=='=' ){ + x = 0; + }else{ + continue; + } + if( bits==0 ){ + y = x; + bits = 6; + }else if( bits==6 ){ + zOut[jj++] = ((y<<2) & 0xfc) | ((x>>4) & 0x03); + y = x & 0xf; + bits = 4; + }else if( bits==4 ){ + zOut[jj++] = ((y<<4) & 0xf0) | ((x>>2) & 0x0f); + y = x & 0x3; + bits = 2; + }else if( bits==2 ){ + zOut[jj++] = ((y<<6) & 0xc0) | (x & 0x3f); + bits = 0; + } + } + zOut[jj] = 0; + printf("%s", zOut); + }else if( textMimetype && decodeType==QUOTED ){ + int ii, jj, c; + for(ii=jj=0; (c = zLine[ii])!=0; ii++){ + if( c=='=' ){ + int x1 = decode_hex(zLine[ii+1]); + int x2 = decode_hex(zLine[ii+2]); + if( x1>=0 && x2>=0 ){ + zOut[jj++] = (x1<<4) | x2; + ii += 2; + }else if( zLine[ii+1]=='\r' && zLine[ii+2]=='\n' ){ + ii += 2; + } + }else{ + zOut[jj++] = c; + } + } + zOut[jj] = 0; + printf("%s", zOut); + }else{ + printf("%s", zLine); + } + continue; + } + n = (int)strlen(zLine); + while( n>0 && isspace(zLine[n-1]) ){ n--; } + zLine[n] = 0; + if( n==0 ){ + inHdr = 0; + printf("|----------------- end of header ---------------|\n"); + continue; + } + printf("| %s\n", zLine); + if( strncasecmp(zLine,"Content-Type:", 13)==0 ){ + textMimetype = strstr(zLine, "text/")!=0; + printf("|** %s content type **|\n", + textMimetype ? "Text" : "Non-text"); + } + if( strncasecmp(zLine,"Content-Transfer-Encoding:", 26)==0 ){ + if( strcasestr(zLine, "base64") ){ + decodeType = BASE64; + }else if( strcasestr(zLine, "quoted-printable") ){ + decodeType = QUOTED; + }else{ + decodeType = BINARY; + } + printf("|** Content encoding %s **|\n", + decodeType==BASE64 ? "BASE64" : + decodeType==QUOTED ? "QUOTED" : "BINARY"); + } + zB = strstr(zLine, "boundary=\""); + if( zB ){ + int kk; + zB += 10; + for(kk=0; zB[kk] && zB[kk]!='"' && kk<sizeof(zBoundary)-1; kk++){ + zBoundary[kk] = zB[kk]; + } + zBoundary[kk] = 0; + nBoundary = kk; + printf("|** boundary [%s] **|\n", zBoundary); + } + } +} + +int main(int argc, char **argv){ + if( argc==1 ){ + convert_file("<stdin>", stdin); + return 0; + }else{ + int i; + for(i=1; i<argc; i++){ + FILE *in = fopen(argv[i], "rb"); + if( in==0 ){ + fprintf(stderr, "cannot open \"%s\"", argv[i]); + }else{ + convert_file(argv[i], in); + fclose(in); + } + } + } + return 0; +} diff --git a/tools/email-monitor.tcl b/tools/email-monitor.tcl new file mode 100755 index 0000000..5af228a --- /dev/null +++ b/tools/email-monitor.tcl @@ -0,0 +1,51 @@ +#!/usr/bin/tcl +# +# Monitor the database file named on the command line for +# incoming email messages. Print the "To:" line of each +# email on standard output as it is received. +# +# It should be relatively easy to modify this scribe to actually +# deliver the emails to a real email transfer agent such as +# Postfix. +# +# For long-term use, set the polling interval to something +# greater than the default 100 milliseconds. Polling once +# every 10 seconds is probably sufficient. +# +set POLLING_INTERVAL 100 ;# milliseconds + +set dbfile [lindex $argv 0] +if {[llength $argv]!=1} { + puts stderr "Usage: $argv0 DBFILE" + exit 1 +} +package require sqlite3 +puts "SQLite version [sqlite3 -version]" +sqlite3 db $dbfile +db timeout 2000 +catch {db eval {PRAGMA journal_mode=WAL}} +db eval { + CREATE TABLE IF NOT EXISTS email( + emailid INTEGER PRIMARY KEY, + msg TXT + ); +} +while {1} { + db transaction immediate { + set n 0 + db eval {SELECT msg FROM email} { + set email ??? + regexp {To: \S*} $msg to + puts "$to ([string length $msg] bytes)" + incr n + } + if {$n>0} { + db eval {DELETE FROM email} + } + # Hold the write lock a little longer in order to exercise + # the SQLITE_BUSY handling logic on the writing inside of + # Fossil. Probably comment-out this line for production use. + after 100 + } + after $POLLING_INTERVAL +} diff --git a/tools/email-sender.tcl b/tools/email-sender.tcl new file mode 100755 index 0000000..cbec22f --- /dev/null +++ b/tools/email-sender.tcl @@ -0,0 +1,65 @@ +#!/usr/bin/tcl +# +# Monitor the database file named by the DBFILE variable +# looking for email messages sent by Fossil. Forward each +# to /usr/sbin/sendmail. +# +set POLLING_INTERVAL 10000 ;# milliseconds +set DBFILE /home/www/data/emailqueue.db +set PIPE "/usr/sbin/sendmail -ti" + +package require sqlite3 +# puts "SQLite version [sqlite3 -version]" +sqlite3 db $DBFILE +db timeout 5000 +catch {db eval {PRAGMA journal_mode=WAL}} +db eval { + CREATE TABLE IF NOT EXISTS email( + emailid INTEGER PRIMARY KEY, + msg TXT + ); + CREATE TABLE IF NOT EXISTS sentlog( + mtime INT, + xto TEXT, + xfrom TEXT, + xsubject TEXT, + xsize INT + ); +} +set ctr 0 +while {1} { + set n 0 + db transaction immediate { + set emailid 0 + db eval {SELECT emailid, msg FROM email LIMIT 1} { + set pipe $PIPE + set to unk + set subject none + set size [string length $msg] + regexp {To:[^\n]*<([^>]+)>} $msg all to + regexp {\nSubject:[ ]*([^\r\n]+)} $msg all subject + set subject [string trim $subject] + if {[regexp {\nFrom:[^\n]*<([^>]+)>} $msg all from]} { + append pipe " -f $from" + } + set out [open |$pipe w] + puts -nonewline $out $msg + flush $out + close $out + incr n + incr ctr + } + if {$n>0} { + db eval {DELETE FROM email WHERE emailid=$emailid} + db eval {INSERT INTO sentlog(mtime,xto,xfrom,xsubject,xsize) + VALUES(unixepoch(),$to,$from,$subject,$size)} + } + } + if {$n==0} { + if {$ctr>100} { + db eval {DELETE FROM sentlog WHERE mtime<unixepoch('now','-30 days')} + set ctr 0 + } + after $POLLING_INTERVAL + } +} diff --git a/tools/emcc.sh.in b/tools/emcc.sh.in new file mode 100644 index 0000000..1264df5 --- /dev/null +++ b/tools/emcc.sh.in @@ -0,0 +1,66 @@ +#!/bin/sh +# ^^^^^^^ Please try to keep this script Bourne-compatible. +######################################################################## +# WARNING: emcc.sh is generated from emcc.sh.in by the configure +# process. Do not edit emcc.sh directly, as it may be deleted or +# overwritten by the configure script. +# +# A wrapper around the emcc compiler which uses configure-time state +# to locate the Emscripten SDK and import the SDK's environment +# script, if needed. +######################################################################## +# EMSDK_HOME comes from the configure --with-emsdk=/dir flag. +# EMSDK_ENV_SH is ${thatDir}/emsdk_env.sh and is also set by the +# configure process. +EMSDK_HOME="@EMSDK_HOME@" +EMSDK_ENV_SH="@EMSDK_ENV_SH@" +emcc="@BIN_EMCC@" + +if [ x = "x${emcc}" ]; then + emcc=`which emcc 2>/dev/null` +fi + +if [ x = "x${emcc}" ]; then + # If emcc is not found in the path, try to find it via an emsdk + # installation. The SDK variant is the official installation style + # supported by the Emscripten project, but emcc is also available + # via package managers on some OSes. + if [ x = "x${EMSDK_HOME}" ]; then + echo "EMSDK_HOME is not set. Pass --with-emsdk=/path/to/emsdk" \ + "to the configure script." 1>&2 + exit 1 + fi + + if [ x = "x${EMSDK_ENV_SH}" ]; then + if [ -f "${EMSDK_HOME}/emsdk_env.sh" ]; then + EMSDK_ENV_SH="${EMSDK_HOME}/emsdk_env.sh" + else + echo "EMSDK_ENV_SH is not set. Expecting configure script to set it." 1>&2 + exit 2 + fi + fi + + if [ ! -f "${EMSDK_ENV_SH}" ]; then + echo "emsdk_env script not found: $EMSDK_ENV_SH" 1>&2 + exit 3 + fi + + # $EMSDK is part of the state set by emsdk_env.sh. + if [ x = "x${EMSDK}" ]; then + EMSDK_QUIET=1 + export EMSDK_QUIET + # ^^^ Squelches informational output from ${EMSDK_ENV_SH}. + source "${EMSDK_ENV_SH}" || { + rc=$? + echo "Error sourcing ${EMSDK_ENV_SH}" + exit $rc + } + fi + emcc=`which emcc 2>/dev/null` + if [ x = "x${emcc}" ]; then + echo "emcc not found in PATH. Normally that's set up by ${EMSDK_ENV_SH}." 1>&2 + exit 4 + fi +fi + +exec $emcc "$@" diff --git a/tools/encode_math.sh b/tools/encode_math.sh new file mode 100755 index 0000000..c870d51 --- /dev/null +++ b/tools/encode_math.sh @@ -0,0 +1,12 @@ +#!/bin/sh +bindir="`dirname "$0"`" +topdir="`dirname "$bindir"`" +mimetex -d "`cat "$topdir/art/encode1.tex"`" > "$topdir/www/encode1.gif" +mimetex -d "`cat "$topdir/art/encode2.tex"`" > "$topdir/www/encode2.gif" +mimetex -d "`cat "$topdir/art/encode3.tex"`" > "$topdir/www/encode3.gif" +mimetex -d "`cat "$topdir/art/encode4.tex"`" > "$topdir/www/encode4.gif" +mimetex -d "`cat "$topdir/art/encode5.tex"`" > "$topdir/www/encode5.gif" +mimetex -d "`cat "$topdir/art/encode6.tex"`" > "$topdir/www/encode6.gif" +mimetex -d "`cat "$topdir/art/encode7.tex"`" > "$topdir/www/encode7.gif" +mimetex -d "`cat "$topdir/art/encode8.tex"`" > "$topdir/www/encode8.gif" +mimetex -d "`cat "$topdir/art/encode9.tex"`" > "$topdir/www/encode9.gif" diff --git a/tools/fake-smtpd.tcl b/tools/fake-smtpd.tcl new file mode 100644 index 0000000..e29fd3c --- /dev/null +++ b/tools/fake-smtpd.tcl @@ -0,0 +1,84 @@ +#!/usr/bin/tclsh +# +# This script is a testing aid for working on the Relay notification method +# in Fossil. +# +# This script listens for connections on port 25 or probably some other TCP +# port specified by the "--port N" option. It pretend to be an SMTP server, +# though it does not actually relay any email. Instead, it just prints the +# SMTP conversation on stdout. +# +# If the "--max N" option is used, then the fake SMTP server shuts down +# with an error after receiving N messages from the client. This can be +# used to test retry capabilities in the client. +# +# Suggested Test Procedure For Fossil Relay Notifications +# +# 1. Bring up "fossil ui" +# 2. Configure notification for relay to localhost:8025 +# 3. Start this script in a separate window. Something like: +# tclsh fake-smtpd.tcl -port 8025 -max 100 +# 4. Send test messages using Fossil +# +proc conn_puts {chan txt} { + puts "S: $txt" + puts $chan $txt + flush $chan +} +set mxMsg 100000000 +proc connection {chan ip port} { + global mxMsg + set nMsg 0 + puts "*** begin connection from $ip:$port ***" + conn_puts $chan "220 localhost fake-SMTPD" + set inData 0 + while {1} { + set line [string trimright [gets $chan]] + if {$line eq ""} { + if {[eof $chan]} break + } + puts "C: $line" + incr nMsg + if {$inData} { + if {$line eq "."} { + set inData 0 + conn_puts $chan "250 Ok" + } + } elseif {$nMsg>$mxMsg} { + conn_puts $chan "999 I'm done!" + break + } elseif {[string match "HELO *" $line]} { + conn_puts $chan "250 Ok" + } elseif {[string match "EHLO *" $line]} { + conn_puts $chan "250-SIZE 100000" + conn_puts $chan "250 HELP" + } elseif {[string match "DATA*" $line]} { + conn_puts $chan "354 End data with <CR><LF>.<CR><LF>" + set inData 1 + } elseif {[string match "QUIT*" $line]} { + conn_puts $chan "221 Bye" + break + } else { + conn_puts $chan "250 Ok" + } + } + puts "*** connection closed ($nMsg messages) ***" + close $chan +} +set port 25 +set argc [llength $argv] +for {set i 0} {$i<$argc-1} {incr i} { + set arg [lindex $argv $i] + if {$arg eq "-port" || $arg eq "--port"} { + incr i + set port [lindex $argv $i] + } + if {$arg eq "-max" || $arg eq "--max"} { + incr i + set mxMsg [lindex $argv $i] + } +} +puts "listening on localhost:$port" +socket -server connection $port +set forever 0 +vwait forever diff --git a/tools/find-fossil-cgis.tcl b/tools/find-fossil-cgis.tcl new file mode 100755 index 0000000..d85b411 --- /dev/null +++ b/tools/find-fossil-cgis.tcl @@ -0,0 +1,141 @@ +#!/usr/bin/tclsh +# +# This script scans a directory hierarchy looking for Fossil CGI files - +# the files that are used to launch Fossil as a CGI program. For each +# such file found, in prints the name of the file and also the file +# content, indented, if the --print option is used. +# +# tclsh find-fossil-cgis.tcl [OPTIONS] DIRECTORY +# +# The argument is the directory from which to begin the search. +# +# OPTIONS can be zero or more of the following: +# +# --has REGEXP Only show the CGI if the body matches REGEXP. +# May be repeated multiple times, in which case +# all must match. +# +# --hasnot REGEXP Only show the CGI if it does NOT match the +# REGEXP. +# +# --print Show the content of the CGI, indented by +# three spaces +# +# --symlink Process DIRECTORY arguments that are symlinks. +# Normally symlinks are silently ignored. +# +# -v Show progress information for debugging +# +# EXAMPLE USE CASES: +# +# Find all CGIs that do not have the "errorlog:" property set +# +# find-fossil-cgis.tcl *.website --has '\nrepository:' \ +# --hasnot '\nerrorlog:' +# +# Add the errorlog: property to any CGI that does not have it: +# +# find-fossil-cgis.tcl *.website --has '\nrepository:' \ +# --hasnot '\nerrorlog:' | while read x +# do +# echo 'errorlog: /logs/errors.txt' >>$x +# done +# +# Find and print all CGIs that do redirects +# +# find-fossil-cgis.tcl *.website --has '\nredirect:' --print +# + + +# Find the CGIs in directory $dir. Invoke recursively to +# scan subdirectories. +# +proc find_in_one_dir {dir} { + global HAS HASNOT PRINT V + if {$V>0} { + puts "# $dir" + } + foreach obj [lsort [glob -nocomplain -directory $dir *]] { + if {[file isdir $obj]} { + find_in_one_dir $obj + continue + } + if {![file isfile $obj]} continue + if {[file size $obj]>5000} continue + if {![file exec $obj]} continue + if {![file readable $obj]} continue + set fd [open $obj rb] + set txt [read $fd] + close $fd + if {![string match #!* $txt]} continue + if {![regexp {fossil} $txt]} continue + if {![regexp {\nrepository: } $txt] && + ![regexp {\ndirectory: } $txt] && + ![regexp {\nredirect: } $txt]} continue + set ok 1 + foreach re $HAS { + if {![regexp $re $txt]} {set ok 0; break;} + } + if {!$ok} continue + foreach re $HASNOT { + if {[regexp $re $txt]} {set ok 0; break;} + } + if {!$ok} continue + # + # At this point assume we have found a CGI file. + # + puts $obj + if {$PRINT} { + regsub -all {\n} [string trim $txt] "\n " out + puts " $out" + } + } +} +set HAS [list] +set HASNOT [list] +set PRINT 0 +set SYMLINK 0 +set V 0 +set N [llength $argv] +set DIRLIST [list] + +# First pass: Gather all the command-line arguments but do no +# processing. +# +for {set i 0} {$i<$N} {incr i} { + set dir [lindex $argv $i] + if {($dir eq "-has" || $dir eq "--has") && $i<[expr {$N-1}]} { + incr i + lappend HAS [lindex $argv $i] + continue + } + if {($dir eq "-hasnot" || $dir eq "--hasnot") && $i<[expr {$N-1}]} { + incr i + lappend HASNOT [lindex $argv $i] + continue + } + if {$dir eq "-print" || $dir eq "--print"} { + set PRINT 1 + continue + } + if {$dir eq "-symlink" || $dir eq "--symlink"} { + set SYMLINK 1 + continue + } + if {$dir eq "-v"} { + set V 1 + continue + } + if {[file type $dir]=="directory"} { + lappend DIRLIST $dir + } +} + +# Second pass: Process the non-option arguments. +# +foreach dir $DIRLIST { + set type [file type $dir] + if {$type eq "directory" || ($SYMLINK && $type eq "link")} { + find_in_one_dir $dir + } +} diff --git a/tools/fossil-autocomplete.bash b/tools/fossil-autocomplete.bash new file mode 100755 index 0000000..c2af0dd --- /dev/null +++ b/tools/fossil-autocomplete.bash @@ -0,0 +1,15 @@ +# Command name completion for Fossil. +# Mailing-list contribution by Stuart Rackham. +function _fossil() { + local cur commands + cur=${COMP_WORDS[COMP_CWORD]} + commands=$(fossil help --all) + if [ $COMP_CWORD -eq 1 ] || [ ${COMP_WORDS[1]} = help ]; then + # Command name completion for 1st argument or 2nd if help command. + COMPREPLY=( $(compgen -W "$commands" $cur) ) + else + # File name completion for other arguments. + COMPREPLY=( $(compgen -f $cur{}) ) + fi +} +complete -o default -F _fossil fossil f diff --git a/tools/fossil-autocomplete.zsh b/tools/fossil-autocomplete.zsh new file mode 100755 index 0000000..0c34951 --- /dev/null +++ b/tools/fossil-autocomplete.zsh @@ -0,0 +1,3529 @@ +#compdef fossil +# Origin: https://chiselapp.com/user/lifepillar/repository/fossil-zsh-completion +################################################################################# +# # +# Copyright 2020 Lifepillar # +# # +# Permission is hereby granted, free of charge, to any person obtaining a copy # +# of this software and associated documentation files (the "Software"), to deal # +# in the Software without restriction, including without limitation the rights # +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # +# copies of the Software, and to permit persons to whom the Software is # +# furnished to do so, subject to the following conditions: # +# # +# The above copyright notice and this permission notice shall be included in # +# all copies or substantial portions of the Software. # +# # +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # +# SOFTWARE. # +# # +################################################################################# + +# To reload the completion function after it has been modified: +# +# $ unfunction _fossil +# $ autoload -U _fossil +# +# See also: http://zsh.sourceforge.net/Doc/Release/Completion-System.html +# See also: https://github.com/zsh-users/zsh-completions/blob/master/zsh-completions-howto.org + +################################################################################ +# Functions that help build this completion file # +################################################################################ + +# This function can be used to generate scaffolding code for the options of all +# the commands. Copy and paste the result at the suitable spot in this script +# to update it. To parse all commands: +# +# __fossil_parse_help -a +# +# To parse all test commands: +# +# __fossil_parse_help -t +# +# NOTE: The code must be adapted manually. Diff with previous version! +function __fossil_parse_help() { + echo ' case "$words[1]" in' + for c in `fossil help $1 | xargs -n1 | sort`; + do + echo " ($c)" + echo ' _arguments \\' + __fossil_format_options $c; + echo " '(- *)'--help'[Show help and exit]' \\" + echo " '*:files:_files'" + echo '' + echo ' ;;' + done; + echo ' esac' + echo ' ;;' +} + +# Extract the options of a command and format it in a way that can be used in +# a ZSH completion script. +# Use `__fossil_format_options -o` to extract the common options. +function __fossil_format_options() { + fossil help $1 2>&1 \ + | grep '^\s\{1,3\}-' \ + | sed -E 's/^ +//' \ + | awk -F ' +' '{ + v=match($1,/\|/) + split($1,y,"|") + printf " " + if (v>0) + printf "\"(--help %s %s)\"{%s,%s}",y[1],y[2],y[1],y[2]; + else + printf "\"(--help %s)\"%s",y[1],y[1]; + $1="" + gsub(/^ +| +$/,"",$0); + gsub(/^ +| +$/,"",$0); + gsub(/\x27/,"\x27\"\x27\"\x27",$0); + print "\x27["$0"]\x27 \\"; + }' +} + + +################################################################################ +# Helper functions used for completion. # +################################################################################ + +function __fossil_commands() { + fossil help --all +} + +function __fossil_test_commands() { + fossil help --test +} + +function __fossil_all_commands() { + __fossil_commands + __fossil_test_commands +} + +function __fossil_users() { + fossil user ls 2>/dev/null | awk '{print $1}' +} + +function __fossil_branches() { + fossil branch ls -a 2>/dev/null | sed 's/\* *//' +} + +function __fossil_tags() { + fossil tag ls 2>/dev/null +} + +function __fossil_repos() { + ls | grep .fossil + fossil all ls 2>/dev/null +} + +function __fossil_remotes() { + fossil remote list 2>/dev/null | awk '{print $1}' +} + +function __fossil_wiki_pages() { + fossil wiki list 2>/dev/null +} + +function __fossil_areas() { + compadd all email project shun skin ticket user alias subscriber + return 0 +} + +function __fossil_settings() { + fossil help --setting +} + +function __fossil_urls() { + local u + u=($(__fossil_remotes)) + compadd -a u + compadd -S '' file:// http:// https:// ssh:// + return 0 +} + +################################################################################ +# Main # +################################################################################ + +function _fossil() { + local context state state_descr line + typeset -A opt_args + + local -a _common_options + # Scaffolding code for common options can be generated with `__fossil_format_options -o`. + _common_options=( + "(--help --args)"--args'[FILENAME Read additional arguments and options from FILENAME]:file:_files' + "(--help --cgitrace)"--cgitrace'[Active CGI tracing]' + "(--help --comfmtflags --comment-format)"--comfmtflags'[VALUE Set comment formatting flags to VALUE]:value:' + "(--help --comment-format --comfmtflags)"--comment-format'[VALUE Alias for --comfmtflags]:value:' + "(--help --errorlog)"--errorlog'[FILENAME Log errors to FILENAME]:file:_files' + "(- -? --help)"{-?,--help}'[Show help on the command rather than running it]' + "(--help --httptrace)"--httptrace'[Trace outbound HTTP requests]' + "(--help --localtime)"--localtime'[Display times using the local timezone]' + "(--help --no-th-hook)"--no-th-hook'[Do not run TH1 hooks]' + "(--help --quiet)"--quiet'[Reduce the amount of output]' + "(--help --sqlstats)"--sqlstats'[Show SQL usage statistics when done]' + "(--help --sqltrace)"--sqltrace'[Trace all SQL commands]' + "(--help --sshtrace)"--sshtrace'[Trace SSH activity]' + "(--help --ssl-identity)"--ssl-identity'[NAME Set the SSL identity to NAME]:name:' + "(--help --systemtrace)"--systemtrace'[Trace calls to system()]' + "(--help --user -U)"{--user,-U}'[USER Make the default user be USER]:user:($(__fossil_users))' + "(--help --utc)"--utc'[Display times using UTC]' + "(--help --vfs)"--vfs'[NAME Cause SQLite to use the NAME VFS]:name:' + ) + + local -a _fossil_clean_options + _fossil_clean_options=( + "(--help --allckouts)"--allckouts'[Check for empty directories within any checkouts]' + "(--help --case-sensitive)"--case-sensitive'[BOOL Override case-sensitive setting]:bool:(yes no)' + "(--help --dirsonly)"--dirsonly'[Only remove empty directories]' + "(--help --disable-undo)"--disable-undo'[Disables use of the undo]' + "(--help --dotfiles)"--dotfiles'[Include files beginning with a dot (".")]' + "(--help --emptydirs)"--emptydirs'[Remove empty directories]' + "(--help -f --force)"{-f,--force}'[Remove files without prompting]' + "(--help -i --prompt)"{-i,--prompt}'[Prompt before removing each file]' + "(--help -x --verily)"{-x,--verily}'[Remove everything that is not managed]' + "(--help --clean)"--clean'[CSG Never prompt to delete files matching CSG glob pattern]:pattern:' + "(--help --ignore)"--ignore'[CSG Ignore files matching CSG glob pattern]:pattern:' + "(--help --keep)"--keep'[CSG Keep files matching CSG glob pattern]:pattern:' + "(--help -n --dry-run)"{-n,--dry-run}'[Delete nothing, but display what would have been deleted]' + "(--help --no-prompt)"--no-prompt'[Assume NO for every question]' + "(--help --temp)"--temp'[Remove only Fossil-generated temporary files]' + "(--help -v --verbose)"{-v,--verbose}'[Show all files as they are removed]' + ) + + local -a _fossil_rebuild_options + _fossil_rebuild_options=( + "(--help --analyze)"--analyze'[Run ANALYZE on the database after rebuilding]' + "(--help --cluster)"--cluster'[Compute clusters for unclustered artifacts]' + "(--help --compress)"--compress'[Strive to make the database as small as possible]' + "(--help --compress-only)"--compress-only'[Skip the rebuilding step. Do --compress only]' + "(--help --deanalyze)"--deanalyze'[Remove ANALYZE tables from the database]' + "(--help --ifneeded)"--ifneeded'[Only do the rebuild if it would change the schema version]' + "(--help --index)"--index'[Always add in the full-text search index]' + "(--help --noverify)"--noverify'[Skip the verification of changes to the BLOB table]' + "(--help --noindex)"--noindex'[Always omit the full-text search index]' + "(--help --pagesize)"--pagesize'[N Set the database pagesize to N. (512..65536 and power of 2)]:number:' + "(--help --quiet)"--quiet'[Only show output if there are errors]' + "(--help --stats)"--stats'[Show artifact statistics after rebuilding]' + "(--help --vacuum)"--vacuum'[Run VACUUM on the database after rebuilding]' + "(--help --wal)"--wal'[Set Write-Ahead-Log journalling mode on the database]' + ) + + local -a _fossil_dbstat_options + _fossil_dbstat_options=( + "(--help --brief -b)"{--brief,-b}'[Only show essential elements]' + "(--help --db-check)"--db-check'[Run "PRAGMA quick_check" on the repository database]' + "(--help --db-verify)"--db-verify'[Run a full verification of the repository integrity]' + "(--help --omit-version-info)"--omit-version-info'[Omit the SQLite and Fossil version information]' + ) + + local -a _fossil_diff_options + _fossil_diff_options=( + "(--help --binary)"--binary'[PATTERN Treat files that match the glob PATTERN as binary]:pattern:' + "(--help --branch)"--branch'[BRANCH Show diff of all changes on BRANCH]:branch:($(__fossil_branches))' + "(--help --brief)"--brief'[Show filenames only]' + "(--help --checkin)"--checkin'[VERSION Show diff of all changes in VERSION]:version:' + "(--help --command)"--command'[PROG External diff program - overrides "diff-command"]:program:' + "(--help --context -c)"{--context,-c}'[N Use N lines of context]:number:' + "(--help --diff-binary)"--diff-binary'[BOOL Include binary files when using external commands]:bool:(yes no)' + "(--help --exec-abs-paths)"--exec-abs-paths'[Force absolute path names with external commands]' + "(--help --exec-rel-paths)"--exec-rel-paths'[Force relative path names with external commands]' + "(--help --from -r)"{--from,-r}'[VERSION Select VERSION as source for the diff]:version:' + "(--help --internal -i)"{--internal,-i}'[Use internal diff logic]' + "(--help --new-file -N)"{--new-file,-N}'[Show complete text of added and deleted files]' + "(--help --numstat)"--numstat'[Show only the number of lines delete and added]' + "(--help --side-by-side -y)"{--side-by-side,-y}'[Side-by-side diff]' + "(--help --strip-trailing-cr)"--strip-trailing-cr'[Strip trailing CR]' + "(--help --tclsh)"--tclsh'[PATH Tcl/Tk used for --tk (default: "tclsh")]' + "(--help --tk)"--tk'[Launch a Tcl/Tk GUI for display]' + "(--help --to)"--to'[VERSION Select VERSION as target for the diff]:version:' + "(--help --undo)"--undo'[Diff against the "undo" buffer]' + "(--help --unified)"--unified'[Unified diff]' + "(--help -v --verbose)"{-v,--verbose}'[Output complete text of added or deleted files]' + "(--help -w --ignore-all-space)"{-w,--ignore-all-space}'[Ignore white space when comparing lines]' + "(--help -W --width)"{-W,--width}'[NUM Width of lines in side-by-side diff]:number:' + "(--help -Z --ignore-trailing-space)"{-Z,--ignore-trailing-space}'[Ignore changes to end-of-line whitespace]' + ) + + local -a _fossil_extras_options + _fossil_extras_options=( + "(--help --abs-paths)"--abs-paths'[Display absolute pathnames]' + "(--help --case-sensitive)"--case-sensitive'[BOOL Override case-sensitive setting]:bool:(yes no)' + "(--help --dotfiles)"--dotfiles'[Include files beginning with a dot (".")]' + "(--help --header)"--header'[Identify the repository if there are extras]' + "(--help --ignore)"--ignore'[CSG Ignore files matching patterns from the argument]:pattern:' + "(--help --rel-paths)"--rel-paths'[Display pathnames relative to the current working]' + ) + + local -a _fossil_server_options + _fossil_server_options=( + "(--help --baseurl)"--baseurl'[URL Use URL as the base]:url:__fossil_urls' + "(--help --create)"--create'[Create a new REPOSITORY if it does not already exist]' + "(--help --extroot)"--extroot'[DIR Document root for the /ext extension mechanism]:directory:_files -/' + "(--help --files)"--files'[GLOBLIST Comma-separated list of glob patterns for static files]:pattern:' + "(--help --localauth)"--localauth'[Enable automatic login for requests from localhost]' + "(--help --localhost)"--localhost'[Listen on 126.0.0.1 only]' + "(--help --https)"--https'[Input passes through a reverse HTTPS->HTTP proxy]' + "(--help --jsmode)"--jsmode'[MODE Determine how JavaScript is delivered with pages]:mode:(inline separate bundled)' + "(--help --max-latency)"--max-latency'[N Do not let any single HTTP request run for more than N seconds]:number:' + "(--help --nocompress)"--nocompress'[Do not compress HTTP replies]' + "(--help --nojail)"--nojail'[Drop root privileges but do not enter the chroot jail]' + "(--help --nossl)"--nossl'[Signal that no SSL connections are available]' + "(--help --notfound)"--notfound'[URL Redirect]:url:__fossil_urls' + "(--help --page)"--page'[PAGE Start "ui" on PAGE. ex: --page "timeline?y=ci"]:number:' + "(--help -P --port)"{-P,--port}'[TCPPORT Listen to request on port TCPPORT]:number:' + "(--help --th-trace)"--th-trace'[Trace TH0 execution (for debugging purposes)]' + "(--help --repolist)"--repolist'[If REPOSITORY is dir, URL "/" lists repos]' + "(--help --scgi)"--scgi'[Accept SCGI rather than HTTP]' + "(--help --skin)"--skin'[LABEL Use override skin LABEL]:label:' + "(--help --usepidkey)"--usepidkey'[Use saved encryption key from parent process]' + ) + + _arguments -C \ + ${_common_options[@]} \ + '1:command:->command' \ + '*::args:->args' + + case $state in + (command) + if [[ $line =~ '^te' ]]; then + _arguments '*:test commands:($(__fossil_test_commands))' + else + _arguments '*:commands:($(__fossil_commands))' + fi + ;; + (args) + if [[ $line[1] =~ '^test-' ]]; then + __fossil_complete_test_commands + return 0 + fi + + case $line[1] in + (3-way-merge) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '*:files:_files' + + ;; + (add) + _arguments \ + "(--help --case-sensitive)"--case-sensitive'[BOOL Override the case-sensitive setting]:bool:(yes no)' \ + "(--help --dotfiles)"--dotfiles'[include files beginning with a dot (".")]' \ + "(--help -f --force)"{-f,--force}'[Add files without prompting]' \ + "(--help --ignore)"--ignore'[CSG Ignore unmanaged files matching Comma Separated Glob]:pattern:' \ + "(--help --clean)"--clean'[CSG Also ignore files matching Comma Separated Glob]:pattern:' \ + "(--help --reset)"--reset'[Reset the ADDED state of a checkout]' \ + "(--help -v --verbose)"{-v,--verbose}'[Outputs information about each --reset file]' \ + "(--help -n --dry-run)"{-n,--dry-run}'[Display instead of run actions]' \ + '(- *)'--help'[Show help and exit]' \ + '*:files:_files' + + ;; + (addremove) + _arguments \ + "(--help --case-sensitive)"--case-sensitive'[BOOL Override the case-sensitive setting]:bool:(yes no)' \ + "(--help --dotfiles)"--dotfiles'[Include files beginning with a dot (".")]' \ + "(--help --ignore)"--ignore'[CSG Ignore unmanaged files matching Comma Separated Glob]:pattern:' \ + "(--help --clean)"--clean'[CSG Also ignore files matching Comma Separated Glob]:pattern:' \ + "(--help -n --dry-run)"{-n,--dry-run}'[If given, display instead of run actions]' \ + "(--help --reset)"--reset'[Reset the ADDED/DELETED state of a checkout]' \ + "(--help --verbose -v)"{--verbose,-v}'[Outputs information about each --reset file]' \ + '(- *)'--help'[Show help and exit]' \ + '*:files:_files' + + ;; + (alerts) + __fossil_alerts + + ;; + (all) + __fossil_all + + ;; + (amend) + _arguments \ + ':hash:' \ + "(--help --author)"--author'[USER Make USER the author for check-in]:user:($(__fossil_users))' \ + "(--help -m --comment)"{-m,--comment}'[COMMENT Make COMMENT the check-in comment]:comment:' \ + "(--help -M --message-file)"{-M,--message-file}'[FILE Read the amended comment from FILE]:file:_files' \ + "(--help -e --edit-comment)"{-e,--edit-comment}'[Launch editor to revise comment]' \ + "(--help --date)"--date'[DATETIME Make DATETIME the check-in time]:datetime:(now)' \ + "(--help --bgcolor)"--bgcolor'[COLOR Apply COLOR to this check-in]:color:' \ + "(--help --branchcolor)"--branchcolor'[COLOR Apply and propagate COLOR to the branch]:color:' \ + "(--help --tag)"--tag'[TAG Add new TAG to this check-in]:tag:($(__fossil_tags))' \ + "(--help --cancel)"--cancel'[TAG Cancel TAG from this check-in]:tag:($(__fossil_tags))' \ + "(--help --branch)"--branch'[NAME Make this check-in the start of branch NAME]:name:($(__fossil_branches))' \ + "(--help --hide)"--hide'[Hide branch starting from this check-in]' \ + "(--help --close)"--close'[Mark this "leaf" as closed]' \ + "(--help -n --dry-run)"{-n,--dry-run}'[Print control artifact, but make no changes]' \ + "(--help --date-override)"--date-override'[DATETIME Set the change time on the control artifact]:datetime:(now)' \ + "(--help --user-override)"--user-override'[USER Set the user name on the control artifact]:user:($(__fossil_users))' \ + '(- *)'--help'[Show help and exit]' + + ;; + (annotate|blame|praise) + _arguments \ + "(--help --filevers)"--filevers'[Show file version numbers]' \ + "(--help -r --revision)"{-r,--revision}'[VERSION The specific check-in containing the file]:version:' \ + "(--help -l --log)"{-l,--log}'[List all versions analyzed]' \ + "(--help -n --limit)"{-n,--limit}'[LIMIT Limit versions]:limit:(none)' \ + "(--help -o --origin)"{-o,--origin}'[VERSION The origin check-in]:version:' \ + "(--help -w --ignore-all-space)"{-w,--ignore-all-space}'[Ignore white space when comparing lines]' \ + "(--help -Z --ignore-trailing-space)"{-Z,--ignore-trailing-space}'[Ignore whitespace at line end]' \ + '(- *)'--help'[Show help and exit]' \ + '1:file:_files' + + ;; + (artifact) + _arguments \ + "(--help -R --repository)"{-R,--repository}'[FILE Extract artifacts from repository FILE]:fossils:($(__fossil_repos))' \ + '(- *)'--help'[Show help and exit]' \ + '1:artifact id:' \ + '2::output file:_files' + + ;; + (attachment) + _arguments \ + "(--help -t --technote)"{-t,--technote}'[DATETIME The timestamp of the technote]:datetime:(now)' \ + "(--help -t --technote)"{-t,--technote}'[TECHNOTE-ID The technote to be updated]:technote-id:' \ + '(- *)'--help'[Show help and exit]' \ + '1:what:(add)' \ + '::pagename:($(__fossil_wiki_pages))' \ + ':file:_files' + + ;; + (backoffice) + _arguments \ + "(--help --debug)"--debug'[Show what this command is doing]' \ + "(--help --logfile)"--logfile'[FILE Append a log of backoffice actions onto FILE]:file:_files' \ + "(--help --min)"--min'[N Invoke backoffice at least once every N seconds]:number:' \ + "(--help --poll)"--poll'[N Polling frequency]:number:' \ + "(--help --trace)"--trace'[Enable debugging output on stderr]' \ + "(--help --nodelay)"--nodelay'[Do not queue up or wait for a backoffice job]' \ + "(--help --nolease)"--nolease'[Always run backoffice]' \ + '(- *)'--help'[Show help and exit]' \ + '*:fossils:($(__fossil_repos))' + + ;; + (backup) + _arguments \ + "(--help --overwrite)"--overwrite'[OK to overwrite an existing file]' \ + "(--help -R)"-R'[NAME Filename of the repository to backup]:fossils:($(__fossil_repos))' \ + '(- *)'--help'[Show help and exit]' \ + '1:file:_files' + + ;; + (bisect) + __fossil_bisect + + ;; + (branch) + __fossil_branch + + ;; + (bundle) + __fossil_bundle + + ;; + (cache) + __fossil_cache + + ;; + (cat) + _arguments \ + "(--help -R --repository)"{-R,--repository}'[FILE Extract artifacts from repository FILE]:fossils:($(__fossil_repos))' \ + "(--help -r)"-r'[VERSION The specific check-in containing the file]:version:' \ + '(- *)'--help'[Show help and exit]' \ + '*:files:_files' + + ;; + (cgi) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '::cgi:' \ + ':file:_files' + + ;; + (changes|status) + _arguments \ + "(--help --abs-paths)"--abs-paths'[Display absolute pathnames]' \ + "(--help --rel-paths)"--rel-paths'[Display pathnames relative to the current directory]' \ + "(--help --hash)"--hash'[Verify file status using hashing]' \ + "(--help --case-sensitive)"--case-sensitive'[BOOL Override case-sensitive setting]:bool:(yes no)' \ + "(--help --dotfiles)"--dotfiles'[Include unmanaged files beginning with a dot]' \ + "(--help --ignore)"--ignore'[CSG Ignore unmanaged files matching CSG glob patterns]:pattern:' \ + "(--help --header)"--header'[Identify the repository if report is non-empty.]' \ + "(--help -v --verbose)"{-v,--verbose}'[Say "(none)" if the change report is empty]' \ + "(--help --classify)"--classify'[Start each line with the file'"'"'s change type]' \ + "(--help --no-classify)"--no-classify'[Do not print file change types]' \ + "(--help --edited)"--edited'[Display edited, merged, and conflicted files]' \ + "(--help --updated)"--updated'[Display files updated by merge/integrate]' \ + "(--help --changed)"--changed'[Combination of --edited and --updated]' \ + "(--help --missing)"--missing'[Display missing files]' \ + "(--help --added)"--added'[Display added files]' \ + "(--help --deleted)"--deleted'[Display deleted files]' \ + "(--help --renamed)"--renamed'[Display renamed files]' \ + "(--help --conflict)"--conflict'[Display files having merge conflicts]' \ + "(--help --meta)"--meta'[Display files with metadata changes]' \ + "(--help --unchanged)"--unchanged'[Display unchanged files]' \ + "(--help --all)"--all'[Display all managed files]' \ + "(--help --extra)"--extra'[Display unmanaged files]' \ + "(--help --differ)"--differ'[Display modified and extra files]' \ + "(--help --merge)"--merge'[Display merge contributors]' \ + "(--help --no-merge)"--no-merge'[Do not display merge contributors]' \ + '(- *)'--help'[Show help and exit]' \ + '*:files:_files' + + ;; + (checkout|co) + _arguments \ + "(--help --force)"--force'[Ignore edited files in the current checkout]' \ + "(--help --keep)"--keep'[Only update the manifest and manifest.uuid files]' \ + "(--help --force-missing)"--force-missing'[Force checkout even if content is missing]' \ + "(--help --setmtime)"--setmtime'[Set timestamps of all files to match their SCM-side]' \ + '(- *)'--help'[Show help and exit]' \ + '*:files:_files' + + ;; + (ci|commit) + _arguments \ + "(--help --allow-conflict)"--allow-conflict'[Allow unresolved merge conflicts]' \ + "(--help --allow-empty)"--allow-empty'[Allow a commit with no changes]' \ + "(--help --allow-fork)"--allow-fork'[Allow the commit to fork]' \ + "(--help --allow-older)"--allow-older'[Allow a commit older than its ancestor]' \ + "(--help --baseline)"--baseline'[Use a baseline manifest in the commit process]' \ + "(--help --bgcolor)"--bgcolor'[COLOR Apply COLOR to this one check-in only]:color:' \ + "(--help --branch)"--branch'[NEW-BRANCH-NAME check in to this new branch]:new branch:' \ + "(--help --branchcolor)"--branchcolor'[COLOR Apply given COLOR to the branch]:color:' \ + "(--help --close)"--close'[Close the branch being committed]' \ + "(--help --date-override)"--date-override'[DATETIME DATE to use instead of '"'"'now'"'"']:datetime:' \ + "(--help --delta)"--delta'[Use a delta manifest in the commit process]' \ + "(--help --hash)"--hash'[Verify file status using hashing]' \ + "(--help --integrate)"--integrate'[Close all merged-in branches]' \ + "(--help -m --comment)"{-m,--comment}'[COMMENT Use COMMENT as commit comment]:comment:' \ + "(--help -M --message-file)"{-M,--message-file}'[FILE Read the commit comment from given file]:file:_files' \ + "(--help --mimetype)"--mimetype'[MIMETYPE Mimetype of check-in comment]:mimetype:' \ + "(--help -n --dry-run)"{-n,--dry-run}'[If given, display instead of run actions]' \ + "(--help --no-prompt)"--no-prompt'[Assume NO for every question]' \ + "(--help --no-warnings)"--no-warnings'[Omit all warnings about file contents]' \ + "(--help --no-verify)"--no-verify'[Do not run before-commit hooks]' \ + "(--help --nosign)"--nosign'[Do not attempt to sign this commit with gpg]' \ + "(--help --override-lock)"--override-lock'[Allow a check-in even though parent is locked]' \ + "(--help --private)"--private'[Do not sync changes and their descendants]' \ + "(--help --tag)"--tag'[TAG-NAME Assign given tag TAG-NAME to the check-in]:tag:($(__fossil_tags))' \ + "(--help --trace)"--trace'[Debug tracing]' \ + "(--help --user-override)"--user-override'[USER Use USER instead of the current default]:user:($(__fossil_users))' \ + '(- *)'--help'[Show help and exit]' \ + '*:files:_files' + + ;; + (clean) + _arguments \ + ${_fossil_clean_options[@]} \ + '*:files:_files' + + ;; + (clone) + _arguments \ + "(--help --admin-user -A)"{--admin-user,-A}'[USERNAME Make USERNAME the administrator]:user:($(__fossil_users))' \ + "(--help --httpauth -B)"{--httpauth,-B}'[USER:PASS Add HTTP Basic Authorization to requests]:user pass:' \ + "(--help --nocompress)"--nocompress'[Omit extra delta compression]' \ + "(--help --once)"--once'[Don'"'"'t remember the URI]' \ + "(--help --private)"--private'[Also clone private branches]' \ + "(--help --save-http-password)"--save-http-password'[Remember the HTTP password without asking]' \ + "(--help --ssh-command -c)"{--ssh-command,-c}'[SSH Use SSH as the "ssh" command]:ssh command:' \ + "(--help --ssl-identity)"--ssl-identity'[FILENAME Use the SSL identity if requested by the server]:file:_files' \ + "(--help -u --unversioned)"{-u,--unversioned}'[Also sync unversioned content]' \ + "(--help -v --verbose)"{-v,--verbose}'[Show more statistics in output]' \ + '(- *)'--help'[Show help and exit]' \ + '1:uri:__fossil_urls' \ + '2:file:_files' + + ;; + (close) + _arguments \ + "(--help --force -f)"{--force,-f}'[Close a check out with uncommitted changes]' \ + '(- *)'--help'[Show help and exit]' + + ;; + (configuration) + __fossil_configuration + + ;; + (dbstat) + _arguments \ + ${_fossil_dbstat_options[@]} \ + '(- *)'--help'[Show help and exit]' + + ;; + (deconstruct) + _arguments \ + "(--help -R --repository)"{-R,--repository}'[REPOSITORY Deconstruct given REPOSITORY]:fossils:($(__fossil_repos))' \ + "(--help -K --keep-rid1)"{-K,--keep-rid1}'[Save the filename of the artifact with RID=1]' \ + "(--help -L --prefixlength)"{-L,--prefixlength}'[N Set the length of the names of the DESTINATION]:number:' \ + "(--help --private)"--private'[Include private artifacts]' \ + "(--help -P --keep-private)"{-P,--keep-private}'[Save the list of private artifacts to .private]' \ + '(- *)'--help'[Show help and exit]' \ + '1:destination:_files -/' + + ;; + (delete|forget|rm) + _arguments \ + "(--help --soft)"--soft'[Skip removing files from the checkout]' \ + "(--help --hard)"--hard'[Remove files from the checkout]' \ + "(--help --case-sensitive)"--case-sensitive'[BOOL Override the case-sensitive setting]:bool:(yes no)' \ + "(--help -n --dry-run)"{-n,--dry-run}'[If given, display instead of run actions]' \ + "(--help --reset)"--reset'[Reset the DELETED state of a checkout]' \ + "(--help --verbose -v)"{--verbose,-v}'[Outputs information about each --reset file]' \ + '(- *)'--help'[Show help and exit]' \ + '*:files:_files' + + ;; + (descendants) + _arguments \ + "(--help -R --repository)"{-R,--repository}'[FILE Extract info from repository FILE]:fossils:($(__fossil_repos))' \ + "(--help -W --width)"{-W,--width}'[NUM Width of lines]:number:' \ + '(- *)'--help'[Show help and exit]' \ + '1::check-in:' + + ;; + (diff|gdiff) + _arguments \ + ${_fossil_diff_options[@]} \ + '(- *)'--help'[Show help and exit]' \ + '*:files:_files' + + ;; + (export) + _arguments \ + '(- *)'--help'[Show help and exit]' + + ;; + (extras) + _arguments \ + ${_fossil_extras_options[@]} \ + '(- *)'--help'[Show help and exit]' \ + '*:files:_files -/' + + ;; + (finfo) + _arguments \ + "(--help -b --brief)"{-b,--brief}'[Display a brief (one line / revision) summary]' \ + "(--help --case-sensitive)"--case-sensitive'[BOOL Enable or disable case-sensitive filenames]:bool:(yes no)' \ + "(--help -l --log)"{-l,--log}'[Select log mode (the default)]' \ + "(--help -n --limit)"{-n,--limit}'[N Display only the first N changes]:number:' \ + "(--help --offset)"--offset'[P Skip P changes]:number:' \ + "(--help -p --print)"{-p,--print}'[Select print mode]' \ + "(--help -r --revision)"{-r,--revision}'[R Print the given revision]:version:' \ + "(--help -s --status)"{-s,--status}'[Select status mode (print a status indicator for FILE)]' \ + "(--help -W --width)"{-W,--width}'[NUM Width of lines]:number:' \ + '(- *)'--help'[Show help and exit]' \ + '1:file:_files' + + ;; + (fts-config) + __fossil_fts_config + + ;; + (git) + __fossil_git + + ;; + (grep) + _arguments \ + "(--help -c --count)"{-c,--count}'[Suppress normal output; print count of files]' \ + "(--help -i --ignore-case)"{-i,--ignore-case}'[Ignore case]' \ + "(--help -l --files-with-matches)"{-l,--files-with-matches}'[List only hash for each match]' \ + "(--help --once)"--once'[Stop searching after the first match]' \ + "(--help -s --no-messages)"{-s,--no-messages}'[Suppress error messages]' \ + "(--help -v --invert-match)"{-v,--invert-match}'[Invert the sense of matching]' \ + "(--help --verbose)"--verbose'[Show each file as it is analyzed]' \ + '(- *)'--help'[Show help and exit]' \ + '1:pattern:' \ + '*:files:_files' + + ;; + (hash-policy) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1::policy:(sha1 auto sha3 sha3-only shun-sha1)' + + ;; + (help) + _arguments \ + "(- :)"{-a,--all}'[List both common and auxiliary commands]' \ + "(- :)"{-o,--options}'[List command-line options common to all commands]' \ + "(- :)"{-s,--setting}'[List setting names]' \ + "(- :)"{-t,--test}'[List unsupported "test" commands]' \ + "(- :)"{-x,--aux}'[List only auxiliary commands]' \ + "(- :)"{-w,--www}'[List all web pages]' \ + "(- :):all commands:($(__fossil_all_commands))" \ + "(- *)"--html'[Format output as HTML rather than plain text]' + + ;; + (hook) + __fossil_hook + + ;; + (http) + _arguments \ + "(--help --baseurl)"--baseurl'[URL Base URL (useful with reverse proxies)]:url:__fossil_urls' \ + "(--help --extroot)"--extroot'[DIR Document root for the /ext extension mechanism]:file:_files -/' \ + "(--help --files)"--files'[GLOB Comma-separate glob patterns for static file to serve]:pattern:' \ + "(--help --host)"--host'[NAME Specify hostname of the server]:name:' \ + "(--help --https)"--https'[Signal a request coming in via https]' \ + "(--help --in)"--in'[FILE Take input from FILE instead of standard input]:file:_files' \ + "(--help --ipaddr)"--ipaddr'[ADDR Assume the request comes from the given IP address]:address:' \ + "(--help --jsmode)"--jsmode'[MODE Determine how JavaScript is delivered with pages]:mode:(inline separate bundled)' \ + "(--help --localauth)"--localauth'[Enable automatic login for local connections]' \ + "(--help --nocompress)"--nocompress'[Do not compress HTTP replies]' \ + "(--help --nodelay)"--nodelay'[Omit backoffice processing if it would delay process exit]' \ + "(--help --nojail)"--nojail'[Drop root privilege but do not enter the chroot jail]' \ + "(--help --nossl)"--nossl'[Signal that no SSL connections are available]' \ + "(--help --notfound)"--notfound'[URL Use URL as "HTTP 404, object not found" page]:url:__fossil_urls' \ + "(--help --out)"--out'[FILE Write results to FILE instead of to standard output]:file:_files' \ + "(--help --repolist)"--repolist'[If REPOSITORY is directory, URL "/" lists all repos]' \ + "(--help --scgi)"--scgi'[Interpret input as SCGI rather than HTTP]' \ + "(--help --skin)"--skin'[LABEL Use override skin LABEL]:label:' \ + "(--help --th-trace)"--th-trace'[Trace TH1 execution (for debugging purposes)]' \ + "(--help --usepidkey)"--usepidkey'[Use saved encryption key from parent process]' \ + '(- *)'--help'[Show help and exit]' \ + '1::fossils:_files' + + ;; + (import) + _arguments \ + "(--help --git)"--git'[Import from the git-fast-export file format (default)]' \ + "(--help --import-marks)"--import-marks'[FILE Restore marks table from FILE]:file:_files' \ + "(--help --export-marks)"--export-marks'[FILE Save marks table to FILE]:files:_files' \ + "(--help --rename-master)"--rename-master'[NAME Renames the master branch to NAME]:name:' \ + "(--help --use-author)"--use-author'[Uses author as the committer]:user:($(__fossil_users))' \ + "(--help --svn)"--svn'[Import from the svnadmin-dump file format]' \ + "(--help --trunk)"--trunk'[FOLDER Name of trunk folder]:file:_files -/' \ + "(--help --branches)"--branches'[FOLDER Name of branches folder]:file:_files -/' \ + "(--help --tags)"--tags'[FOLDER Name of tags folder]:file:_files -/' \ + "(--help --base)"--base'[PATH Path to project root in repository]:file:_files' \ + "(--help --flat)"--flat'[The whole dump is a single branch]' \ + "(--help --rev-tags)"--rev-tags'[Tag each revision, implied by -i]' \ + "(--help --no-rev-tags)"--no-rev-tags'[Disables tagging effect of -i]' \ + "(--help --rename-rev)"--rename-rev'[PAT Rev tag names, default "svn-rev-%"]:pattern:' \ + "(--help --ignore-tree)"--ignore-tree'[DIR Ignores subtree rooted at DIR]:file:_files -/' \ + "(--help -i --incremental)"{-i,--incremental}'[Allow importing into an existing repository]' \ + "(--help -f --force)"{-f,--force}'[Overwrite repository if already exists]' \ + "(--help -q --quiet)"{-q,--quiet}'[Omit progress output]' \ + "(--help --no-rebuild)"--no-rebuild'[Skip the "rebuilding metadata" step]' \ + "(--help --no-vacuum)"--no-vacuum'[Skip the final VACUUM of the database file]' \ + "(--help --rename-trunk)"--rename-trunk'[NAME use NAME as name of imported trunk branch]:name:' \ + "(--help --rename-branch)"--rename-branch'[PAT Rename all branch names using PAT pattern]:pattern:' \ + "(--help --rename-tag)"--rename-tag'[PAT Rename all tag names using PAT pattern]:pattern:' \ + "(--help --admin-user -A)"{--admin-user,-A}'[NAME Use NAME for the admin user]:user:($(__fossil_users))' \ + '(- *)'--help'[Show help and exit]' \ + '*:files:_files' + + ;; + (info) + _arguments \ + "(--help -R --repository)"{-R,--repository}'[FILE Extract info from repository FILE]:fossils:($(__fossil_repos))' \ + "(--help -v --verbose)"{-v,--verbose}'[Show extra information about repositories]' \ + '(- *)'--help'[Show help and exit]' \ + '1:fossils:($(__fossil_repos))' + + ;; + (init|new) + _arguments \ + "(--help --template)"--template'[FILE Copy settings from repository file]:file:_files' \ + "(--help --admin-user -A)"{--admin-user,-A}'[USERNAME Select given USERNAME as admin user]:user:($(__fossil_users))' \ + "(--help --date-override)"--date-override'[DATETIME Use DATETIME as time of the initial check-in]:datetime:(now)' \ + "(--help --sha1)"--sha1'[Use an initial hash policy of "sha1"]' \ + '(- *)'--help'[Show help and exit]' \ + '1:file:_files' + + ;; + (json) + __fossil_json + + ;; + (leaves) + _arguments \ + "(--help -a --all)"{-a,--all}'[Show ALL leaves]' \ + "(--help --bybranch)"--bybranch'[Order output by branch name]' \ + "(--help -c --closed)"{-c,--closed}'[Show only closed leaves]' \ + "(--help -m --multiple)"{-m,--multiple}'[Show only cases with multiple leaves on a single branch]' \ + "(--help --recompute)"--recompute'[Recompute the "leaf" table in the repository DB]' \ + "(--help -W --width)"{-W,--width}'[NUM Width of lines (default is to auto-detect)]:number:' \ + '(- *)'--help'[Show help and exit]' + + ;; + (login-group) + __fossil_login_group + + ;; + (ls) + _arguments \ + "(--help --age)"--age'[Show when each file was committed]' \ + "(--help -v --verbose)"{-v,--verbose}'[Provide extra information about each file]' \ + "(--help -t)"-t'[Sort output in time order]' \ + "(--help -r)"-r'[VERSION The specific check-in to list]:version:' \ + "(--help -R --repository)"{-R,--repository}'[FILE Extract info from repository FILE]:fossils:($(__fossil_repos))' \ + '(- *)'--help'[Show help and exit]' \ + '*:files:_files' + + ;; + (md5sum) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '*:files:_files' + + ;; + (merge) + _arguments \ + "(--help --backout)"--backout'[Do a reverse cherrypick merge against VERSION]' \ + "(--help --baseline)"--baseline'[BASELINE Use BASELINE as the "pivot" of the merge instead]:version:' \ + "(--help --binary)"--binary'[GLOBPATTERN Treat files that match GLOBPATTERN as binary]:pattern:' \ + "(--help --case-sensitive)"--case-sensitive'[BOOL Override the case-sensitive setting]:bool:(yes no)' \ + "(--help --cherrypick)"--cherrypick'[Do a cherrypick merge VERSION into the current checkout]' \ + "(--help -f --force)"{-f,--force}'[Force the merge even if it would be a no-op]' \ + "(--help --force-missing)"--force-missing'[Force the merge even if there is missing content]' \ + "(--help --integrate)"--integrate'[Merged branch will be closed when committing]' \ + "(--help -K --keep-merge-files)"{-K,--keep-merge-files}'[On merge conflict, retain the temporary files]' \ + "(--help -n --dry-run)"{-n,--dry-run}'[If given, display instead of run actions]' \ + "(--help -v --verbose)"{-v,--verbose}'[Show additional details of the merge]' \ + '(- *)'--help'[Show help and exit]' \ + '1:version:' + + ;; + (mv|rename) + _arguments \ + "(--help --soft)"--soft'[Skip moving files within the checkout]' \ + "(--help --hard)"--hard'[Move files within the checkout]' \ + "(--help --case-sensitive)"--case-sensitive'[BOOL Override the case-sensitive setting]:bool:(yes no)' \ + "(--help -n --dry-run)"{-n,--dry-run}'[If given, display instead of run actions]' \ + '(- *)'--help'[Show help and exit]' \ + '*:files:_files' + + ;; + (open) + _arguments \ + "(--help --empty)"--empty'[Initialize checkout as being empty, but still connected]' \ + "(--help --force)"--force'[Continue even if the directory is not empty]' \ + "(--help --force-missing)"--force-missing'[Force opening a repository with missing content]' \ + "(--help --keep)"--keep'[Only modify the manifest and manifest.uuid files]' \ + "(--help --nested)"--nested'[Allow opening a repository inside an opened checkout]' \ + "(--help --repodir)"--repodir'[DIR Store the clone in DIR]:directory:_files -/' \ + "(--help --setmtime)"--setmtime'[Set timestamps of all files to match their SCM-side times]' \ + "(--help --workdir)"--workdir'[DIR Use DIR as the working directory]:directory:_files -/' \ + '(- *)'--help'[Show help and exit]' \ + '1:fossils:($(__fossil_repos))' \ + '::version:' + + ;; + (pop3d) + _arguments \ + "(--help --logdir)"--logdir'[DIR Create log files inside DIR]:directory:_files -/' \ + '(- *)'--help'[Show help and exit]' \ + '1:fossils:($(__fossil_repos))' + + ;; + (publish) + _arguments \ + '(--help --only)'--only'[Publish only the specific artifacts identified by the tags]' \ + '(- *)'--help'[Show help and exit]' \ + '*:tags:($(__fossil_tags))' + + ;; + (pull) + _arguments \ + "(--help -B --httpauth)"{-B,--httpauth}'[USER:PASS Credentials for the simple HTTP auth protocol]:user pass:' \ + "(--help --from-parent-project)"--from-parent-project'[Pull content from the parent project]' \ + "(--help --ipv4)"--ipv4'[Use only IPv4, not IPv6]' \ + "(--help --once)"--once'[Do not remember URL for subsequent syncs]' \ + "(--help --private)"--private'[Pull private branches too]' \ + "(--help --project-code)"--project-code'[CODE Use CODE as the project code]:project code:' \ + "(--help --proxy)"--proxy'[PROXY Use the specified HTTP proxy]:proxy:' \ + "(--help -R --repository)"{-R,--repository}'[REPO Local repository to pull into]:fossils:($(__fossil_repos))' \ + "(--help --ssl-identity)"--ssl-identity'[FILE Local SSL credentials]:ssl credentials:' \ + "(--help --ssh-command)"--ssh-command'[SSH Use SSH as the "ssh" command]:ssh command:' \ + "(--help -v --verbose)"{-v,--verbose}'[Additional (debugging) output]' \ + "(--help --verily)"--verily'[Exchange extra information with the remote]' \ + '(- *)'--help'[Show help and exit]' \ + '1:url:__fossil_urls' + + ;; + (purge) + __fossil_purge + + ;; + (push) + _arguments \ + "(--help -B --httpauth)"{-B,--httpauth}'[USER:PASS Credentials for the simple HTTP auth protocol]:user pass:' \ + "(--help --ipv4)"--ipv4'[Use only IPv4, not IPv6]' \ + "(--help --once)"--once'[Do not remember URL for subsequent syncs]' \ + "(--help --proxy)"--proxy'[PROXY Use the specified HTTP proxy]' \ + "(--help --private)"--private'[Push private branches too]' \ + "(--help -R --repository)"{-R,--repository}'[REPO Local repository to push from]:fossils:($(__fossil_repos))' \ + "(--help --ssl-identity)"--ssl-identity'[FILE Local SSL credentials]:ssl credentials:' \ + "(--help --ssh-command)"--ssh-command'[SSH Use SSH as the "ssh" command]:ssh command:' \ + "(--help -v --verbose)"{-v,--verbose}'[Additional (debugging) output]' \ + "(--help --verily)"--verily'[Exchange extra information with the remote]' \ + '(- *)'--help'[Show help and exit]' \ + '1::url:__fossil_urls' + + ;; + (rebuild) + _arguments \ + ${_fossil_rebuild_options[@]} \ + "(--help --force)"--force'[Force the rebuild to complete even if errors are seen]' \ + "(--help --randomize)"--randomize'[Scan artifacts in a random order]' \ + '(- *)'--help'[Show help and exit]' \ + '1:fossils:($(__fossil_repos))' + + ;; + (reconstruct) + _arguments \ + "(--help -K --keep-rid1)"{-K,--keep-rid1}'[Read the filename of the artifact with RID=1 from .rid]' \ + "(--help -P --keep-private)"{-P,--keep-private}'[Mark the artifacts listed in .private as private]' \ + '(- *)'--help'[Show help and exit]' \ + '1:file:_files' \ + '2:directory:_files -/' + + ;; + (redo|undo) + _arguments \ + "(--help -n --dry-run)"{-n,--dry-run}'[Do not make changes but show what would be done]' \ + '(- *)'--help'[Show help and exit]' \ + '*:files:_files' + + ;; + (remote|remote-url) + __fossil_remote + + ;; + (reparent) + _arguments \ + "(--help --test)"--test'[Make database entries but do not add the tag artifact]' \ + "(--help -n --dryrun)"{-n,--dryrun}'[Do not actually change the database]' \ + "(--help --date-override)"--date-override'[DATETIME Set the change time on the control artifact]:datetime:' \ + "(--help --user-override)"--user-override'[USER Set the user name on the control artifact]:user:($(__fossil_users))' \ + '(- *)'--help'[Show help and exit]' \ + '1:check-in:' \ + '*:parents:' + + ;; + (revert) + _arguments \ + "(--help -r --revision)"{-r,--revision}'[VERSION Revert to given VERSION]:version:' \ + '(- *)'--help'[Show help and exit]' \ + '*:files:_files' + + ;; + (rss) + _arguments \ + "(--help -type -y)"{-type,-y}'[FLAG]:type:(all ci t w)' \ + "(--help -limit -n)"{-limit,-n}'[LIMIT The maximum number of items to show]:number:' \ + "(--help -tkt)"-tkt'[HASH Filters for only those events for the specified ticket]:hash:' \ + "(--help -tag)"-tag'[TAG Filters for a tag]:tag:($(__fossil_tags))' \ + "(--help -wiki)"-wiki'[NAME Filters on a specific wiki page]:wiki page:($(__fossil_wiki_pages))' \ + "(--help -name)"-name'[FILENAME filters for a specific file]:file:_files' \ + "(--help -url)"-url'[STRING Sets the RSS feed'"'"'s root URL to the given string]:url:__fossil_urls' \ + '(- *)'--help'[Show help and exit]' + + ;; + (scrub) + _arguments \ + "(--help --force)"--force'[Do not prompt for confirmation]' \ + "(--help --private)"--private'[Only private branches are removed from the repository]' \ + "(--help --verily)"--verily'[Scrub real thoroughly (see above)]' \ + '(- *)'--help'[Show help and exit]' \ + '1::fossils:($(__fossil_repos))' + + ;; + (search) + _arguments \ + "(--help -a --all)"{-a,--all}'[Output all matches, not just best matches]' \ + "(--help -n --limit)"{-n,--limit}'[N Limit output to N matches]:number:' \ + "(--help -W --width)"{-W,--width}'[WIDTH Set display width to WIDTH columns]:number:' \ + '(- *)'--help'[Show help and exit]' \ + '*:patterns:' + + ;; + (server|ui) + _arguments \ + ${_fossil_server_options[@]} \ + '1::fossils:($(__fossil_repos))' + + ;; + (set|settings) + _arguments \ + "(--global)"--global'[Set or unset the given property globally]' \ + "(--exact)"--exact'[Only consider exact name matches]' \ + "1:setting:($(__fossil_settings))" \ + '2:value:' + + ;; + (sha1sum) + _arguments \ + "(--help -h --dereference)"{-h,--dereference}'[Resolve symbolic links]' \ + '(- *)'--help'[Show help and exit]' \ + '*:files:_files' + + ;; + (sha3sum) + _arguments \ + "(--help --224 --256 --384 --512 --size)"--224'[Compute a SHA3-224 hash]' \ + "(--help --256 --224 --384 --512 --size)"--256'[Compute a SHA3-256 hash (the default)]' \ + "(--help --384 --224 --256 --512 --size)"--384'[Compute a SHA3-384 hash]' \ + "(--help --512 --224 --256 --384 --size)"--512'[Compute a SHA3-512 hash]' \ + "(--help --size --224 --256 --384 --512 --size)"--size'[N An N-bit hash]:number:' \ + "(--help -h --dereference)"{-h,--dereference}'[Resolve symbolic links]' \ + '(- *)'--help'[Show help and exit]' \ + '*:files:_files' + + ;; + (shell) + _arguments \ + '(- *)'--help'[Show help and exit]' + + ;; + (smtpd) + _arguments \ + "(--help --dryrun)"--dryrun'[Do not record any emails in the database]' \ + "(--help --trace)"--trace'[Print a transcript of the conversation on stderr]' \ + "(--help --ipaddr)"--ipaddr'[ADDR The SMTP connection originates at ADDR]:address:' \ + '(- *)'--help'[Show help and exit]' \ + '1:fossils:($(__fossil_repos))' + + ;; + (sql|sqlite3) + _arguments \ + "(--help --no-repository)"--no-repository'[Skip opening the repository database]' \ + "(--help --readonly)"--readonly'[Open the repository read-only]' \ + "(--help -R)"-R'[REPOSITORY Use REPOSITORY as the repository database]:fossils:($(__fossil_repos))' \ + '(- *)'--help'[Show help and exit]' + + ;; + (sqlar) + _arguments \ + "(--help -X --exclude)"{-X,--exclude}'[GLOBLIST Comma-separated list of GLOBs of files to exclude]:pattern:' \ + "(--help --include)"--include'[GLOBLIST Comma-separated list of GLOBs of files to include]:pattern:' \ + "(--help --name)"--name'[DIR The name of the top-level directory in the archive]:directory:_files -/' \ + "(--help -R)"-R'[REPOSITORY Specify a Fossil repository]:fossils:($(__fossil_repos))' \ + '(- *)'--help'[Show help and exit]' \ + '1:version:' \ + '2:output file:_files' + + ;; + (stash) + __fossil_stash + + ;; + (sync) + _arguments \ + "(--help -B --httpauth)"{-B,--httpauth}'[USER:PASS Credentials for the simple HTTP auth protocol]:user pass:' \ + "(--help --ipv3)"--ipv4'[Use only IPv4, not IPv6]' \ + "(--help --once)"--once'[Do not remember URL for subsequent syncs]' \ + "(--help --proxy)"--proxy'[PROXY Use the specified HTTP proxy]:proxy:' \ + "(--help --private)"--private'[Sync private branches too]' \ + "(--help -R --repository)"{-R,--repository}'[REPO Local repository to sync with]:fossils:($(__fossil_repos))' \ + "(--help --ssl-identity)"--ssl-identity'[FILE Local SSL credentials]:ssl credentials:' \ + "(--help --ssh-command)"--ssh-command'[SSH Use SSH as the "ssh" command]:ssh command:' \ + "(--help -u --unversioned)"{-u,--unversioned}'[Also sync unversioned content]' \ + "(--help -v --verbose)"{-v,--verbose}'[Additional (debugging) output]' \ + "(--help --verily)"--verily'[Exchange extra information with the remote]' \ + '(- *)'--help'[Show help and exit]' \ + '1::url:__fossil_urls' + + ;; + (tag) + __fossil_tag + + ;; + (tarball) + _arguments \ + "(--help -X --exclude)"{-X,--exclude}'[GLOBLIST Comma-separated list of GLOBs of files to exclude]:pattern:' \ + "(--help --include)"--include'[GLOBLIST Comma-separated list of GLOBs of files to include]:pattern:' \ + "(--help --name)"--name'[DIR The name of the top-level directory in the archive]:directory:_files -/' \ + "(--help -R)"-R'[REPOSITORY Specify a Fossil repository]:fossils:($(__fossil_repos))' \ + '(- *)'--help'[Show help and exit]' \ + '1:version:' \ + '2:ouput file:_files' + + ;; + (ticket) + __fossil_ticket + + ;; + (timeline) + _arguments \ + "(--help -n --limit)"{-n,--limit}'[N Output the first N entries]:number:' \ + "(--help -p --path)"{-p,--path}'[PATH Output items affecting PATH only]:path:_files' \ + "(--help --offset)"--offset'[P skip P changes]:number:' \ + "(--help --sql)"--sql'[Show the SQL used to generate the timeline]' \ + "(--help -t --type)"{-t,--type}'[TYPE Output items from the given types only]:type:(ci e t w)' \ + "(--help -v --verbose)"{-v,--verbose}'[Output the list of files changed by each commit]' \ + "(--help -W --width)"{-W,--width}'[NUM Width of lines (default is to auto-detect)]:number:' \ + "(--help -R)"-R'[REPO Specifies the repository db to use]:fossils:($(__fossil_repos))' \ + '(- *)'--help'[Show help and exit]' \ + '::when:(before after descendants children ancestors parents)' \ + '::check-in:' \ + '::datetime:' + + ;; + (tls-config) + __fossil_tls_config + + ;; + (touch) + _arguments \ + "(--help --now -c --checkin -C --checkout)"--now'[Stamp each affected file with the current time]' \ + "(--help -c --checkin --now -C --checkout)"{-c,--checkin}'[Stamp with the last modification time]' \ + "(--help -C --checkout -c --checkin --now)"{-C,--checkout}'[Stamp with the time of the current checkout]' \ + "(--help -g)"-g'[GLOBLIST Comma-separated list of glob patterns]:pattern:' \ + "(--help -G)"-G'[GLOBFILE Like -g but reads globs from glob default file]:pattern:' \ + "(--help -v -verbose)"{-v,-verbose}'[Outputs extra information about its globs]' \ + "(--help -n --dry-run)"{-n,--dry-run}'[Do not touch anything]' \ + "(--help -q --quiet)"{-q,--quiet}'[Suppress warnings]' \ + '(- *)'--help'[Show help and exit]' \ + '*:files:_files' + + ;; + (unpublished) + _arguments \ + "(--help --all)"--all'[Show all artifacts, not just check-ins]' \ + '(- *)'--help'[Show help and exit]' + + ;; + (unset) + _arguments \ + "(--global)"--global'[Set or unset the given property globally]' \ + "(--exact)"--exact'[Only consider exact name matches]' \ + "1:setting:($(__fossil_settings))" + + ;; + (unversioned|uv) + __fossil_unversioned + + ;; + (update) + _arguments \ + "(--help --case-sensitive)"--case-sensitive'[BOOL Override case-sensitive setting]:bool:(yes no)' \ + "(--help --debug)"--debug'[Print debug information on stdout]' \ + "(--help --latest)"--latest'[Update to latest version]' \ + "(--help --force-missing)"--force-missing'[Force update if missing content after sync]' \ + "(--help -n --dry-run)"{-n,--dry-run}'[If given, display instead of run actions]' \ + "(--help -v --verbose)"{-v,--verbose}'[Print status information about all files]' \ + "(--help -W --width)"{-W,--width}'[WIDTH Width of lines (default is to auto-detect)]:number:' \ + "(--help --setmtime)"--setmtime'[Set timestamps of all files to match their SCM-side times]' \ + "(--help -K --keep-merge-files)"{-K,--keep-merge-files}'[On merge conflict, retain the temporary files]' \ + '(- *)'--help'[Show help and exit]' \ + '1::version:' \ + '*::files:_files' + + ;; + (user) + __fossil_user + + ;; + (version) + _arguments \ + "(--help -v --verbose)"{-v,--verbose}'[Additional details about optional features]' \ + '(- *)'--help'[Show help and exit]' + + ;; + (whatis) + _arguments \ + "(--help --type)"--type'[TYPE Only find artifacts of TYPE]:type:(ci t w g e)' \ + "(--help -v --verbose)"{-v,--verbose}'[Provide extra information (such as the RID)]' \ + '(- *)'--help'[Show help and exit]' \ + '1:name:' + + ;; + (wiki) + __fossil_wiki + + ;; + (zip) + _arguments \ + "(--help -X --exclude)"{-X,--exclude}'[GLOBLIST Comma-separated list of GLOBs of files to exclude]:pattern:' \ + "(--help --include)"--include'[GLOBLIST Comma-separated list of GLOBs of files to include]:pattern:' \ + "(--help --name)"--name'[DIR The name of the top-level directory in the archive]:directory:_files -/' \ + "(--help -R)"-R'[REPOSITORY Specify a Fossil repository]:fossils:($(__fossil_repos))' \ + '(- *)'--help'[Show help and exit]' \ + '1:version:' \ + '2:output file:_files' + + ;; + esac + ;; + *) + esac + return 0 +} + +################################################################################ +# Subcommands # +################################################################################ + +######################################## +# fossil alerts # +######################################## +function __fossil_alerts_settings() { + fossil alerts settings 2>/dev/null +} + +function __fossil_alerts_subscribers() { + fossil alerts subscribers 2>/dev/null +} + +function __fossil_alerts() { + local curcontext="$curcontext" state line + typeset -A opt_args + local -a _common_options + + _common_options=( + "(--help -R --repository)"{-R,--repository}'[FILE Run command on repository FILE]:fossils:($(__fossil_repos))' + "(-)"--help'[Show help and exit]' + ) + + _arguments -C \ + ${_common_options[@]} \ + '1:subcommand:->subcommand' \ + '*::options:->options' + + case $state in + (subcommand) + _values 'subcommand' \ + pending reset send settings status subscribers \ + test-message unsubscribe + + ;; + (options) + case $line[1] in + (pending|reset) + _arguments \ + ${_common_options[@]} + + ;; + (send) + _arguments \ + ${_common_options[@]} \ + "(--help --digest)"--digest'[Send digests]' \ + "(--help --test)"--test'[Write to standard output]' + + ;; + (set|settings) + _arguments \ + ${_common_options[@]} \ + '1::name:($(__fossil_alerts_settings))' \ + '2::value:' + + ;; + (subscribers) + _arguments \ + ${_common_options[@]} \ + '1::pattern:' + + ;; + (test-message) + _arguments \ + ${_common_options[@]} \ + "(--help --body)"--body'[FILENAME]:file:_files' \ + "(--help --smtp-trace)"--smtp-trace'[]' \ + "(--help --stdout)"--stdout'[]' \ + "(--help --subject -S)"{--subject,-S}'[SUBJECT]:subject:' \ + '1:recipient:' + + ;; + unsubscribe) + _arguments \ + ${_common_options[@]} \ + '1:email:($(__fossil_alerts_subscribers))' + + ;; + *) + _message 'no more arguments' + + ;; + esac + ;; + esac + return 0 +} + +######################################## +# fossil all # +######################################## +function __fossil_all() { + local curcontext="$curcontext" state line + typeset -A opt_args + local -a _common_options + + _common_options=( + "(-)"--help'[Show help and exit]' + "(--help --showfile)"--showfile'[Show the repository or checkout being operated upon]' + "(--help --dontstop)"--dontstop'[Continue with other repositories even after an error]' + "(--help --dry-run)"--dry-run'[If given, display instead of run actions]' + ) + + _arguments -C \ + ${_common_options[@]} \ + '1:subcommand:->subcommand' \ + '*::options:->options' + + case $state in + (subcommand) + _values 'subcommand' \ + backup cache changes clean config dbstat extras fts-config info \ + pull push rebuild sync set unset server ui add ignore list ls + + ;; + (options) + case $line[1] in + (backup) + _arguments \ + ${_common_options[@]} \ + '1:output directory:_files -/' + + ;; + (cache) + __fossil_cache + + ;; + (clean) + _arguments \ + ${_common_options[@]} \ + ${_fossil_clean_options[@]} \ + "(--help --whatif)"--whatif'[Review the files to be deleted]' + + ;; + (config) + _arguments \ + ${_common_options[@]} \ + '1:what:(pull)' \ + '2:area:' + + ;; + (dbstat) + _arguments \ + ${_fossil_dbstat_options[@]} + + ;; + (extras) + _arguments \ + ${_fossil_extras_options[@]} + + ;; + (fts-config) + __fossil_fts_config + + ;; + (pull|push) + _arguments \ + ${_common_options[@]} \ + "(--help -v --verbose)"{-v,--verbose}'[Additional (debugging) output]' + + ;; + (rebuild) + _arguments \ + ${_common_options[@]} \ + ${_fossil_rebuild_options[@]} + + ;; + (sync) + _arguments \ + ${_common_options[@]} \ + "(--help -u --unversioned)"{-u,--unversioned}'[Also sync unversioned content]' \ + "(--help -v --verbose)"{-v,--verbose}'[Additional (debugging) output]' + + ;; + (set|unset) + _arguments \ + ${_common_options[@]} \ + "1:setting:($(__fossil_settings))" \ + '2:value:' \ + "(--help --global)"--global'[Set or unset the given property globally]' \ + "(--help --exact)"--exact'[Only consider exact name matches]' + + ;; + (server|ui) + _arguments \ + ${_common_options[@]} \ + ${_fossil_server_options[@]} + + ;; + (add) + _arguments \ + ${_common_options[@]} \ + '*:fossils:($(__fossil_repos))' + + ;; + (ignore) + _arguments \ + ${_common_options[@]} \ + "(--help -c --ckout)"{-c,--ckout}'[Ignore local checkouts instead of repositories]' \ + '*:fossils:($(__fossil_repos))' + + ;; + (list|ls) + _arguments \ + "(-)"--help'[Show help and exit]' \ + "(--help -c --ckout)"{-c,--ckout}'[List local checkouts instead of repositories]' + + ;; + *) + _message 'no more arguments' + + ;; + esac + ;; + esac + return 0 +} + + +######################################## +# fossil bisect # +######################################## +function __fossil_bisect() { + local curcontext="$curcontext" state line + typeset -A opt_args + local -a _common_options + + _common_options=( + "(-)"--help'[Show help and exit]' + ) + + _arguments -C \ + ${_common_options[@]} \ + '1:subcommand:->subcommand' \ + '*::options:->options' + + case $state in + (subcommand) + _values 'subcommand' \ + bad good log chart next options reset skip vlist ls status ui undo + + ;; + (options) + case $line[1] in + (bad|good|skip) + _arguments \ + '1:version:' + + ;; + (options) + _arguments \ + "1::name:($(fossil bisect options 2>/dev/null | awk '{print $1}'))" \ + '2::value:' + + ;; + (vlist|ls|status) + _arguments \ + "(-a --all)"{-a,--all}'[List all]' + + ;; + (ui) + _arguments \ + ${_fossil_server_options[@]} + + ;; + *) + _message 'no more arguments' + + ;; + esac + ;; + esac + return 0 +} + +######################################## +# fossil branch # +######################################## +function __fossil_branch() { + local curcontext="$curcontext" state line + typeset -A opt_args + local -a _common_options + + _common_options=( + "(-)"--help'[Show help and exit]' + "(--help -R --repository)"{-R,--repository}'[FILE Run commands on repository FILE]:fossils:($(__fossil_repos))' + ) + + _arguments -C \ + ${_common_options[@]} \ + '1:subcommand:->subcommand' \ + '*::options:->options' + + case $state in + (subcommand) + _values 'subcommand' \ + current info list ls new + + ;; + (options) + case $line[1] in + (current) + _arguments \ + ${_common_options[@]} + + ;; + (info) + _arguments \ + ${_common_options[@]} \ + ':branch:($(__fossil_branches))' + + ;; + (list|ls) + _arguments \ + ${_common_options[@]} \ + "(--help -a --all)"{-a,--all}'[List all branches]' \ + "(--help -c --closed)"{-c,--closed}'[List closed branches]' \ + "(--help -r)"-r'[Reverse the sort order]' \ + "(--help -t)"-t'[Show recently changed branches first]' + + ;; + (new) + _arguments \ + ${_common_options[@]} \ + "(--help --private)"--private'[Branch is private]' \ + "(--help --bgcolor)"--bgcolor'[COLOR Use COLOR instead of automatic background]:color:' \ + "(--help --nosign)"--nosign'[Do not sign contents on this branch]' \ + "(--help --date-override)"--date-override'[DATE Use DATE instead of '"'"'now'"'"']:date:(now)' \ + "(--help --user-override)"--user-override'[USER Use USER instead of the default]:user:($(__fossil_users))' \ + '1:branch name:' \ + '2:base check-in:' + + ;; + *) + _message 'no more arguments' + + ;; + esac + ;; + esac + return 0 +} + +######################################## +# fossil bundle # +######################################## +function __fossil_bundle() { + local curcontext="$curcontext" state line + typeset -A opt_args + local -a _common_options + + _common_options=( + "(-)"--help'[Show help and exit]' + ) + + _arguments -C \ + ${_common_options[@]} \ + '1:subcommand:->subcommand' \ + '*::options:->options' + + case $state in + (subcommand) + _values 'subcommand' \ + append cat export extend import ls purge + + ;; + (options) + case $line[1] in + (append) + _arguments \ + '1:bundle:_files' \ + '*:files:_files' + + ;; + (bundle) + _arguments \ + '1:what:(cat)' \ + '2:bundle:_files' \ + '*:hashes:' + + ;; + (export) + _arguments \ + "(--branch)"--branch'[BRANCH Package all check-ins on BRANCH]:branch:($(__fossil_branches))' \ + "(--from)"--from'[TAG1 --to TAG2 Package check-ins starting with TAG1]:tag:($(__fossil_tags))' \ + "(--to)"--to'[TAG2 Package check-ins up to TAG2]:tag:($(__fossil_tags))' \ + "(--checkin)"--checkin'[TAG Package the single check-in TAG]:tag:($(__fossil_tags))' \ + "(--standalone)"--standalone'[Do no use delta-encoding against]' \ + '1:bundle:_files' + + ;; + (extend|ls|purge) + _arguments \ + '1:bundle:_files' + + ;; + (import) + _arguments \ + "(--publish)"--publish'[Make the import public]' \ + "(--force)"--force'[Import even if project codes do not match]' \ + '1:bundle:_files' + + ;; + *) + _message 'no more arguments' + + ;; + esac + ;; + esac + return 0 +} + +######################################## +# fossil cache # +######################################## +function __fossil_cache_subcommand { +} + +function __fossil_cache() { + local curcontext="$curcontext" state line + typeset -A opt_args + local -a _common_options + + _common_options=( + "(-)"--help'[Show help and exit]' + ) + + _arguments -C \ + ${_common_options[@]} \ + '1:subcommand:->subcommand' \ + '*::options:->options' + + case $state in + (subcommand) + _values 'subcommand' \ + clear init list ls status + + ;; + *) + _message 'no more arguments' + + ;; + esac + return 0 +} + +######################################## +# fossil configuration # +######################################## +function __fossil_configuration() { + local curcontext="$curcontext" state line + typeset -A opt_args + local -a _common_options + + _common_options=( + "(--help -R --repository)"{-R,--repository}'[FILE Extract info from repository FILE]:fossils:($(__fossil_repos))' + "(-)"--help'[Show help and exit]' + ) + + _arguments -C \ + ${_common_options[@]} \ + '1:method:->method' \ + '*::options:->options' + + case $state in + (method) + _values 'method' \ + export import merge pull push reset sync + + ;; + (options) + case $line[1] in + (export) + _arguments \ + ${_common_options[@]} \ + '1:area:__fossil_areas' \ + '2:file:_files' + + ;; + (import|merge) + _arguments \ + ${_common_options[@]} \ + '1:file:_files' + + ;; + (pull) + _arguments \ + ${_common_options[@]} \ + "(--help --overwrite)"--overwrite'[Replace local settings]' \ + '1:area:__fossil_areas' \ + '2::url:__fossil_urls' + + ;; + (push|sync) + _arguments \ + ${_common_options[@]} \ + '1:area:__fossil_areas' \ + '2::url:__fossil_urls' + + ;; + (reset) + _arguments \ + ${_common_options[@]} \ + '1:area:__fossil_areas' + + ;; + *) + _message 'no more arguments' + + ;; + esac + ;; + esac + return 0 +} + +######################################## +# fossil fts-config # +######################################## +function __fossil_fts_config() { + local curcontext="$curcontext" state line + typeset -A opt_args + local -a _common_options + + _common_options=( + "(-)"--help'[Show help and exit]' + ) + + _arguments -C \ + ${_common_options[@]} \ + '1:subcommand:->subcommand' \ + '*::options:->options' + + case $state in + (subcommand) + _values 'subcommand' \ + reindex index enable disable stemmer + + ;; + (options) + case $line[1] in + index|stemmer) + _arguments \ + "(- *):setting:(on off)" + + ;; + enable|disable) + _arguments \ + "(- *):setting:(cdtwe)" + + ;; + *) + _message 'no more arguments' + + ;; + esac + ;; + esac + return 0 +} + +######################################## +# fossil git # +######################################## +function __fossil_git() { + local curcontext="$curcontext" state line + typeset -A opt_args + local -a _common_options + + _common_options=( + "(-)"--help'[Show help and exit]' + ) + + _arguments -C \ + ${_common_options[@]} \ + '1:subcommand:->subcommand' \ + '*::options:->options' + + case $state in + subcommand) + _values 'subcommand' \ + export import status + + ;; + options) + case $line[1] in + (export) + _arguments \ + ${_common_options[@]} \ + "(--help --autopush)"--autopush'[URL Automatically do a '"'"'git push'"'"' to URL]:url:__fossil_urls' \ + "(--help --debug)"--debug'[FILE Write fast-export text to FILE]:file:_files' \ + "(--help --force -f)"{--force,-f}'[Do the export even if nothing has changed]' \ + "(--help --limit)"--limit'[N Add no more than N new check-ins to MIRROR]:number:' \ + "()*"{--quiet,-q}'[Reduce output. Repeat for even less output]' \ + "(--verbose -v)"{--verbose,-v}'[More output]' + + ;; + (import) + _arguments \ + ${_common_options[@]} \ + ':url:__fossil_urls' + + ;; + (status) + _arguments \ + ${_common_options[@]} + + ;; + *) + _message 'no more arguments' + + ;; + esac + ;; + esac + return 0 +} + +######################################## +# fossil hook # +######################################## +function __fossil_hook() { + local curcontext="$curcontext" state line + typeset -A opt_args + local -a _common_options + + _common_options=( + "(-)"--help'[Show help and exit]' + ) + + _arguments -C \ + ${_common_options[@]} \ + '1:subcommand:->subcommand' \ + '*::options:->options' + + case $state in + (subcommand) + _values 'subcommand' \ + add delete edit list status test + + ;; + (options) + case $line[1] in + (add) + _arguments \ + ${_common_options[@]} \ + "(--help --command)"--command'[COMMAND]:command:' \ + "(--help --type)"--type'[TYPE]:type:' \ + "(--help --sequence)"--sequence'[NUM]:number:' + + ;; + (delete) + _arguments \ + ${_common_options[@]} \ + '*:IDs:' + + ;; + (edit) + _arguments \ + ${_common_options[@]} \ + "(--help --command)"--command'[COMMAND]:command:' \ + "(--help --type)"--type'[TYPE]:type:' \ + "(--help --sequence)"--sequence'[NUM]:number:' \ + '*:IDs:' + + ;; + (list|status) + _arguments \ + ${_common_options[@]} \ + + ;; + (test) + _arguments \ + ${_common_options[@]} \ + "(--help --dry-run)"--dry-run'[Print the script on stdout rather than run it]' \ + "(--help --base-rcvid)"--base-rcvid'[N Pretend that the hook-last-rcvid value is N]:number:' \ + "(--help --new-rcvid)"--new-rcvid'[M Pretend that the last rcvid valud is M]:number:' \ + "(--help --aux-file)"--aux-file'[NAME NAME is substituted for %A in the script]:name:' \ + '1:ID:' + + ;; + *) + _message 'no more arguments' + + ;; + esac + ;; + esac + return 0 +} + +######################################## +# fossil json # +######################################## +function __fossil_json_subcommands() { + _values 'subcommand' \ + anonymousPassword artifact branch cap config diff dir g login logout \ + query rebuild report resultCodes stat tag timeline user version \ + whoami wiki +} + +function __fossil_json() { + local curcontext="$curcontext" state line + typeset -A opt_args + local -a _common_options + + _common_options=( + "(-)"--help'[Show help and exit]' + "(--help -R --repository)"{-R,--repository}'[FILE Run commands on repository FILE]:fossils:($(__fossil_repos))' + ) + + _arguments -C \ + ${_common_options[@]} \ + '1:subcommand:->subcommand' \ + '*::options:->options' + + case $state in + (subcommand) + _arguments \ + '(- *)'-json-input'[FILE Read JSON data from FILE]:file:_files' \ + '1:subcommand:__fossil_json_subcommands' + + ;; + (options) + case $line[1] in + -json-input) + _arguments \ + ${_common_options[@]} \ + '1:file:_files' + + ;; + *) + _message 'no more arguments' + + ;; + esac + ;; + *) + esac + return 0 +} + +######################################## +# fossil login-group # +######################################## +function __fossil_login_group() { + local curcontext="$curcontext" state line + typeset -A opt_args + local -a _common_options + + _common_options=( + "(-)"--help'[Show help and exit]' + ) + + _arguments -C \ + ${_common_options[@]} \ + '1:subcommand:->subcommand' \ + '*::options:->options' + + case $state in + (subcommand) + _values 'subcommand' \ + join leave + + ;; + (options) + case $line[1] in + (join) + _arguments \ + "(-name)"-name'[Specified the name of the login group]:name:' \ + ':fossils:($(__fossil_repos))' + + ;; + *) + _message 'no more arguments' + + ;; + esac + ;; + esac + return 0 +} + +######################################## +# fossil purge # +######################################## +function __fossil_purge() { + local curcontext="$curcontext" state line + typeset -A opt_args + local -a _common_options + + _common_options=( + "(--help --explain --dry-run)"--explain'[Make no changes, but show what would happen]' + "(--help --dry-run --explain)"--dry-run'[An alias for --explain]' + "(-)"--help'[Show help and exit]' + ) + + _arguments -C \ + "(-)"--help'[Show help and exit]' \ + '1:subcommand:->subcommand' \ + '*::options:->options' + + case $state in + (subcommand) + _values 'subcommand' \ + artifacts cat checkins files list ls obliterate tickets undo wiki + + ;; + (options) + case $line[1] in + (artifacts|cat) + _arguments \ + ${_common_options[@]} \ + '*:hashes:' + + ;; + (checkins) + _arguments \ + ${_common_options[@]} \ + '*:tags:($(__fossil_tags))' + + ;; + (files) + _arguments \ + ${_common_options[@]} \ + '*:files:_files' + + ;; + (list|ls) + _arguments \ + ${_common_options[@]} \ + "(--help -l)"-l'[Provide more details]' + + ;; + (obliterate) + _arguments \ + ${_common_options[@]} \ + "(--help --force)"--force'[Suppress confirmation prompt]' \ + '*:IDs:' + + ;; + (tickets) + _arguments \ + ${_common_options[@]} \ + '*:ticket names:' + + ;; + (undo) + _arguments \ + ${_common_options[@]} \ + '1:ID:' + + ;; + (wiki) + _arguments \ + ${_common_options[@]} \ + '*:wiki pages:($(__fossil_wiki_pages))' + + ;; + *) + _message 'no more arguments' + + ;; + esac + ;; + esac + return 0 +} + +######################################## +# fossil remote # +######################################## +function __fossil_remote() { + local curcontext="$curcontext" state line + typeset -A opt_args + local -a _common_options + + _common_options=( + "(-)"--help'[Show help and exit]' + ) + + _arguments -C \ + ${_common_options[@]} \ + '1:subcommand:->subcommand' \ + '*::options:->options' + + case $state in + (subcommand) + _values 'subcommand' \ + add delete list off + + ;; + (options) + case $line[1] in + (add) + _arguments \ + '1:name:' \ + '2:url:__fossil_urls' + + ;; + + (delete) + _arguments \ + '1:name:($(__fossil_remotes))' + + ;; + *) + _message 'no more arguments' + + ;; + esac + ;; + esac + return 0 +} + +######################################## +# fossil stash # +######################################## +function __fossil_stash_ids() { + fossil stash ls | grep '^\s*\d\+:' | awk -F ':' '{print $1}' | sed 's/^ *//' +} + +function __fossil_stash() { + local curcontext="$curcontext" state line + typeset -A opt_args + local -a _common_options + + _common_options=( + "(-)"--help'[Show help and exit]' + ) + + _arguments -C \ + ${_common_options[@]} \ + '1:subcommand:->subcommand' \ + '*::options:->options' + + case $state in + (subcommand) + _values 'subcommand' \ + save snapshot list ls show gshow pop apply goto drop diff gdiff + + ;; + (options) + case $line[1] in + (save|snapshot) + _arguments \ + "(-m --comment)"{-m,--comment}'[MSG Add comment]:comment:' \ + '*:files:_files' + + ;; + (list|ls) + _arguments \ + "(-v --verbose)"{-v,--verbose}'[Show info about individual files]' \ + "(-W --width)"{-W,--width}'[N Set width]:number:' + + ;; + (show|cat|diff|gdiff) + _arguments \ + ${_fossil_diff_options[@]} \ + '1::stash ID:($(__fossil_stash_ids))' + + ;; + (apply|goto) + _arguments \ + '1::stash ID:($(__fossil_stash_ids))' + + ;; + (drop|rm) + _arguments \ + "(-a --all)"{-a,--all}'[Forget the whole stash (CANNOT UNDO!)]' \ + '1::stash ID:($(__fossil_stash_ids))' + + ;; + *) + _message 'no more arguments' + + ;; + esac + ;; + esac + return 0 +} + +######################################## +# fossil tag # +######################################## +function __fossil_tag() { + local curcontext="$curcontext" state line + typeset -A opt_args + local -a _common_options + + _common_options=( + "(-)"--help'[Show help and exit]' + ) + + _arguments -C \ + ${_common_options[@]} \ + '1:subcommand:->subcommand' \ + '*::options:->options' + + case $state in + (subcommand) + _values 'subcommand' \ + add cancel find list ls + + ;; + (options) + case $line[1] in + (add) + _arguments \ + ${_common_options[@]} \ + "(--help --raw)"--raw'[Raw tag name]' \ + "(--help --propagate)"--propagate'[Propagating tag]' \ + "(--help --date-override)"--date-override'[DATETIME Set date and time added]:datetime:' \ + "(--help --user-override)"--user-override'[USER Name USER when adding the tag]:user:($(__fossil_users))' \ + "(--help --dryrun -n)"{--dryrun,-n}'[Display the tag text, but do not]' \ + '1:tag name:($(__fossil_tags))' \ + '2:check-in:' \ + '3::value:' + + ;; + (cancel) + _arguments \ + ${_common_options[@]} \ + "(--help --raw)"--raw'[Raw tag name]' \ + "(--help --date-override)"--date-override'[DATETIME Set date and time deleted]:datetime:' \ + "(--help --user-override)"--user-override'[USER Name USER when deleting the tag]:user:($(__fossil_users))' \ + "(--help --dryrun -n)"{--dryrun,-n}'[Display the control artifact, but do]' \ + '1:tag name:($(__fossil_tags))' \ + '2:check-in:' + + ;; + (find) + _arguments \ + ${_common_options[@]} \ + "(--help --raw)"--raw'[Raw tag name]' \ + "(--help -t --type)"{-t,--type}'[TYPE One of "ci", or "e"]:(ci e)' \ + "(--help -n --limit)"{-n,--limit}'[N Limit to N results]:number:' \ + '1:tag name:($(__fossil_tags))' + + ;; + (list|ls) + _arguments \ + ${_common_options[@]} \ + "(--help --raw)"--raw'[List tags raw names of tags]' \ + "(--help --tagtype)"--tagtype'[TYPE List only tags of type TYPE]:tag type:(ci e)' \ + "(--help -v --inverse)"{-v,--inverse}'[Inverse the meaning of --tagtype TYPE]' \ + '1::check-in:' + + ;; + *) + _message 'no more arguments' + + ;; + esac + ;; + esac + return 0 +} + +######################################## +# fossil ticket # +######################################## +function __fossil_ticket() { + local curcontext="$curcontext" state line + typeset -A opt_args + local -a _common_options + + _common_options=( + "(-)"--help'[Show help and exit]' + ) + + _arguments -C \ + ${_common_options[@]} \ + '1:subcommand:->subcommand' \ + '*::options:->options' + + case $state in + (subcommand) + _values 'subcommand' \ + show list ls set change add history + + ;; + (options) + case $line[1] in + (show) + _arguments \ + ${_common_options[@]} \ + "(--help -l --limit)"{-l,--limit}'[LIMITCHAR]:limit:' \ + "(--help -q --quote)"{-q,--quote}'[]' \ + "(--help -R --repository)"{-R,--repository}'[FILE]:fossils:($(__fossil_repos))' \ + '1:report title/nr:' \ + '2::ticket filter:' + + ;; + (list|ls) + _arguments \ + ':what:(fields reports)' + + ;; + (set|change) + _arguments \ + ${_common_options[@]} \ + "(--help -q --quote)"{-q,--quote}'[]' \ + '1:ticket UUID:' \ + '*:field/value:' + + ;; + (add) + _arguments \ + ${_common_options[@]} \ + "(--help -q --quote)"{-q,--quote}'[]' \ + '*:field/value:' + + ;; + (history) + _arguments \ + ':ticket UUID:' + + ;; + *) + _message 'no more arguments' + + ;; + esac + ;; + esac + return 0 +} + +######################################## +# fossil tls-config # +######################################## +function __fossil_tls_config() { + local curcontext="$curcontext" state line + typeset -A opt_args + local -a _common_options + + _common_options=( + "(-)"--help'[Show help and exit]' + ) + + _arguments -C \ + ${_common_options[@]} \ + '1:subcommand:->subcommand' \ + '*::options:->options' + + case $state in + (subcommand) + _values 'subcommand' \ + show remove-exception + + ;; + (options) + case $line[1] in + (remove-exception) + _arguments \ + '*:domains:' + + ;; + *) + _message 'no more arguments' + + ;; + esac + ;; + esac + return 0 +} + +######################################## +# fossil unversioned/uv # +######################################## +function __fossil_unversioned() { + local curcontext="$curcontext" state line + typeset -A opt_args + local -a _common_options + + _common_options=( + "(--help -R --repository)"{-R,--repository}'[FILE Use FILE as the repository]:fossils:($(__fossil_repos))' + "(--help --mtime)"--mtime'[TIMESTAMP Use TIMESTAMP instead of "now"]:timestamp:' + "(-)"--help'[Show help and exit]' + ) + + _arguments -C \ + ${_common_options[@]} \ + '1:subcommand:->subcommand' \ + '*::options:->options' + + case $state in + (subcommand) + _values 'subcommand' \ + add cat edit export list ls revert remove rm delete sync touch + + ;; + (options) + case $line[1] in + (add) + _arguments \ + ${_common_options[@]} \ + "(--help --as)"--as'[UVFILE]:file:_files' \ + '*:files:_files' + + ;; + (cat) + _arguments \ + ${_common_options[@]} \ + '*:files:_files' + + ;; + (edit) + _arguments \ + ${_common_options[@]} \ + '1:file:_files' + + ;; + (export) + _arguments \ + ${_common_options[@]} \ + '1:file:_files' \ + '2:output file:_files' + + ;; + (list|ls) + _arguments \ + ${_common_options[@]} \ + "(--help --glob)"--glob'[PATTERN Show only files that match]:pattern:' \ + "(--help --like)"--like'[PATTERN Show only files that match]:pattern:' + + ;; + (revert) + _arguments \ + ${_common_options[@]} \ + "(--help -v --verbose)"{-v,--verbose}'[Extra diagnostic output]' \ + "(--help -n --dryrun)"{-n,--dryrun}'[Show what would have happened]' \ + '1::url:__fossil_urls' + + ;; + (remove|rm|delete) + _arguments \ + ${_common_options[@]} \ + "(--help --glob)"--glob'[PATTERN Remove files that match]:pattern:' \ + "(--help --like)"--like'[PATTERN Remove files that match]:pattern:' \ + '*:files:_files' + + ;; + (sync) + _arguments \ + ${_common_options[@]} \ + "(--help -v --verbose)"{-v,--verbose}'[Extra diagnostic output]' \ + "(--help -n --dryrun)"{-n,--dryrun}'[Show what would have happened]' \ + '1::url:__fossil_urls' + + ;; + (touch) + _arguments \ + ${_common_options[@]} \ + '*:files:_files' + + ;; + *) + _message 'no more arguments' + + ;; + esac + ;; + esac + return 0 +} + +######################################## +# fossil user # +######################################## +function __fossil_user() { + local curcontext="$curcontext" state line + typeset -a opt_args + local -a _common_options + + _common_options=( + "(--help -R --repository)"{-R,--repository}'[FILE Apply command to repository FILE]:fossils:($(__fossil_repos))' + "(-)"--help'[show help and exit]' + ) + + _arguments -c \ + ${_common_options[@]} \ + '1:subcommand:->subcommand' \ + '*::options:->options' + + case $state in + (subcommand) + _values 'subcommand' \ + capabilities default list ls new password + + ;; + (options) + case $line[1] in + (capabilities) + _arguments \ + ${_common_options[@]} \ + '1:user:($(__fossil_users))' \ + '2::string:' + + ;; + (default) + _arguments \ + ${_common_options[@]} \ + '1::user:($(__fossil_users))' + + ;; + (list|ls) + _arguments \ + ${_common_options[@]} + + ;; + (new) + _arguments \ + ${_common_options[@]} \ + '1::username:' \ + '2::contact info:' \ + '3::password:' + + ;; + (password) + _arguments \ + ${_common_options[@]} \ + '1:user:($(__fossil_users))' \ + '2::password:' + + ;; + *) + _message 'no more arguments' + + ;; + esac + ;; + esac + return 0 +} + +######################################## +# fossil wiki # +######################################## +function __fossil_wiki() { + local curcontext="$curcontext" state line + typeset -a opt_args + local -a _common_options + + _common_options=( + "(-)"--help'[show help and exit]' + ) + + _arguments -c \ + ${_common_options[@]} \ + '1:subcommand:->subcommand' \ + '*::options:->options' + + case $state in + (subcommand) + _values 'subcommand' \ + export create commit list + + ;; + (options) + case $line[1] in + (export) + _arguments \ + ${_common_options[@]} \ + "(--help --technote -t)"{--technote,-t}'[DATETIME|TECHNOTE-ID Export a technote]:datetime/technote-id:(now)' \ + "(--help -h --html -H --HTML)"--html'[Render only HTML body]' \ + "(--help -H --HTML -h --html)"--HTML'[Like -h|-html but wraps the output in <html>/</html>]' \ + "(--help -p --pre)"{-p,--pre}'[Wrap into <pre>...</pre>]' \ + '::pagename:($(__fossil_wiki_pages))' \ + '::file:_files' + + ;; + (create|commit) + _arguments \ + ${_common_options[@]} \ + "(--help -M --mimetype)"{-M,--mimetype}'[TEXT-FORMAT The mime type of the update]:mimetype:' \ + "(--help --technote -t)"{--technote,-t}'[DATETIME|TECHNOTE-ID]:datetime/technote-id:(now)' \ + "(--help --technote-tags)"--technote-tags'[TAGS The set of tags for a technote]:tags:' \ + "(--help --technote-bgcolor)"--technote-bgcolor'[COLOR The color used for the technote]:color:' \ + '1:pagename:($(__fossil_wiki_pages))' \ + '2::file:_files' + + ;; + (list|ls) + _arguments \ + ${_common_options[@]} \ + "(--help -t --technote)"{-t,--technote}'[List technotes]' \ + "(--help -s --show-technote-ids)"{-s,--show-technote-ids}'[The id of the tech note will be listed]' + + ;; + *) + _message 'no more arguments' + + ;; + esac + ;; + esac + return 0 +} + + +######################################## +# fossil test commands # +######################################## + +function __fossil_complete_test_commands() { + case $line[1] in + (test-add-alerts) + _arguments \ + "(--help --backoffice)"--backoffice'[Run alert_backoffice() after all alerts have been added]' \ + "(--help --debug)"--debug'[Like --backoffice, but print to stdout]' \ + "(--help --digest)"--digest'[Process emails using SENDALERT_DIGEST]' \ + '(- *)'--help'[Show help and exit]' \ + '*:event IDs:' + + ;; + (test-agg-cksum) + _arguments \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-alert) + _arguments \ + "(--help --digest)"--digest'[Generate digest alert text]' \ + "(--help --needmod)"--needmod'[Assume all events are pending moderator approval]' \ + '(- *)'--help'[Show help and exit]' \ + '*:event IDs:' + + ;; + (test-all-help) + _arguments \ + "(--help -e --everything)"{-e,--everything}'[Show all commands and pages]' \ + "(--help -t --test)"{-t,--test}'[Include test- commands]' \ + "(--help -w --www)"{-w,--www}'[Show WWW pages]' \ + "(--help -s --settings)"{-s,--settings}'[Show settings]' \ + "(--help -h --html)"{-h,--html}'[Transform output to HTML]' \ + "(--help -r --raw)"{-r,--raw}'[No output formatting]' \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-ambiguous) + _arguments \ + "(--help --minsize)"--minsize'[N Show hases with N characters or more]:number:' \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-ancestor-path) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1:version 1:' \ + '2:version 2:' + + ;; + (test-approx-match) + _arguments \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-backlinks) + _arguments \ + "(--help --mtime)"--mtime'[DATETIME Use an alternative date/time]:datetime:' \ + "(--help --mimetype)"--mimetype'[TYPE Use an alternative mimetype]:mimetype:' \ + '(- *)'--help'[Show help and exit]' \ + '1:srctype:' \ + '2:srcid:' \ + '3:input file:_files' + + ;; + (test-backoffice-lease) + _arguments \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-builtin-get) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1:name:' \ + '2::output file:_files' + + ;; + (test-builtin-list) + _arguments \ + "(--help --verbose)"--verbose'[Output total item count and size]' \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-canonical-name) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '*:files:_files' + + ;; + (test-captcha) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '*:numbers:' + + ;; + (test-ci-mini) + _arguments \ + "(--help --repository -R)"{--repository,-R}'[REPO The repository file to commit to]:fossils:($(__fossil_repos))' \ + "(--help --as)"--as'[FILENAME The repository-side name of the input file]:file:_files' \ + "(--help --comment -m)"{--comment,-m}'[COMMENT Required checkin comment]:comment:' \ + "(--help --comment-file -M)"{--comment-file,-M}'[FILE Reads checkin comment from the given file]:file:_files' \ + "(--help --revision -r)"{--revision,-r}'[VERSION Commit from this version]:version:' \ + "(--help --allow-fork)"--allow-fork'[Allow commit against a non-leaf parent]' \ + "(--help --allow-merge-conflict)"--allow-merge-conflict'[Allow checkin of a file with conflict markers]' \ + "(--help --user-override)"--user-override'[USER User to use instead of the default]:user:($(__fossil_users))' \ + "(--help --date-override)"--date-override'[DATETIME Date to use instead of '"'"'now'"'"']:datetime:(now)' \ + "(--help --allow-older)"--allow-older'[Allow a commit to be older than its ancestor]' \ + "(--help --convert-eol-inherit)"--convert-eol-inherit'[Inherit EOL style from previous content]' \ + "(--help --convert-eol-unix)"--convert-eol-unix'[Convert the EOL style to Unix]' \ + "(--help --convert-eol-windows)"--convert-eol-windows'[Convert the EOL style to Windows]' \ + "(--help --delta)"--delta'[Prefer to generate a delta manifest]' \ + "(--help --allow-new-file)"--allow-new-file'[Allow addition of a new file this way]' \ + "(--help --dump-manifest -d)"{--dump-manifest,-d}'[Dumps the generated manifest to stdout]' \ + "(--help --save-manifest)"--save-manifest'[FILE Saves the generated manifest to a file]:file:_files' \ + "(--help --wet-run)"--wet-run'[Disables the default dry-run mode]' \ + '(- *)'--help'[Show help and exit]' \ + '1:file:_files' + + ;; + (test-clusters) + _arguments \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-command-stats) + _arguments \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-comment-format) + _arguments \ + "(--help --file)"--file'[The comment text is really just a file name to read it from]' \ + "(--help --decode)"--decode'[Decode the text using the same method used for a C-card from a manifest]' \ + "(--help --legacy)"--legacy'[Use the legacy comment printing algorithm]' \ + "(--help --trimcrlf)"--trimcrlf'[Enable trimming of leading/trailing CR/LF]' \ + "(--help --trimspace)"--trimspace'[Enable trimming of leading/trailing spaces]' \ + "(--help --wordbreak)"--wordbreak'[Attempt to break lines on word boundaries]' \ + "(--help --origbreak)"--origbreak'[Attempt to break when the original comment text is detected]' \ + "(--help --indent)"--indent'[NUM Number of spaces to indent]:number:' \ + "(--help -W --width)"{-W,--width}'[NUM Width of lines]:number:' \ + '(- *)'--help'[Show help and exit]' \ + '1:prefix:' \ + '2:text:' \ + '3::origtext:' + + ;; + (test-commit-warning) + _arguments \ + "(--help --no-settings)"--no-settings'[Do not consider any glob settings]' \ + "(--help -v --verbose)"{-v,--verbose}'[Show per-file results for all pre-commit checks]' \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-compress) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1:input file:_files' \ + '2:output file:_files' + + ;; + (test-compress-2) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1:input file 1:_files' \ + '2:input file 2:_files' \ + '3:output file:_files' + + ;; + (test-contains-selector) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1:file:_files' \ + '2:css selector:' + + ;; + (test-content-deltify) + _arguments \ + "(--help --force)"--force'[]' \ + '(- *)'--help'[Show help and exit]' \ + '1:rid:' \ + '*:src id:' + + ;; + (test-content-erase) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '*:rid:' + + ;; + (test-content-put) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1:file:_files' + + ;; + (test-content-rawget) + _arguments \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-content-undelta) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1:record id:' + + ;; + (test-convert-stext) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1:file:_files' \ + '2:mimetype:' + + ;; + (test-create-clusters) + _arguments \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-crosslink) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1:record id:' + + ;; + (test-cycle-compress) + _arguments \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-database-names) + _arguments \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-date-format) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '*:date string:' + + ;; + (test-db-exec-error) + _arguments \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-decode-email) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1:file:_files' + + ;; + (test-decode64) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1:string:' + + ;; + (test-delta) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1:file:_files' \ + '2:file:_files' + + ;; + (test-delta-analyze) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1:file:_files' \ + '2:file:_files' + + ;; + (test-delta-apply) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1:file:_files' \ + '2:delta:_files' + + ;; + (test-delta-create) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1:file:_files' \ + '2:file:_files' \ + '3:delta:_files' + + ;; + (test-describe-artifacts) + _arguments \ + "(--help --from)"--from'[S An artifact]:artifact:' \ + "(--help --count)"--count'[N Number of artifacts]:number:' \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-detach) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1::fossils:($(__fossil_repos))' + + ;; + (test-diff) + _arguments \ + ${_fossil_diff_options[@]} \ + '1:file 1:_files' \ + '2:file 2:_files' + + ;; + (test-dir-size) + _arguments \ + "(--help --nodots)"--nodots'[Omit files that begin with '"'"'.'"'"']' \ + '(- *)'--help'[Show help and exit]' \ + '1:directory:_files -/' \ + '2::glob pattern:' + + ;; + (test-echo) + _arguments \ + "(--help --hex)"--hex'[Show the output as hexadecimal]' \ + '(- *)'--help'[Show help and exit]' \ + '*:args:' + + ;; + (test-emailblob-refcheck) + _arguments \ + "(--help --repair)"--repair'[Fix up the enref field]' \ + "(--help --full)"--full'[Give a full report]' \ + "(--help --clean)"--clean'[Used with --repair, removes entries with enref==0]' \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-encode64) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1:string:' + + ;; + (test-escaped-arg) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '*:args:' + + ;; + (test-etag) + _arguments \ + "(--help --key)"--key'[KEYNUM]:key number:' \ + "(--help --hash)"--hash'[HASH]:hash:' \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-file-copy) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1:source:_files' \ + '2:destination:_files' + + ;; + (test-file-environment) + _arguments \ + "(--help --allow-symlinks)"--allow-symlinks'[BOOL Temporarily turn allow-symlinks on/off]:bool:(yes no)' \ + "(--help --open-config)"--open-config'[Open the configuration database first]' \ + "(--help --slash)"--slash'[Trailing slashes, if any, are retaine]' \ + "(--help --reset)"--reset'[Reset cached stat() info for each file]' \ + '(- *)'--help'[Show help and exit]' \ + '*:files:_files' + + ;; + (test-fileage) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1:checkin:' + + ;; + (test-filezip) + _arguments \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-find-mx) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '*:domain:' + + ;; + (test-find-pivot) + _arguments \ + "(--help --ignore-merges)"--ignore-merges'[Ignore merges for discovering name pivots]' \ + '(- *)'--help'[Show help and exit]' \ + '*:args:' + + ;; + (test-fingerprint) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1::rcvid:' + + ;; + (test-forumthread) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1:thread id:' + + ;; + (test-fossil-system) + _arguments \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-fuzz) + _arguments \ + "(--help --type)"--type'[TYPE]:type:(wiki markdown artifact)' \ + '(- *)'--help'[Show help and exit]' \ + '*:files:_files' + + ;; + (test-glob) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1:pattern:' \ + '2:string:' + + ;; + (test-grep) + _arguments \ + "(--help -i --ignore-case)"{-i,--ignore-case}'[Ignore case]' \ + '(- *)'--help'[Show help and exit]' \ + '1:regexp:' \ + '*:files:_files' + + ;; + (test-gzip) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1:file:_files' + + ;; + (test-hash-color) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '*:tags:($(__fossil_tags))' + + ;; + (test-hash-passwords) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1:fossils:($(__fossil_repos))' + + ;; + (test-html-tidy) + _arguments \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-html-to-text) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '*:files:_files' + + ;; + (test-html-tokenize) + _arguments \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-http) + _arguments \ + "(--help --th-trace)"--th-trace'[Trace TH1 execution (for debugging purposes)]' \ + "(--help --usercap)"--usercap'[CAP user capability string]:capability string:' \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-httpmsg) + _arguments \ + "(--help --compress)"--compress'[Use ZLIB compression on the payload]' \ + "(--help --mimetype)"--mimetype'[TYPE Mimetype of the payload]:mimetype:' \ + "(--help --out)"--out'[FILE Store the reply in FILE]:file:_files' \ + "(--help -v)"-v'[Verbose output]' \ + '(- *)'--help'[Show help and exit]' \ + '1:url:__fossil_urls' \ + '2::payload:' + + ;; + (test-integrity) + _arguments \ + "(--help -d --db-only)"{-d,--db-only}'[Run "PRAGMA integrity_check" on the database only]' \ + "(--help --parse)"--parse'[Parse all manifests, wikis, tickets, events, etc]' \ + "(--help -q --quick)"{-q,--quick}'[Run "PRAGMA quick_check" on the database only]' \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-is-reserved-name|test-is-ckout-db) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '*:files:_files' + + ;; + (test-ishuman) + _arguments \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-isspace) + _arguments \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-leaf-ambiguity) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '*:names:' + + ;; + (test-list-page) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1:directory:_files -/' + + ;; + (test-list-webpage) + _arguments \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-loadavg) + _arguments \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-looks-like-utf) + _arguments \ + "(--help -n --limit)"{-n,--limit}'[NUM Repeat looks-like function NUM times]:number:' \ + "(--help --utf8)"--utf8'[Ignoring BOM and file size, force UTF-8 checking]' \ + "(--help --utf16)"--utf16'[Ignoring BOM and file size, force UTF-16 checking]' \ + '(- *)'--help'[Show help and exit]' \ + '1:file:_files' + + ;; + (test-mailbox-hashname) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '*:human name:' + + ;; + (test-markdown-render) + _arguments \ + "(--help --safe)"--safe'[Restrict the output to use only "safe" HTML]' \ + '(- *)'--help'[Show help and exit]' \ + '*:files:_files' + + ;; + (test-match) + _arguments \ + "(--help --begin)"--begin'[TEXT Text to insert before each match]:text:' \ + "(--help --end)"--end'[TEXT Text to insert after each match]:text:' \ + "(--help --gap)"--gap'[TEXT Text to indicate elided content]:text:' \ + "(--help --html)"--html'[Input is HTML]' \ + "(--help --static)"--static'[Use the static Search object]' \ + '(- *)'--help'[Show help and exit]' \ + '1:search string:' \ + '*:files:_files' + + ;; + (test-mimetype) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '*:files:_files' + + ;; + (test-missing) + _arguments \ + "(--help --notshunned)"--notshunned'[Do not report shunned artifacts]' \ + "(--help --quiet)"--quiet'[Only show output if there are errors]' \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-move-repository) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1:pathname:_files' + + ;; + (test-name-changes) + _arguments \ + "(--help --debug)"--debug'[Enable debugging]' \ + '(- *)'--help'[Show help and exit]' \ + '1:version 1:' \ + '2:version 2:' + + ;; + (test-name-to-id) + _arguments \ + "(--help --count)"--count'[N Repeat the conversion N times]:number:' \ + '(- *)'--help'[Show help and exit]' \ + '*:name:' + + ;; + (test-obscure) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '*:args:' + + ;; + (test-orphans) + _arguments \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-parse-all-blobs) + _arguments \ + "(--help --limit)"--limit'[N Parse no more than N blobs]:number:' \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-parse-manifest) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1:file:_files' \ + '2::n:' + + ;; + (test-phantoms) + _arguments \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-process-id) + _arguments \ + "(--help --sleep)"--sleep'[N Sleep for N seconds before exiting]:number:' \ + '(- *)'--help'[Show help and exit]' \ + '*:process id:' + + ;; + (test-prompt-password) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1:prompt:' \ + '2:verify:(0 1 2)' + + ;; + (test-prompt-user) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1:prompt:' + + ;; + (test-random-password) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1::length:' + + ;; + (test-rawdiff) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1:file 1:_files' \ + '2:file 2:_files' + + ;; + (test-relative-name) + _arguments \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-reserved-names) + _arguments \ + "(--help -omitrepo)"-omitrepo'[]' \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-safe-html) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '*:files:_files' + + ;; + (test-sanitize-name) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '*:args:' + + ;; + (test-search-stext) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1:type:(c d e t w)' \ + '2:rid:' \ + '3:name:' + + ;; + (test-set-mtime) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1:file:_files' \ + '2:date/time:' + + ;; + (test-shortest-path) + _arguments \ + "(--help --no-merge)"--no-merge'[Follow only direct parent-child paths]' \ + '(- *)'--help'[Show help and exit]' \ + '1:version 1:' \ + '2:version 2:' + + ;; + (test-simplify-name) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '*:files:_files' + + ;; + (test-smtp-probe) + _arguments \ + "(--help --direct)"--direct'[Use DOMAIN directly without going through MX]' \ + "(--help --port)"--port'[N Talk on TCP port N]:number:' \ + '(- *)'--help'[Show help and exit]' \ + '1:domain:' \ + '2::me:' + + ;; + (test-smtp-send) + _arguments \ + "(--help --direct)"--direct'[Bypass MX lookup]' \ + "(--help --relayhost)"--relayhost'[HOST Use HOST as relay for delivery]:host:' \ + "(--help --port)"--port'[Use TCP port N instead of 25]:number:' \ + "(--help --trace)"--trace'[Show the SMTP conversation on the console]' \ + '(- *)'--help'[Show help and exit]' \ + '1:email:_files' \ + '2:from:' \ + '*:to:' + + ;; + (test-smtp-senddata) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1:file:_files' + + ;; + (test-subtree) + _arguments \ + "(--help --branch)"--branch'[BRANCH Include only check-ins on BRANCH]:branch:($(__fossil_branches))' \ + "(--help --from)"--from'[TAG Start the subtree at TAG]:tag:($(__fossil_tags))' \ + "(--help --to)"--to'[TAG End the subtree at TAG]:tag:($(__fossil_tags))' \ + "(--help --checkin)"--checkin'[TAG The subtree is the single check-in TAG]:tag:($(__fossil_tags))' \ + "(--help --all)"--all'[Include FILE and TAG artifacts]' \ + "(--help --exclusive)"--exclusive'[Include FILES exclusively on check-ins]' \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-tag) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1:tag:($(__fossil_tags))' \ + '2:artifact id:' \ + '3::value:' + + ;; + (test-tarball) + _arguments \ + "(--help -h --dereference)"{-h,--dereference}'[Follow symlinks; archive the files they point to]' \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-tempname) + _arguments \ + "(--help --time)"--time'[SUFFIX Generate names based on the time of the day]:suffix:' \ + "(--help --tag)"--tag'[NAME Try to use NAME as the differentiator]:name:' \ + '(- *)'--help'[Show help and exit]' \ + '*:basename:' + + ;; + (test-terminal-size) + _arguments \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-th-eval) + _arguments \ + "(--help --cgi)"--cgi'[Include a CGI response header in the output]' \ + "(--help --http)"--http'[Include an HTTP response header in the output]' \ + "(--help --open-config)"--open-config'[Open the configuration database]' \ + "(--help --set-anon-caps)"--set-anon-caps'[Set anonymous login capabilities]' \ + "(--help --set-user-caps)"--set-user-caps'[Set user login capabilities]' \ + "(--help --th-trace)"--th-trace'[Trace TH1 execution]' \ + '(- *)'--help'[Show help and exit]' \ + '1:script:_files' + + ;; + (test-th-render) + _arguments \ + "(--help --cgi)"--cgi'[Include a CGI response header in the output]' \ + "(--help --http)"--http'[Include an HTTP response header in the output]' \ + "(--help --open-config)"--open-config'[Open the configuration database]' \ + "(--help --set-anon-caps)"--set-anon-caps'[Set anonymous login capabilities]' \ + "(--help --set-user-caps)"--set-user-caps'[Set user login capabilities]' \ + "(--help --th-trace)"--th-trace'[Trace TH1 execution]' \ + '(- *)'--help'[Show help and exit]' \ + '1:file:_files' + + ;; + (test-th-source) + _arguments \ + "(--help --cgi)"--cgi'[Include a CGI response header in the output]' \ + "(--help --http)"--http'[Include an HTTP response header in the output]' \ + "(--help --open-config)"--open-config'[Open the configuration database]' \ + "(--help --set-anon-caps)"--set-anon-caps'[Set anonymous login capabilities]' \ + "(--help --set-user-caps)"--set-user-caps'[Set user login capabilities]' \ + "(--help --th-trace)"--th-trace'[Trace TH1 execution]' \ + '(- *)'--help'[Show help and exit]' \ + '1:file:_files' + + ;; + (test-ticket-rebuild) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1:ticket id:(all)' + + ;; + (test-timespan) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1:timestamp:' + + ;; + (test-timewarp-list) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '*:files:_files' + + ;; + (test-topological-sort) + _arguments \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-tree-name) + _arguments \ + "(--help --absolute)"--absolute'[Return an absolute path instead of a relative one]' \ + "(--help --case-sensitive)"--case-sensitive'[BOOL Enable or disable case-sensitive filenames]:bool:(yes no)' \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-unclustered) + _arguments \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-uncompress) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1:in:_files' \ + '2:out:_files' + + ;; + (test-unsent) + _arguments \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-urlparser) + _arguments \ + "(--help --remember)"--remember'[Store results in last-sync-url]' \ + "(--help --prompt-pw)"--prompt-pw'[Prompt for password if missing]' \ + '(- *)'--help'[Show help and exit]' \ + '1:url:__fossil_urls' + + ;; + (test-usernames) + _arguments \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-valid-for-windows) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '*:files:_files' + + ;; + (test-var-get) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1:var:' \ + '2::file:_files' + + ;; + (test-var-list) + _arguments \ + "(--help --unset)"--unset'[Delete entries instead of displaying them]' \ + "(--help --mtime)"--mtime'[Show last modification time]' \ + '(- *)'--help'[Show help and exit]' \ + '1::pattern:' + + ;; + (test-var-set) + _arguments \ + "(--help --blob --file)"--blob'[FILE Binary file to read from]:file:_files' \ + "(--help --file --blob)"--file'[FILE File to read from]:file:_files' \ + '(- *)'--help'[Show help and exit]' \ + '1:var:' \ + '2::value:' + + ;; + (test-verify-all) + _arguments \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-whatis-all) + _arguments \ + '(- *)'--help'[Show help and exit]' + + ;; + (test-which) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '*:executable name:' + + ;; + (test-wiki-relink) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1:wiki page:($(__fossil_wiki_pages))' + + ;; + (test-wiki-render) + _arguments \ + "(--help --buttons)"--buttons'[Set the WIKI_BUTTONS flag]' \ + "(--help --htmlonly)"--htmlonly'[Set the WIKI_HTMLONLY flag]' \ + "(--help --linksonly)"--linksonly'[Set the WIKI_LINKSONLY flag]' \ + "(--help --nobadlinks)"--nobadlinks'[Set the WIKI_NOBADLINKS flag]' \ + "(--help --inline)"--inline'[Set the WIKI_INLINE flag]' \ + "(--help --noblock)"--noblock'[Set the WIKI_NOBLOCK flag]' \ + '(- *)'--help'[Show help and exit]' \ + '1:file:_files' + + ;; + (test-without-rowid) + _arguments \ + "(--help -n --dryrun)"{-n,--dryrun}'[Just print what would happen]' \ + '(- *)'--help'[Show help and exit]' \ + '*:files:_files' + + ;; + (test-xfer) + _arguments \ + '(- *)'--help'[Show help and exit]' \ + '1:xfer message:' + + ;; + esac +} + +################################################################################ +_fossil + diff --git a/tools/fossil-diff-log b/tools/fossil-diff-log new file mode 100755 index 0000000..2a2af5f --- /dev/null +++ b/tools/fossil-diff-log @@ -0,0 +1,72 @@ +#!/usr/bin/env perl +# Fossil emulation of the "git log --patch / -p" feature: emit a stream +# of diffs from one version to the next for each file named on the +# command line. +# +# LIMITATIONS: It does not assume "all files" if you give no args, and +# it cannot take a directory to mean "all files under this parent". +# +# PREREQUISITES: This script needs several CPAN modules to run properly. +# There are multiple methods to install them: +# +# sudo dnf install perl-File-Which perl-IO-Interactive +# sudo apt install libfile-which-perl libio-interactive-perl +# sudo cpanm File::Which IO::Interactive +# ...etc... + +use strict; +use warnings; + +use Carp; +use File::Which; +use IO::Interactive qw(is_interactive); + +die "usage: $0 <files...>\n\n" unless @ARGV; + +my $out; +if (is_interactive()) { + my $pager = $ENV{PAGER} || which('less') || which('more'); + open $out, '|-', $pager or croak "Cannot pipe to $pager: $!"; +} +else { + $out = *STDOUT; +} + +open my $bcmd, '-|', 'fossil branch current' + or die "Cannot get branch: $!\n"; +my $cbranch = <$bcmd>; +chomp $cbranch; +close $bcmd; + +for my $file (@ARGV) { + my $lastckid; + open my $finfo, '-|', "fossil finfo --brief --limit 0 '$file'" + or die "Failed to get file info: $!\n"; + my @filines = <$finfo>; + close $finfo; + + for my $line (@filines) { + my ($currckid, $date, $user, $branch, @cwords) = split ' ', $line; + next unless $branch eq $cbranch; + if (defined $lastckid and defined $branch) { + my $comment = join ' ', @cwords; + open my $diff, '-|', 'fossil', 'diff', $file, + '--from', $currckid, + '--to', $lastckid, + or die "Failed to diff $currckid -> $lastckid: $!\n"; + my @dl = <$diff>; + close $diff; + my $patch = join '', @dl; + + print $out <<"OUT" +Checkin ID $currckid to $branch by $user on $date +Comment: $comment + +$patch + +OUT + } + + $lastckid = $currckid; + } +} diff --git a/tools/fossil-stress.tcl b/tools/fossil-stress.tcl new file mode 100755 index 0000000..cdda826 --- /dev/null +++ b/tools/fossil-stress.tcl @@ -0,0 +1,144 @@ +#!/usr/bin/tclsh +# +# Run this script, giving the url of a Fossil server instances as the +# argument, and this script will start sending HTTP requests into the +# that server instance as fast as it can, as a stress test for the +# server implementation. +# +set nthread 10 +for {set i 0} {$i<[llength $argv]} {incr i} { + set x [lindex $argv $i] + if {[regexp {^--[a-z]} $x]} { + set x [string range $x 1 end] + } + if {$x=="-threads"} { + incr i + set nthread [lindex $argv $i] + } elseif {[string index $x 0]=="-"} { + error "unknown option \"$x\"" + } elseif {[info exists url]} { + error "unknown argument \"$x\"" + } else { + set url $x + } +} +if {![info exists url]} { + error "Usage: $argv0 [-threads N] URL" +} +if {![regexp {^https?://([^/:]+)(:\d+)?(/.*)$} $url all domain port path]} { + error "could not parse the URL [list $url] -- should be of the\ + form \"http://domain/path\"" +} +set useragent {Mozilla/5.0 (fossil-stress.tcl) Gecko/20100101 Firefox/57.0} +set path [string trimright $path /] +set port [string trimleft $port :] +if {$port==""} {set port 80} + +proc send_one_request {tid domain port path} { + while {[catch { + set x [socket $domain $port] + fconfigure $x -translation binary -blocking 0 + puts $x "GET $path HTTP/1.0\r" + if {$port==80} { + puts $x "Host: $domain\r" + } else { + puts $x "Host: $domain:$port\r" + } + puts $x "User-Agent: $::useragent\r" + puts $x "Accept: text/html,q=0.9,*/*;q=0.8\r" + puts $x "Accept-Language: en-US,en;q=0.5\r" + puts $x "Connection: close\r" + puts $x "\r" + } msg]} { + puts "ERROR: $msg" + after 1000 + } + global cnt stime threadid + set cnt($x) 0 + set stime($x) [clock seconds] + set threadid($x) $tid + flush $x + fileevent $x readable [list get_reply $tid $path $x] +} + +proc close_connection {x} { + global cnt stime tid + close $x + unset -nocomplain cnt($x) + unset -nocomplain stime($x) + unset -nocomplain threadid($x) +} + +proc get_reply {tid info x} { + global cnt + if {[eof $x]} { + puts "[format %3d: $tid] $info ($cnt($x) bytes)" + flush stdout + close_connection $x + start_another_request $tid + } else { + incr cnt($x) [string length [read $x]] + } +} + +set pages { + /timeline?n=20 + /timeline?n=20&a=1970-01-01 + /home + /brlist + /info/trunk + /info/2015-01-01 + /vdiff?from=2015-01-01&to=trunk&diff=0 + /wcontent + /fileage + /dir + /tree + /uvlist + /stat + /test-env + /sitemap + /hash-collisions + /artifact_stats + /bloblist + /bigbloblist + /wiki_rules + /md_rules + /help + /test-all-help + /timewarps + /taglist +} + +set pageidx 0 +proc start_another_request {tid} { + global pages pageidx domain port path + set p [lindex $pages $pageidx] + incr pageidx + if {$pageidx>=[llength $pages]} {set pageidx 0} + send_one_request $tid $domain $port $path$p +} + +proc unhang_stalled_threads {} { + global stime threadid + set now [clock seconds] + # puts "checking for stalled threads...." + foreach x [array names stime] { + # puts -nonewline " $threadid($x)=[expr {$now-$stime($x)}]" + if {$stime($x)+0<$now-10} { + set t $threadid($x) + puts "RESTART thread $t" + flush stdout + close_connection $x + start_another_request $t + } + } + # puts "" + flush stdout + after 10000 unhang_stalled_threads +} + +unhang_stalled_threads +for {set i 1} {$i<=$nthread} {incr i} { + start_another_request $i +} +vwait forever diff --git a/tools/fossilwiki b/tools/fossilwiki new file mode 100755 index 0000000..3ee0b4a --- /dev/null +++ b/tools/fossilwiki @@ -0,0 +1,132 @@ +#!/usr/bin/perl +# vim: cin : + +$repofile = shift; +$repocmd = ''; +$repocmd = "-R $repofile" if -f $repofile; +$mainpage = ''; + +@rep = (); +if ( ! -f $repofile ) +{ + @rep = `fossil info | grep 'project-name'`; +} +else +{ + @rep = `fossil info $repofile | grep 'project-name'`; +} + +$mainpage = $rep[0]; +chomp $mainpage; +$mainpage =~ s/^project-name:\s+//; + + +@pages = `fossil wiki list $repocmd`; + +%pages = (); +foreach $page ( @pages ) +{ + chomp $page; + $text = `fossil wiki export "$page" $repocmd`; + $pages{$page} = $text; +} + +@orphans = (); +@nointernals = (); +@terminals = (); +@empties = (); +%badlinks = (); +%alllinks = (); +%links = (); +foreach $page ( keys %pages ) +{ + my @links = (); + my $text = $pages{$page}; + while ( $text =~ m/\[([^][]+)\]/g ) + { + push @links,$1; + } + + $numlinks = $#links; + + if (@links == ()) + { + push @terminals, $page; + } + else + { + my @internals = grep { $_ !~ /(http:)|(mailto:)|(https:)/ } @links; + if (@internals == ()) + { + push @nointernals, $page; + } + else + { + @{$links{$page}{'links'}} = map {my ($a,$b) = split /\|/; $a;} @internals; + foreach $internal ( @internals ) + { + my ($int_link, $display) = split /\|/, $internal; + ${$links{$int_link}{'refs'}}++; + $alllinks{$int_link} = 1; + } + } + } + + if ($text eq '' || $text =~ m/^<i>Empty Page<\/i>/) + { + chomp $tail; + my ($head, $tail) = split /\/i>/ , $text; + if ($tail =~ m/^\s*$/s) { + push @empties, $page; + } + } +} +foreach $page ( keys %links ) +{ + if ($page ne $mainpage && (${$links{$page}{'refs'}} == 0)) + { + push @orphans, $page; + } +} +foreach $link (keys %alllinks ) +{ + if (! exists($pages{$link}) && $link !~ /^\./ && $link !~ /^\//) + { + $badlinks{$link} = 1; + } +} +foreach $empty ( @empties ) +{ + print ("empty: '$empty'\n"); +} +foreach $nointernals ( @nointernals ) +{ + print ("nointernals: '$nointernals'\n"); +} +foreach $terminal ( @terminals ) +{ + print ("terminal: '$terminal'\n"); +} +foreach $orphan ( @orphans ) +{ + print ("orphan: '$orphan'\n"); +} +foreach $link ( keys %badlinks ) +{ + print ("badlink: '$link'\n"); +} +foreach $page ( sort keys %links ) +{ + my @links = @{$links{$page}{'links'}}; + if (@links != ()) + { + if ($page eq $mainpage) + { + print "links: *** '$page' *** -> ", join (", ", @links), "\n"; + } + else + { + print "links: '$page' -> ", join (", ", @links), "\n"; + } + } +} diff --git a/tools/fslsrv b/tools/fslsrv new file mode 100755 index 0000000..b757a18 --- /dev/null +++ b/tools/fslsrv @@ -0,0 +1,59 @@ +#!/bin/bash +FOSSIL=fossil +PGARGS="-P 1" +OLDPID=`pgrep -P 1 fossil` +SITE=https://example.com +PORT=12345 + +if [ "$1" = "-f" ] ; then PGARGS= ; shift ; fi + +if [ -n "$OLDPID" ] +then + echo "Killing running Fossil server first..." + pkill $PGARGS fossil + + for i in $(seq 30) + do + if [ -n "$(pgrep $PGARGS fossil)" ] + then + if [ $i -eq 1 ] + then + echo -n "Waiting for it to die..." + else + echo -n . + fi + sleep '0.1' + else + break + fi + echo + done + + killall -9 fossil 2> /dev/null +fi + +if [ -x ./fossil ] +then + # We're running from a build tree, so use that version instead + FOSSIL=./fossil +fi + +function start_one() { + bn=$1 + ln="$2" + + $FOSSIL server $extra \ + --scgi \ + --localhost \ + --port $PORT \ + --jsmode bundled \ + --baseurl ${SITE}/$bn \ + --errorlog ~/log/fossil/$bn-errors.log \ + ~/museum/$bn.fossil > ~/log/fossil/$bn-stdout.log & + echo Started $ln Fossil server, port $PORT, PID $!. + PORT=$(($PORT + 1)) +} + +start_one first "First Project" +start_one second "Second Project" +start_one third "Third Project" diff --git a/tools/makeheaders.c b/tools/makeheaders.c new file mode 100644 index 0000000..a7c5c2e --- /dev/null +++ b/tools/makeheaders.c @@ -0,0 +1,3441 @@ +/* +** 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".) +** +** Copyright 1993 D. Richard Hipp. All rights reserved. +** +** Redistribution and use in source and binary forms, with or +** without modification, are permitted provided that the following +** conditions are met: +** +** 1. Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** +** 2. Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** +** This software is provided "as is" and any express or implied warranties, +** including, but not limited to, the implied warranties of merchantability +** and fitness for a particular purpose are disclaimed. In no event shall +** the author or contributors be liable for any direct, indirect, incidental, +** special, exemplary, or consequential damages (including, but not limited +** to, procurement of substitute goods or services; loss of use, data or +** profits; or business interruption) however caused and on any theory of +** liability, whether in contract, strict liability, or tort (including +** negligence or otherwise) arising in any way out of the use of this +** software, even if advised of the possibility of such damage. +** +** 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. +*/ +#include <stdio.h> +#include <stdlib.h> +#include <ctype.h> +#include <memory.h> +#include <sys/stat.h> +#include <assert.h> +#include <string.h> + +#if defined( __MINGW32__) || defined(__DMC__) || \ + defined(_MSC_VER) || defined(__POCC__) +# ifndef WIN32 +# define WIN32 +# endif +#else +# include <unistd.h> +#endif + +/* +** Macros for debugging. +*/ +#ifdef DEBUG +static int debugMask = 0; +# define debug0(F,M) if( (F)&debugMask ){ fprintf(stderr,M); } +# define debug1(F,M,A) if( (F)&debugMask ){ fprintf(stderr,M,A); } +# define debug2(F,M,A,B) if( (F)&debugMask ){ fprintf(stderr,M,A,B); } +# define debug3(F,M,A,B,C) if( (F)&debugMask ){ fprintf(stderr,M,A,B,C); } +# define PARSER 0x00000001 +# define DECL_DUMP 0x00000002 +# define TOKENIZER 0x00000004 +#else +# define debug0(Flags, Format) +# define debug1(Flags, Format, A) +# define debug2(Flags, Format, A, B) +# define debug3(Flags, Format, A, B, C) +#endif + +/* +** The following macros are purely for the purpose of testing this +** program on itself. They don't really contribute to the code. +*/ +#define INTERFACE 1 +#define EXPORT_INTERFACE 1 +#define EXPORT + +/* +** Each token in a source file is represented by an instance of +** the following structure. Tokens are collected onto a list. +*/ +typedef struct Token Token; +struct Token { + const char *zText; /* The text of the token */ + int nText; /* Number of characters in the token's text */ + int eType; /* The type of this token */ + int nLine; /* The line number on which the token starts */ + Token *pComment; /* Most recent block comment before this token */ + Token *pNext; /* Next token on the list */ + Token *pPrev; /* Previous token on the list */ +}; + +/* +** During tokenization, information about the state of the input +** stream is held in an instance of the following structure +*/ +typedef struct InStream InStream; +struct InStream { + const char *z; /* Complete text of the input */ + int i; /* Next character to read from the input */ + int nLine; /* The line number for character z[i] */ +}; + +/* +** Each declaration in the C or C++ source files is parsed out and stored as +** an instance of the following structure. +** +** A "forward declaration" is a declaration that an object exists that +** doesn't tell about the objects structure. A typical forward declaration +** is: +** +** struct Xyzzy; +** +** Not every object has a forward declaration. If it does, thought, the +** forward declaration will be contained in the zFwd field for C and +** the zFwdCpp for C++. The zDecl field contains the complete +** declaration text. +*/ +typedef struct Decl Decl; +struct Decl { + char *zName; /* Name of the object being declared. The appearance + ** of this name is a source file triggers the declaration + ** to be added to the header for that file. */ + const char *zFile; /* File from which extracted. */ + char *zIf; /* Surround the declaration with this #if */ + char *zFwd; /* A forward declaration. NULL if there is none. */ + char *zFwdCpp; /* Use this forward declaration for C++. */ + char *zDecl; /* A full declaration of this object */ + char *zExtra; /* Extra declaration text inserted into class objects */ + int extraType; /* Last public:, protected: or private: in zExtraDecl */ + struct Include *pInclude; /* #includes that come before this declaration */ + int flags; /* See the "Properties" below */ + Token *pComment; /* A block comment associated with this declaration */ + Token tokenCode; /* Implementation of functions and procedures */ + Decl *pSameName; /* Next declaration with the same "zName" */ + Decl *pSameHash; /* Next declaration with same hash but different zName */ + Decl *pNext; /* Next declaration with a different name */ +}; + +/* +** Properties associated with declarations. +** +** DP_Forward and DP_Declared are used during the generation of a single +** header file in order to prevent duplicate declarations and definitions. +** DP_Forward is set after the object has been given a forward declaration +** and DP_Declared is set after the object gets a full declarations. +** (Example: A forward declaration is "typedef struct Abc Abc;" and the +** full declaration is "struct Abc { int a; float b; };".) +** +** The DP_Export and DP_Local flags are more permanent. They mark objects +** that have EXPORT scope and LOCAL scope respectively. If both of these +** marks are missing, then the object has library scope. The meanings of +** the scopes are as follows: +** +** LOCAL scope The object is only usable within the file in +** which it is declared. +** +** library scope The object is visible and usable within other +** files in the same project. By if the project is +** a library, then the object is not visible to users +** of the library. (i.e. the object does not appear +** in the output when using the -H option.) +** +** EXPORT scope The object is visible and usable everywhere. +** +** The DP_Flag is a temporary use flag that is used during processing to +** prevent an infinite loop. It's use is localized. +** +** The DP_Cplusplus, DP_ExternCReqd and DP_ExternReqd flags are permanent +** and are used to specify what type of declaration the object requires. +*/ +#define DP_Forward 0x001 /* Has a forward declaration in this file */ +#define DP_Declared 0x002 /* Has a full declaration in this file */ +#define DP_Export 0x004 /* Export this declaration */ +#define DP_Local 0x008 /* Declare in its home file only */ +#define DP_Flag 0x010 /* Use to mark a subset of a Decl list + ** for special processing */ +#define DP_Cplusplus 0x020 /* Has C++ linkage and cannot appear in a + ** C header file */ +#define DP_ExternCReqd 0x040 /* Prepend 'extern "C"' in a C++ header. + ** Prepend nothing in a C header */ +#define DP_ExternReqd 0x080 /* Prepend 'extern "C"' in a C++ header if + ** DP_Cplusplus is not also set. If DP_Cplusplus + ** is set or this is a C header then + ** prepend 'extern' */ + +/* +** Convenience macros for dealing with declaration properties +*/ +#define DeclHasProperty(D,P) (((D)->flags&(P))==(P)) +#define DeclHasAnyProperty(D,P) (((D)->flags&(P))!=0) +#define DeclSetProperty(D,P) (D)->flags |= (P) +#define DeclClearProperty(D,P) (D)->flags &= ~(P) + +/* +** These are state properties of the parser. Each of the values is +** distinct from the DP_ values above so that both can be used in +** the same "flags" field. +** +** Be careful not to confuse PS_Export with DP_Export or +** PS_Local with DP_Local. Their names are similar, but the meanings +** of these flags are very different. +*/ +#define PS_Extern 0x000800 /* "extern" has been seen */ +#define PS_Export 0x001000 /* If between "#if EXPORT_INTERFACE" + ** and "#endif" */ +#define PS_Export2 0x002000 /* If "EXPORT" seen */ +#define PS_Typedef 0x004000 /* If "typedef" has been seen */ +#define PS_Static 0x008000 /* If "static" has been seen */ +#define PS_Interface 0x010000 /* If within #if INTERFACE..#endif */ +#define PS_Method 0x020000 /* If "::" token has been seen */ +#define PS_Local 0x040000 /* If within #if LOCAL_INTERFACE..#endif */ +#define PS_Local2 0x080000 /* If "LOCAL" seen. */ +#define PS_Public 0x100000 /* If "PUBLIC" seen. */ +#define PS_Protected 0x200000 /* If "PROTECTED" seen. */ +#define PS_Private 0x400000 /* If "PRIVATE" seen. */ +#define PS_PPP 0x700000 /* If any of PUBLIC, PRIVATE, PROTECTED */ + +/* +** The following set of flags are ORed into the "flags" field of +** a Decl in order to identify what type of object is being +** declared. +*/ +#define TY_Class 0x00100000 +#define TY_Subroutine 0x00200000 +#define TY_Macro 0x00400000 +#define TY_Typedef 0x00800000 +#define TY_Variable 0x01000000 +#define TY_Structure 0x02000000 +#define TY_Union 0x04000000 +#define TY_Enumeration 0x08000000 +#define TY_Defunct 0x10000000 /* Used to erase a declaration */ + +/* +** Each nested #if (or #ifdef or #ifndef) is stored in a stack of +** instances of the following structure. +*/ +typedef struct Ifmacro Ifmacro; +struct Ifmacro { + int nLine; /* Line number where this macro occurs */ + char *zCondition; /* Text of the condition for this macro */ + Ifmacro *pNext; /* Next down in the stack */ + int flags; /* Can hold PS_Export, PS_Interface or PS_Local flags */ +}; + +/* +** When parsing a file, we need to keep track of what other files have +** be #include-ed. For each #include found, we create an instance of +** the following structure. +*/ +typedef struct Include Include; +struct Include { + char *zFile; /* The name of file include. Includes "" or <> */ + char *zIf; /* If not NULL, #include should be enclosed in #if */ + char *zLabel; /* A unique label used to test if this #include has + * appeared already in a file or not */ + Include *pNext; /* Previous include file, or NULL if this is the first */ +}; + +/* +** Identifiers found in a source file that might be used later to provoke +** the copying of a declaration into the corresponding header file are +** stored in a hash table as instances of the following structure. +*/ +typedef struct Ident Ident; +struct Ident { + char *zName; /* The text of this identifier */ + Ident *pCollide; /* Next identifier with the same hash */ + Ident *pNext; /* Next identifier in a list of them all */ +}; + +/* +** A complete table of identifiers is stored in an instance of +** the next structure. +*/ +#define IDENT_HASH_SIZE 2237 +typedef struct IdentTable IdentTable; +struct IdentTable { + Ident *pList; /* List of all identifiers in this table */ + Ident *apTable[IDENT_HASH_SIZE]; /* The hash table */ +}; + +/* +** The following structure holds all information for a single +** source file named on the command line of this program. +*/ +typedef struct InFile InFile; +struct InFile { + char *zSrc; /* Name of input file */ + char *zHdr; /* Name of the generated .h file for this input. + ** Will be NULL if input is to be scanned only */ + int flags; /* One or more DP_, PS_ and/or TY_ flags */ + InFile *pNext; /* Next input file in the list of them all */ + IdentTable idTable; /* All identifiers in this input file */ +}; + +/* +** An unbounded string is able to grow without limit. We use these +** to construct large in-memory strings from lots of smaller components. +*/ +typedef struct String String; +struct String { + int nAlloc; /* Number of bytes allocated */ + int nUsed; /* Number of bytes used (not counting nul terminator) */ + char *zText; /* Text of the string */ +}; + +/* +** The following structure contains a lot of state information used +** while generating a .h file. We put the information in this structure +** and pass around a pointer to this structure, rather than pass around +** all of the information separately. This helps reduce the number of +** arguments to generator functions. +*/ +typedef struct GenState GenState; +struct GenState { + String *pStr; /* Write output to this string */ + IdentTable *pTable; /* A table holding the zLabel of every #include that + * has already been generated. Used to avoid + * generating duplicate #includes. */ + const char *zIf; /* If not NULL, then we are within a #if with + * this argument. */ + int nErr; /* Number of errors */ + const char *zFilename; /* Name of the source file being scanned */ + int flags; /* Various flags (DP_ and PS_ flags above) */ +}; + +/* +** The following text line appears at the top of every file generated +** by this program. By recognizing this line, the program can be sure +** never to read a file that it generated itself. +** +** The "#undef INTERFACE" part is a hack to work around a name collision +** in MSVC 2008. +*/ +const char zTopLine[] = + "/* \aThis file was automatically generated. Do not edit! */\n" + "#undef INTERFACE\n"; +#define nTopLine (sizeof(zTopLine)-1) + +/* +** The name of the file currently being parsed. +*/ +static const char *zFilename; + +/* +** The stack of #if macros for the file currently being parsed. +*/ +static Ifmacro *ifStack = 0; + +/* +** A list of all files that have been #included so far in a file being +** parsed. +*/ +static Include *includeList = 0; + +/* +** The last block comment seen. +*/ +static Token *blockComment = 0; + +/* +** The following flag is set if the -doc flag appears on the +** command line. +*/ +static int doc_flag = 0; + +/* +** If the following flag is set, then makeheaders will attempt to +** generate prototypes for static functions and procedures. +*/ +static int proto_static = 0; + +/* +** A list of all declarations. The list is held together using the +** pNext field of the Decl structure. +*/ +static Decl *pDeclFirst; /* First on the list */ +static Decl *pDeclLast; /* Last on the list */ + +/* +** A hash table of all declarations +*/ +#define DECL_HASH_SIZE 3371 +static Decl *apTable[DECL_HASH_SIZE]; + +/* +** The TEST macro must be defined to something. Make sure this is the +** case. +*/ +#ifndef TEST +# define TEST 0 +#endif + +#ifdef NOT_USED +/* +** We do our own assertion macro so that we can have more control +** over debugging. +*/ +#define Assert(X) if(!(X)){ CantHappen(__LINE__); } +#define CANT_HAPPEN CantHappen(__LINE__) +static void CantHappen(int iLine){ + fprintf(stderr,"Assertion failed on line %d\n",iLine); + *(char*)1 = 0; /* Force a core-dump */ +} +#endif + +/* +** Memory allocation functions that are guaranteed never to return NULL. +*/ +static void *SafeMalloc(int nByte){ + void *p = malloc( nByte ); + if( p==0 ){ + fprintf(stderr,"Out of memory. Can't allocate %d bytes.\n",nByte); + exit(1); + } + return p; +} +static void SafeFree(void *pOld){ + if( pOld ){ + free(pOld); + } +} +static void *SafeRealloc(void *pOld, int nByte){ + void *p; + if( pOld==0 ){ + p = SafeMalloc(nByte); + }else{ + p = realloc(pOld, nByte); + if( p==0 ){ + fprintf(stderr, + "Out of memory. Can't enlarge an allocation to %d bytes\n",nByte); + exit(1); + } + } + return p; +} +static char *StrDup(const char *zSrc, int nByte){ + char *zDest; + if( nByte<=0 ){ + nByte = strlen(zSrc); + } + zDest = SafeMalloc( nByte + 1 ); + strncpy(zDest,zSrc,nByte); + zDest[nByte] = 0; + return zDest; +} + +/* +** Return TRUE if the character X can be part of an identifier +*/ +#define ISALNUM(X) ((X)=='_' || isalnum(X)) + +/* +** Routines for dealing with unbounded strings. +*/ +static void StringInit(String *pStr){ + pStr->nAlloc = 0; + pStr->nUsed = 0; + pStr->zText = 0; +} +static void StringReset(String *pStr){ + SafeFree(pStr->zText); + StringInit(pStr); +} +static void StringAppend(String *pStr, const char *zText, int nByte){ + if( nByte<=0 ){ + nByte = strlen(zText); + } + if( pStr->nUsed + nByte >= pStr->nAlloc ){ + if( pStr->nAlloc==0 ){ + pStr->nAlloc = nByte + 100; + pStr->zText = SafeMalloc( pStr->nAlloc ); + }else{ + pStr->nAlloc = pStr->nAlloc*2 + nByte; + pStr->zText = SafeRealloc(pStr->zText, pStr->nAlloc); + } + } + strncpy(&pStr->zText[pStr->nUsed],zText,nByte); + pStr->nUsed += nByte; + pStr->zText[pStr->nUsed] = 0; +} +#define StringGet(S) ((S)->zText?(S)->zText:"") + +/* +** Compute a hash on a string. The number returned is a non-negative +** value between 0 and 2**31 - 1 +*/ +static int Hash(const char *z, int n){ + unsigned int h = 0; + if( n<=0 ){ + n = strlen(z); + } + while( n-- ){ + h = h ^ (h<<5) ^ *z++; + } + return (int)(h & 0x7fffffff); +} + +/* +** Given an identifier name, try to find a declaration for that +** identifier in the hash table. If found, return a pointer to +** the Decl structure. If not found, return 0. +*/ +static Decl *FindDecl(const char *zName, int len){ + int h; + Decl *p; + + if( len<=0 ){ + len = strlen(zName); + } + h = Hash(zName,len) % DECL_HASH_SIZE; + p = apTable[h]; + while( p && (strncmp(p->zName,zName,len)!=0 || p->zName[len]!=0) ){ + p = p->pSameHash; + } + return p; +} + +/* +** Install the given declaration both in the hash table and on +** the list of all declarations. +*/ +static void InstallDecl(Decl *pDecl){ + int h; + Decl *pOther; + + h = Hash(pDecl->zName,0) % DECL_HASH_SIZE; + pOther = apTable[h]; + while( pOther && strcmp(pDecl->zName,pOther->zName)!=0 ){ + pOther = pOther->pSameHash; + } + if( pOther ){ + pDecl->pSameName = pOther->pSameName; + pOther->pSameName = pDecl; + }else{ + pDecl->pSameName = 0; + pDecl->pSameHash = apTable[h]; + apTable[h] = pDecl; + } + pDecl->pNext = 0; + if( pDeclFirst==0 ){ + pDeclFirst = pDeclLast = pDecl; + }else{ + pDeclLast->pNext = pDecl; + pDeclLast = pDecl; + } +} + +/* +** Look at the current ifStack. If anything declared at the current +** position must be surrounded with +** +** #if STUFF +** #endif +** +** Then this routine computes STUFF and returns a pointer to it. Memory +** to hold the value returned is obtained from malloc(). +*/ +static char *GetIfString(void){ + Ifmacro *pIf; + char *zResult = 0; + int hasIf = 0; + String str; + + for(pIf = ifStack; pIf; pIf=pIf->pNext){ + if( pIf->zCondition==0 || *pIf->zCondition==0 ) continue; + if( !hasIf ){ + hasIf = 1; + StringInit(&str); + }else{ + StringAppend(&str," && ",4); + } + StringAppend(&str,pIf->zCondition,0); + } + if( hasIf ){ + zResult = StrDup(StringGet(&str),0); + StringReset(&str); + }else{ + zResult = 0; + } + return zResult; +} + +/* +** Create a new declaration and put it in the hash table. Also +** return a pointer to it so that we can fill in the zFwd and zDecl +** fields, and so forth. +*/ +static Decl *CreateDecl( + const char *zName, /* Name of the object being declared. */ + int nName /* Length of the name */ +){ + Decl *pDecl; + + pDecl = SafeMalloc( sizeof(Decl) + nName + 1); + memset(pDecl,0,sizeof(Decl)); + pDecl->zName = (char*)&pDecl[1]; + memcpy(pDecl->zName, zName, nName); + pDecl->zName[nName] = 0; + pDecl->zFile = zFilename; + pDecl->pInclude = includeList; + pDecl->zIf = GetIfString(); + InstallDecl(pDecl); + return pDecl; +} + +/* +** Insert a new identifier into an table of identifiers. Return TRUE if +** a new identifier was inserted and return FALSE if the identifier was +** already in the table. +*/ +static int IdentTableInsert( + IdentTable *pTable, /* The table into which we will insert */ + const char *zId, /* Name of the identifiers */ + int nId /* Length of the identifier name */ +){ + int h; + Ident *pId; + + if( nId<=0 ){ + nId = strlen(zId); + } + h = Hash(zId,nId) % IDENT_HASH_SIZE; + for(pId = pTable->apTable[h]; pId; pId=pId->pCollide){ + if( strncmp(zId,pId->zName,nId)==0 && pId->zName[nId]==0 ){ + /* printf("Already in table: %.*s\n",nId,zId); */ + return 0; + } + } + pId = SafeMalloc( sizeof(Ident) + nId + 1 ); + pId->zName = (char*)&pId[1]; + memcpy(pId->zName, zId, nId); + pId->zName[nId] = 0; + pId->pNext = pTable->pList; + pTable->pList = pId; + pId->pCollide = pTable->apTable[h]; + pTable->apTable[h] = pId; + /* printf("Add to table: %.*s\n",nId,zId); */ + return 1; +} + +/* +** Check to see if the given value is in the given IdentTable. Return +** true if it is and false if it is not. +*/ +static int IdentTableTest( + IdentTable *pTable, /* The table in which to search */ + const char *zId, /* Name of the identifiers */ + int nId /* Length of the identifier name */ +){ + int h; + Ident *pId; + + if( nId<=0 ){ + nId = strlen(zId); + } + h = Hash(zId,nId) % IDENT_HASH_SIZE; + for(pId = pTable->apTable[h]; pId; pId=pId->pCollide){ + if( strncmp(zId,pId->zName,nId)==0 && pId->zName[nId]==0 ){ + return 1; + } + } + return 0; +} + +/* +** Remove every identifier from the given table. Reset the table to +** its initial state. +*/ +static void IdentTableReset(IdentTable *pTable){ + Ident *pId, *pNext; + + for(pId = pTable->pList; pId; pId = pNext){ + pNext = pId->pNext; + SafeFree(pId); + } + memset(pTable,0,sizeof(IdentTable)); +} + +#ifdef DEBUG +/* +** Print the name of every identifier in the given table, one per line +*/ +static void IdentTablePrint(IdentTable *pTable, FILE *pOut){ + Ident *pId; + + for(pId = pTable->pList; pId; pId = pId->pNext){ + fprintf(pOut,"%s\n",pId->zName); + } +} +#endif + +/* +** Read an entire file into memory. Return a pointer to the memory. +** +** The memory is obtained from SafeMalloc and must be freed by the +** calling function. +** +** If the read fails for any reason, 0 is returned. +*/ +static char *ReadFile(const char *zFilename){ + struct stat sStat; + FILE *pIn; + char *zBuf; + int n; + + if( stat(zFilename,&sStat)!=0 +#ifndef WIN32 + || !S_ISREG(sStat.st_mode) +#endif + ){ + return 0; + } + pIn = fopen(zFilename,"r"); + if( pIn==0 ){ + return 0; + } + zBuf = SafeMalloc( sStat.st_size + 1 ); + n = fread(zBuf,1,sStat.st_size,pIn); + zBuf[n] = 0; + fclose(pIn); + return zBuf; +} + +/* +** Write the contents of a string into a file. Return the number of +** errors +*/ +static int WriteFile(const char *zFilename, const char *zOutput){ + FILE *pOut; + pOut = fopen(zFilename,"w"); + if( pOut==0 ){ + return 1; + } + fwrite(zOutput,1,strlen(zOutput),pOut); + fclose(pOut); + return 0; +} + +/* +** Major token types +*/ +#define TT_Space 1 /* Contiguous white space */ +#define TT_Id 2 /* An identifier */ +#define TT_Preprocessor 3 /* Any C preprocessor directive */ +#define TT_Comment 4 /* Either C or C++ style comment */ +#define TT_Number 5 /* Any numeric constant */ +#define TT_String 6 /* String or character constants. ".." or '.' */ +#define TT_Braces 7 /* All text between { and a matching } */ +#define TT_EOF 8 /* End of file */ +#define TT_Error 9 /* An error condition */ +#define TT_BlockComment 10 /* A C-Style comment at the left margin that + * spans multiple lines */ +#define TT_Other 0 /* None of the above */ + +/* +** Get a single low-level token from the input file. Update the +** file pointer so that it points to the first character beyond the +** token. +** +** A "low-level token" is any token except TT_Braces. A TT_Braces token +** consists of many smaller tokens and is assembled by a routine that +** calls this one. +** +** The function returns the number of errors. An error is an +** unterminated string or character literal or an unterminated +** comment. +** +** Profiling shows that this routine consumes about half the +** CPU time on a typical run of makeheaders. +*/ +static int GetToken(InStream *pIn, Token *pToken){ + int i; + const char *z; + int cStart; + int c; + int startLine; /* Line on which a structure begins */ + int nlisc = 0; /* True if there is a new-line in a ".." or '..' */ + int nErr = 0; /* Number of errors seen */ + + z = pIn->z; + i = pIn->i; + pToken->nLine = pIn->nLine; + pToken->zText = &z[i]; + switch( z[i] ){ + case 0: + pToken->eType = TT_EOF; + pToken->nText = 0; + break; + + case '#': + if( i==0 || z[i-1]=='\n' || (i>1 && z[i-1]=='\r' && z[i-2]=='\n')){ + /* We found a preprocessor statement */ + pToken->eType = TT_Preprocessor; + i++; + while( z[i]!=0 && z[i]!='\n' ){ + if( z[i]=='\\' ){ + i++; + if( z[i]=='\n' ) pIn->nLine++; + } + i++; + } + pToken->nText = i - pIn->i; + }else{ + /* Just an operator */ + pToken->eType = TT_Other; + pToken->nText = 1; + } + break; + + case ' ': + case '\t': + case '\r': + case '\f': + case '\n': + while( isspace(z[i]) ){ + if( z[i]=='\n' ) pIn->nLine++; + i++; + } + pToken->eType = TT_Space; + pToken->nText = i - pIn->i; + break; + + case '\\': + pToken->nText = 2; + pToken->eType = TT_Other; + if( z[i+1]=='\n' ){ + pIn->nLine++; + pToken->eType = TT_Space; + }else if( z[i+1]==0 ){ + pToken->nText = 1; + } + break; + + case '\'': + case '\"': + cStart = z[i]; + startLine = pIn->nLine; + do{ + i++; + c = z[i]; + if( c=='\n' ){ + if( !nlisc ){ + fprintf(stderr, + "%s:%d: (warning) Newline in string or character literal.\n", + zFilename, pIn->nLine); + nlisc = 1; + } + pIn->nLine++; + } + if( c=='\\' ){ + i++; + c = z[i]; + if( c=='\n' ){ + pIn->nLine++; + } + }else if( c==cStart ){ + i++; + c = 0; + }else if( c==0 ){ + fprintf(stderr, "%s:%d: Unterminated string or character literal.\n", + zFilename, startLine); + nErr++; + } + }while( c ); + pToken->eType = TT_String; + pToken->nText = i - pIn->i; + break; + + case '/': + if( z[i+1]=='/' ){ + /* C++ style comment */ + while( z[i] && z[i]!='\n' ){ i++; } + pToken->eType = TT_Comment; + pToken->nText = i - pIn->i; + }else if( z[i+1]=='*' ){ + /* C style comment */ + int isBlockComment = i==0 || z[i-1]=='\n'; + i += 2; + startLine = pIn->nLine; + while( z[i] && (z[i]!='*' || z[i+1]!='/') ){ + if( z[i]=='\n' ){ + pIn->nLine++; + if( isBlockComment ){ + if( z[i+1]=='*' || z[i+2]=='*' ){ + isBlockComment = 2; + }else{ + isBlockComment = 0; + } + } + } + i++; + } + if( z[i] ){ + i += 2; + }else{ + isBlockComment = 0; + fprintf(stderr,"%s:%d: Unterminated comment\n", + zFilename, startLine); + nErr++; + } + pToken->eType = isBlockComment==2 ? TT_BlockComment : TT_Comment; + pToken->nText = i - pIn->i; + }else{ + /* A divide operator */ + pToken->eType = TT_Other; + pToken->nText = 1 + (z[i+1]=='+'); + } + break; + + case '0': + if( z[i+1]=='x' || z[i+1]=='X' ){ + /* A hex constant */ + i += 2; + while( isxdigit(z[i]) ){ i++; } + }else{ + /* An octal constant */ + while( isdigit(z[i]) ){ i++; } + } + pToken->eType = TT_Number; + pToken->nText = i - pIn->i; + break; + + case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + while( isdigit(z[i]) ){ i++; } + if( (c=z[i])=='.' ){ + i++; + while( isdigit(z[i]) ){ i++; } + c = z[i]; + if( c=='e' || c=='E' ){ + i++; + if( ((c=z[i])=='+' || c=='-') && isdigit(z[i+1]) ){ i++; } + while( isdigit(z[i]) ){ i++; } + c = z[i]; + } + if( c=='f' || c=='F' || c=='l' || c=='L' ){ i++; } + }else if( c=='e' || c=='E' ){ + i++; + if( ((c=z[i])=='+' || c=='-') && isdigit(z[i+1]) ){ i++; } + while( isdigit(z[i]) ){ i++; } + }else if( c=='L' || c=='l' ){ + i++; + c = z[i]; + if( c=='u' || c=='U' ){ i++; } + }else if( c=='u' || c=='U' ){ + i++; + c = z[i]; + if( c=='l' || c=='L' ){ i++; } + } + pToken->eType = TT_Number; + pToken->nText = i - pIn->i; + break; + + case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': + case 'h': case 'i': case 'j': case 'k': case 'l': case 'm': case 'n': + case 'o': case 'p': case 'q': case 'r': case 's': case 't': case 'u': + case 'v': case 'w': case 'x': case 'y': case 'z': case 'A': case 'B': + case 'C': case 'D': case 'E': case 'F': case 'G': case 'H': case 'I': + case 'J': case 'K': case 'L': case 'M': case 'N': case 'O': case 'P': + case 'Q': case 'R': case 'S': case 'T': case 'U': case 'V': case 'W': + case 'X': case 'Y': case 'Z': case '_': + while( isalnum(z[i]) || z[i]=='_' ){ i++; }; + pToken->eType = TT_Id; + pToken->nText = i - pIn->i; + break; + + case ':': + pToken->eType = TT_Other; + pToken->nText = 1 + (z[i+1]==':'); + break; + + case '=': + case '<': + case '>': + case '+': + case '-': + case '*': + case '%': + case '^': + case '&': + case '|': + pToken->eType = TT_Other; + pToken->nText = 1 + (z[i+1]=='='); + break; + + default: + pToken->eType = TT_Other; + pToken->nText = 1; + break; + } + pIn->i += pToken->nText; + return nErr; +} + +/* +** This routine recovers the next token from the input file which is +** not a space or a comment or any text between an "#if 0" and "#endif". +** +** This routine returns the number of errors encountered. An error +** is an unterminated token or unmatched "#if 0". +** +** Profiling shows that this routine uses about a quarter of the +** CPU time in a typical run. +*/ +static int GetNonspaceToken(InStream *pIn, Token *pToken){ + int nIf = 0; + int inZero = 0; + const char *z; + int value; + int startLine; + int nErr = 0; + + startLine = pIn->nLine; + while( 1 ){ + nErr += GetToken(pIn,pToken); + /* printf("%04d: Type=%d nIf=%d [%.*s]\n", + pToken->nLine,pToken->eType,nIf,pToken->nText, + pToken->eType!=TT_Space ? pToken->zText : "<space>"); */ + pToken->pComment = blockComment; + switch( pToken->eType ){ + case TT_Comment: /*0123456789 12345678 */ + if( strncmp(pToken->zText, "/*MAKEHEADERS-STOP", 18)==0 ) return nErr; + break; + + case TT_Space: + break; + + case TT_BlockComment: + if( doc_flag ){ + blockComment = SafeMalloc( sizeof(Token) ); + *blockComment = *pToken; + } + break; + + case TT_EOF: + if( nIf ){ + fprintf(stderr,"%s:%d: Unterminated \"#if\"\n", + zFilename, startLine); + nErr++; + } + return nErr; + + case TT_Preprocessor: + z = &pToken->zText[1]; + while( *z==' ' || *z=='\t' ) z++; + if( sscanf(z,"if %d",&value)==1 && value==0 ){ + nIf++; + inZero = 1; + }else if( inZero ){ + if( strncmp(z,"if",2)==0 ){ + nIf++; + }else if( strncmp(z,"endif",5)==0 ){ + nIf--; + if( nIf==0 ) inZero = 0; + } + }else{ + return nErr; + } + break; + + default: + if( !inZero ){ + return nErr; + } + break; + } + } + /* NOT REACHED */ +} + +/* +** This routine looks for identifiers (strings of contiguous alphanumeric +** characters) within a preprocessor directive and adds every such string +** found to the given identifier table +*/ +static void FindIdentifiersInMacro(Token *pToken, IdentTable *pTable){ + Token sToken; + InStream sIn; + int go = 1; + + sIn.z = pToken->zText; + sIn.i = 1; + sIn.nLine = 1; + while( go && sIn.i < pToken->nText ){ + GetToken(&sIn,&sToken); + switch( sToken.eType ){ + case TT_Id: + IdentTableInsert(pTable,sToken.zText,sToken.nText); + break; + + case TT_EOF: + go = 0; + break; + + default: + break; + } + } +} + +/* +** This routine gets the next token. Everything contained within +** {...} is collapsed into a single TT_Braces token. Whitespace is +** omitted. +** +** If pTable is not NULL, then insert every identifier seen into the +** IdentTable. This includes any identifiers seen inside of {...}. +** +** The number of errors encountered is returned. An error is an +** unterminated token. +*/ +static int GetBigToken(InStream *pIn, Token *pToken, IdentTable *pTable){ + const char *zStart; + int iStart; + int nBrace; + int c; + int nLine; + int nErr; + + nErr = GetNonspaceToken(pIn,pToken); + switch( pToken->eType ){ + case TT_Id: + if( pTable!=0 ){ + IdentTableInsert(pTable,pToken->zText,pToken->nText); + } + return nErr; + + case TT_Preprocessor: + if( pTable!=0 ){ + FindIdentifiersInMacro(pToken,pTable); + } + return nErr; + + case TT_Other: + if( pToken->zText[0]=='{' ) break; + return nErr; + + default: + return nErr; + } + + iStart = pIn->i; + zStart = pToken->zText; + nLine = pToken->nLine; + nBrace = 1; + while( nBrace ){ + nErr += GetNonspaceToken(pIn,pToken); + /* printf("%04d: nBrace=%d [%.*s]\n",pToken->nLine,nBrace, + pToken->nText,pToken->zText); */ + switch( pToken->eType ){ + case TT_EOF: + fprintf(stderr,"%s:%d: Unterminated \"{\"\n", + zFilename, nLine); + nErr++; + pToken->eType = TT_Error; + return nErr; + + case TT_Id: + if( pTable ){ + IdentTableInsert(pTable,pToken->zText,pToken->nText); + } + break; + + case TT_Preprocessor: + if( pTable!=0 ){ + FindIdentifiersInMacro(pToken,pTable); + } + break; + + case TT_Other: + if( (c = pToken->zText[0])=='{' ){ + nBrace++; + }else if( c=='}' ){ + nBrace--; + } + break; + + default: + break; + } + } + pToken->eType = TT_Braces; + pToken->nText = 1 + pIn->i - iStart; + pToken->zText = zStart; + pToken->nLine = nLine; + return nErr; +} + +/* +** This routine frees up a list of Tokens. The pComment tokens are +** not cleared by this. So we leak a little memory when using the -doc +** option. So what. +*/ +static void FreeTokenList(Token *pList){ + Token *pNext; + while( pList ){ + pNext = pList->pNext; + SafeFree(pList); + pList = pNext; + } +} + +/* +** Tokenize an entire file. Return a pointer to the list of tokens. +** +** Space for each token is obtained from a separate malloc() call. The +** calling function is responsible for freeing this space. +** +** If pTable is not NULL, then fill the table with all identifiers seen in +** the input file. +*/ +static Token *TokenizeFile(const char *zFile, IdentTable *pTable){ + InStream sIn; + Token *pFirst = 0, *pLast = 0, *pNew; + int nErr = 0; + + sIn.z = zFile; + sIn.i = 0; + sIn.nLine = 1; + blockComment = 0; + + while( sIn.z[sIn.i]!=0 ){ + pNew = SafeMalloc( sizeof(Token) ); + nErr += GetBigToken(&sIn,pNew,pTable); + debug3(TOKENIZER, "Token on line %d: [%.*s]\n", + pNew->nLine, pNew->nText<50 ? pNew->nText : 50, pNew->zText); + if( pFirst==0 ){ + pFirst = pLast = pNew; + pNew->pPrev = 0; + }else{ + pLast->pNext = pNew; + pNew->pPrev = pLast; + pLast = pNew; + } + if( pNew->eType==TT_EOF ) break; + } + if( pLast ) pLast->pNext = 0; + blockComment = 0; + if( nErr ){ + FreeTokenList(pFirst); + pFirst = 0; + } + + return pFirst; +} + +#if TEST==1 +/* +** Use the following routine to test or debug the tokenizer. +*/ +void main(int argc, char **argv){ + char *zFile; + Token *pList, *p; + IdentTable sTable; + + if( argc!=2 ){ + fprintf(stderr,"Usage: %s filename\n",*argv); + exit(1); + } + memset(&sTable,0,sizeof(sTable)); + zFile = ReadFile(argv[1]); + if( zFile==0 ){ + fprintf(stderr,"Can't read file \"%s\"\n",argv[1]); + exit(1); + } + pList = TokenizeFile(zFile,&sTable); + for(p=pList; p; p=p->pNext){ + int j; + switch( p->eType ){ + case TT_Space: + printf("%4d: Space\n",p->nLine); + break; + case TT_Id: + printf("%4d: Id %.*s\n",p->nLine,p->nText,p->zText); + break; + case TT_Preprocessor: + printf("%4d: Preprocessor %.*s\n",p->nLine,p->nText,p->zText); + break; + case TT_Comment: + printf("%4d: Comment\n",p->nLine); + break; + case TT_BlockComment: + printf("%4d: Block Comment\n",p->nLine); + break; + case TT_Number: + printf("%4d: Number %.*s\n",p->nLine,p->nText,p->zText); + break; + case TT_String: + printf("%4d: String %.*s\n",p->nLine,p->nText,p->zText); + break; + case TT_Other: + printf("%4d: Other %.*s\n",p->nLine,p->nText,p->zText); + break; + case TT_Braces: + for(j=0; j<p->nText && j<30 && p->zText[j]!='\n'; j++){} + printf("%4d: Braces %.*s...}\n",p->nLine,j,p->zText); + break; + case TT_EOF: + printf("%4d: End of file\n",p->nLine); + break; + default: + printf("%4d: type %d\n",p->nLine,p->eType); + break; + } + } + FreeTokenList(pList); + SafeFree(zFile); + IdentTablePrint(&sTable,stdout); +} +#endif + +#ifdef DEBUG +/* +** For debugging purposes, write out a list of tokens. +*/ +static void PrintTokens(Token *pFirst, Token *pLast){ + int needSpace = 0; + int c; + + pLast = pLast->pNext; + while( pFirst!=pLast ){ + switch( pFirst->eType ){ + case TT_Preprocessor: + printf("\n%.*s\n",pFirst->nText,pFirst->zText); + needSpace = 0; + break; + + case TT_Id: + case TT_Number: + printf("%s%.*s", needSpace ? " " : "", pFirst->nText, pFirst->zText); + needSpace = 1; + break; + + default: + c = pFirst->zText[0]; + printf("%s%.*s", + (needSpace && (c=='*' || c=='{')) ? " " : "", + pFirst->nText, pFirst->zText); + needSpace = pFirst->zText[0]==','; + break; + } + pFirst = pFirst->pNext; + } +} +#endif + +/* +** Convert a sequence of tokens into a string and return a pointer +** to that string. Space to hold the string is obtained from malloc() +** and must be freed by the calling function. +** +** Certain keywords (EXPORT, PRIVATE, PUBLIC, PROTECTED) are always +** skipped. +** +** If pSkip!=0 then skip over nSkip tokens beginning with pSkip. +** +** If zTerm!=0 then append the text to the end. +*/ +static char *TokensToString( + Token *pFirst, /* First token in the string */ + Token *pLast, /* Last token in the string */ + char *zTerm, /* Terminate the string with this text if not NULL */ + Token *pSkip, /* Skip this token if not NULL */ + int nSkip /* Skip a total of this many tokens */ +){ + char *zReturn; + String str; + int needSpace = 0; + int c; + int iSkip = 0; + int skipOne = 0; + + StringInit(&str); + pLast = pLast->pNext; + while( pFirst!=pLast ){ + if( pFirst==pSkip ){ iSkip = nSkip; } + if( iSkip>0 ){ + iSkip--; + pFirst=pFirst->pNext; + continue; + } + switch( pFirst->eType ){ + case TT_Preprocessor: + StringAppend(&str,"\n",1); + StringAppend(&str,pFirst->zText,pFirst->nText); + StringAppend(&str,"\n",1); + needSpace = 0; + break; + + case TT_Id: + switch( pFirst->zText[0] ){ + case 'E': + if( pFirst->nText==6 && strncmp(pFirst->zText,"EXPORT",6)==0 ){ + skipOne = 1; + } + break; + case 'P': + switch( pFirst->nText ){ + case 6: skipOne = !strncmp(pFirst->zText,"PUBLIC", 6); break; + case 7: skipOne = !strncmp(pFirst->zText,"PRIVATE",7); break; + case 9: skipOne = !strncmp(pFirst->zText,"PROTECTED",9); break; + default: break; + } + break; + default: + break; + } + if( skipOne ){ + pFirst = pFirst->pNext; + skipOne = 0; + continue; + } + /* Fall thru to the next case */ + case TT_Number: + if( needSpace ){ + StringAppend(&str," ",1); + } + StringAppend(&str,pFirst->zText,pFirst->nText); + needSpace = 1; + break; + + default: + c = pFirst->zText[0]; + if( needSpace && (c=='*' || c=='{') ){ + StringAppend(&str," ",1); + } + StringAppend(&str,pFirst->zText,pFirst->nText); + /* needSpace = pFirst->zText[0]==','; */ + needSpace = 0; + break; + } + pFirst = pFirst->pNext; + } + if( zTerm && *zTerm ){ + StringAppend(&str,zTerm,strlen(zTerm)); + } + zReturn = StrDup(StringGet(&str),0); + StringReset(&str); + return zReturn; +} + +/* +** This routine is called when we see one of the keywords "struct", +** "enum", "union" or "class". This might be the beginning of a +** type declaration. This routine will process the declaration and +** remove the declaration tokens from the input stream. +** +** If this is a type declaration that is immediately followed by a +** semicolon (in other words it isn't also a variable definition) +** then set *pReset to ';'. Otherwise leave *pReset at 0. The +** *pReset flag causes the parser to skip ahead to the next token +** that begins with the value placed in the *pReset flag, if that +** value is different from 0. +*/ +static int ProcessTypeDecl(Token *pList, int flags, int *pReset){ + Token *pName, *pEnd; + Decl *pDecl; + String str; + int need_to_collapse = 1; + int type = 0; + + *pReset = 0; + if( pList==0 || pList->pNext==0 || pList->pNext->eType!=TT_Id ){ + return 0; + } + pName = pList->pNext; + + /* Catch the case of "struct Foo;" and skip it. */ + if( pName->pNext && pName->pNext->zText[0]==';' ){ + *pReset = ';'; + return 0; + } + + for(pEnd=pName->pNext; pEnd && pEnd->eType!=TT_Braces; pEnd=pEnd->pNext){ + switch( pEnd->zText[0] ){ + case '(': + case ')': + case '*': + case '[': + case '=': + case ';': + return 0; + } + } + if( pEnd==0 ){ + return 0; + } + + /* + ** At this point, we know we have a type declaration that is bounded + ** by pList and pEnd and has the name pName. + */ + + /* + ** If the braces are followed immediately by a semicolon, then we are + ** dealing a type declaration only. There is not variable definition + ** following the type declaration. So reset... + */ + if( pEnd->pNext==0 || pEnd->pNext->zText[0]==';' ){ + *pReset = ';'; + need_to_collapse = 0; + }else{ + need_to_collapse = 1; + } + + if( proto_static==0 && (flags & (PS_Local|PS_Export|PS_Interface))==0 ){ + /* Ignore these objects unless they are explicitly declared as interface, + ** or unless the "-local" command line option was specified. */ + *pReset = ';'; + return 0; + } + +#ifdef DEBUG + if( debugMask & PARSER ){ + printf("**** Found type: %.*s %.*s...\n", + pList->nText, pList->zText, pName->nText, pName->zText); + PrintTokens(pList,pEnd); + printf(";\n"); + } +#endif + + /* + ** Create a new Decl object for this definition. Actually, if this + ** is a C++ class definition, then the Decl object might already exist, + ** so check first for that case before creating a new one. + */ + switch( *pList->zText ){ + case 'c': type = TY_Class; break; + case 's': type = TY_Structure; break; + case 'e': type = TY_Enumeration; break; + case 'u': type = TY_Union; break; + default: /* Can't Happen */ break; + } + if( type!=TY_Class ){ + pDecl = 0; + }else{ + pDecl = FindDecl(pName->zText, pName->nText); + if( pDecl && (pDecl->flags & type)!=type ) pDecl = 0; + } + if( pDecl==0 ){ + pDecl = CreateDecl(pName->zText,pName->nText); + } + if( (flags & PS_Static) || !(flags & (PS_Interface|PS_Export)) ){ + DeclSetProperty(pDecl,DP_Local); + } + DeclSetProperty(pDecl,type); + + /* The object has a full declaration only if it is contained within + ** "#if INTERFACE...#endif" or "#if EXPORT_INTERFACE...#endif" or + ** "#if LOCAL_INTERFACE...#endif". Otherwise, we only give it a + ** forward declaration. + */ + if( flags & (PS_Local | PS_Export | PS_Interface) ){ + pDecl->zDecl = TokensToString(pList,pEnd,";\n",0,0); + }else{ + pDecl->zDecl = 0; + } + pDecl->pComment = pList->pComment; + StringInit(&str); + StringAppend(&str,"typedef ",0); + StringAppend(&str,pList->zText,pList->nText); + StringAppend(&str," ",0); + StringAppend(&str,pName->zText,pName->nText); + StringAppend(&str," ",0); + StringAppend(&str,pName->zText,pName->nText); + StringAppend(&str,";\n",2); + pDecl->zFwd = StrDup(StringGet(&str),0); + StringReset(&str); + StringInit(&str); + StringAppend(&str,pList->zText,pList->nText); + StringAppend(&str," ",0); + StringAppend(&str,pName->zText,pName->nText); + StringAppend(&str,";\n",2); + pDecl->zFwdCpp = StrDup(StringGet(&str),0); + StringReset(&str); + if( flags & PS_Export ){ + DeclSetProperty(pDecl,DP_Export); + }else if( flags & PS_Local ){ + DeclSetProperty(pDecl,DP_Local); + } + + /* Here's something weird. ANSI-C doesn't allow a forward declaration + ** of an enumeration. So we have to build the typedef into the + ** definition. + */ + if( pDecl->zDecl && DeclHasProperty(pDecl, TY_Enumeration) ){ + StringInit(&str); + StringAppend(&str,pDecl->zDecl,0); + StringAppend(&str,pDecl->zFwd,0); + SafeFree(pDecl->zDecl); + SafeFree(pDecl->zFwd); + pDecl->zFwd = 0; + pDecl->zDecl = StrDup(StringGet(&str),0); + StringReset(&str); + } + + if( pName->pNext->zText[0]==':' ){ + DeclSetProperty(pDecl,DP_Cplusplus); + } + if( pName->nText==5 && strncmp(pName->zText,"class",5)==0 ){ + DeclSetProperty(pDecl,DP_Cplusplus); + } + + /* + ** Remove all but pList and pName from the input stream. + */ + if( need_to_collapse ){ + while( pEnd!=pName ){ + Token *pPrev = pEnd->pPrev; + pPrev->pNext = pEnd->pNext; + pEnd->pNext->pPrev = pPrev; + SafeFree(pEnd); + pEnd = pPrev; + } + } + return 0; +} + +/* +** Given a list of tokens that declare something (a function, procedure, +** variable or typedef) find the token which contains the name of the +** thing being declared. +** +** Algorithm: +** +** The name is: +** +** 1. The first identifier that is followed by a "[", or +** +** 2. The first identifier that is followed by a "(" where the +** "(" is followed by another identifier, or +** +** 3. The first identifier followed by "::", or +** +** 4. If none of the above, then the last identifier. +** +** In all of the above, certain reserved words (like "char") are +** not considered identifiers. +*/ +static Token *FindDeclName(Token *pFirst, Token *pLast){ + Token *pName = 0; + Token *p; + int c; + + if( pFirst==0 || pLast==0 ){ + return 0; + } + pLast = pLast->pNext; + for(p=pFirst; p && p!=pLast; p=p->pNext){ + if( p->eType==TT_Id ){ + static IdentTable sReserved; + static int isInit = 0; + static const char *aWords[] = { "char", "class", + "const", "double", "enum", "extern", "EXPORT", "ET_PROC", + "float", "int", "long", + "PRIVATE", "PROTECTED", "PUBLIC", + "register", "static", "struct", "sizeof", "signed", "typedef", + "union", "volatile", "virtual", "void", }; + + if( !isInit ){ + int i; + for(i=0; i<sizeof(aWords)/sizeof(aWords[0]); i++){ + IdentTableInsert(&sReserved,aWords[i],0); + } + isInit = 1; + } + if( !IdentTableTest(&sReserved,p->zText,p->nText) ){ + pName = p; + } + }else if( p==pFirst ){ + continue; + }else if( (c=p->zText[0])=='[' && pName ){ + break; + }else if( c=='(' && p->pNext && p->pNext->eType==TT_Id && pName ){ + break; + }else if( c==':' && p->zText[1]==':' && pName ){ + break; + } + } + return pName; +} + +/* +** This routine is called when we see a method for a class that begins +** with the PUBLIC, PRIVATE, or PROTECTED keywords. Such methods are +** added to their class definitions. +*/ +static int ProcessMethodDef(Token *pFirst, Token *pLast, int flags){ + Token *pClass; + char *zDecl; + Decl *pDecl; + String str; + int type; + + pLast = pLast->pPrev; + while( pFirst->zText[0]=='P' ){ + int rc = 1; + switch( pFirst->nText ){ + case 6: rc = strncmp(pFirst->zText,"PUBLIC",6); break; + case 7: rc = strncmp(pFirst->zText,"PRIVATE",7); break; + case 9: rc = strncmp(pFirst->zText,"PROTECTED",9); break; + default: break; + } + if( rc ) break; + pFirst = pFirst->pNext; + } + pClass = FindDeclName(pFirst,pLast); + if( pClass==0 ){ + fprintf(stderr,"%s:%d: Unable to find the class name for this method\n", + zFilename, pFirst->nLine); + return 1; + } + pDecl = FindDecl(pClass->zText, pClass->nText); + if( pDecl==0 || (pDecl->flags & TY_Class)!=TY_Class ){ + pDecl = CreateDecl(pClass->zText, pClass->nText); + DeclSetProperty(pDecl, TY_Class); + } + StringInit(&str); + if( pDecl->zExtra ){ + StringAppend(&str, pDecl->zExtra, 0); + SafeFree(pDecl->zExtra); + pDecl->zExtra = 0; + } + type = flags & PS_PPP; + if( pDecl->extraType!=type ){ + if( type & PS_Public ){ + StringAppend(&str, "public:\n", 0); + pDecl->extraType = PS_Public; + }else if( type & PS_Protected ){ + StringAppend(&str, "protected:\n", 0); + pDecl->extraType = PS_Protected; + }else if( type & PS_Private ){ + StringAppend(&str, "private:\n", 0); + pDecl->extraType = PS_Private; + } + } + StringAppend(&str, " ", 0); + zDecl = TokensToString(pFirst, pLast, ";\n", pClass, 2); + if(strncmp(zDecl, pClass->zText, pClass->nText)==0){ + /* If member initializer list is found after a constructor, + ** skip that part. */ + char * colon = strchr(zDecl, ':'); + if(colon!=0 && colon[1]!=0){ + *colon++ = ';'; + *colon++ = '\n'; + *colon = 0; + } + } + StringAppend(&str, zDecl, 0); + SafeFree(zDecl); + pDecl->zExtra = StrDup(StringGet(&str), 0); + StringReset(&str); + return 0; +} + +/* +** This routine is called when we see a function or procedure definition. +** We make an entry in the declaration table that is a prototype for this +** function or procedure. +*/ +static int ProcessProcedureDef(Token *pFirst, Token *pLast, int flags){ + Token *pName; + Decl *pDecl; + Token *pCode; + + if( pFirst==0 || pLast==0 ){ + return 0; + } + if( flags & PS_Method ){ + if( flags & PS_PPP ){ + return ProcessMethodDef(pFirst, pLast, flags); + }else{ + return 0; + } + } + if( (flags & PS_Static)!=0 && !proto_static ){ + return 0; + } + pCode = pLast; + while( pLast && pLast!=pFirst && pLast->zText[0]!=')' ){ + pLast = pLast->pPrev; + } + if( pLast==0 || pLast==pFirst || pFirst->pNext==pLast ){ + fprintf(stderr,"%s:%d: Unrecognized syntax.\n", + zFilename, pFirst->nLine); + return 1; + } + if( flags & (PS_Interface|PS_Export|PS_Local) ){ + fprintf(stderr,"%s:%d: Missing \"inline\" on function or procedure.\n", + zFilename, pFirst->nLine); + return 1; + } + pName = FindDeclName(pFirst,pLast); + if( pName==0 ){ + fprintf(stderr,"%s:%d: Malformed function or procedure definition.\n", + zFilename, pFirst->nLine); + return 1; + } + if( strncmp(pName->zText,"main",pName->nText)==0 ){ + /* skip main() decl. */ + return 0; + } + /* + ** At this point we've isolated a procedure declaration between pFirst + ** and pLast with the name pName. + */ +#ifdef DEBUG + if( debugMask & PARSER ){ + printf("**** Found routine: %.*s on line %d...\n", pName->nText, + pName->zText, pFirst->nLine); + PrintTokens(pFirst,pLast); + printf(";\n"); + } +#endif + pDecl = CreateDecl(pName->zText,pName->nText); + pDecl->pComment = pFirst->pComment; + if( pCode && pCode->eType==TT_Braces ){ + pDecl->tokenCode = *pCode; + } + DeclSetProperty(pDecl,TY_Subroutine); + pDecl->zDecl = TokensToString(pFirst,pLast,";\n",0,0); + if( (flags & (PS_Static|PS_Local2))!=0 ){ + DeclSetProperty(pDecl,DP_Local); + }else if( (flags & (PS_Export2))!=0 ){ + DeclSetProperty(pDecl,DP_Export); + } + + if( flags & DP_Cplusplus ){ + DeclSetProperty(pDecl,DP_Cplusplus); + }else{ + DeclSetProperty(pDecl,DP_ExternCReqd); + } + + return 0; +} + +/* +** This routine is called whenever we see the "inline" keyword. We +** need to seek-out the inline function or procedure and make a +** declaration out of the entire definition. +*/ +static int ProcessInlineProc(Token *pFirst, int flags, int *pReset){ + Token *pName; + Token *pEnd; + Decl *pDecl; + + for(pEnd=pFirst; pEnd; pEnd = pEnd->pNext){ + if( pEnd->zText[0]=='{' || pEnd->zText[0]==';' ){ + *pReset = pEnd->zText[0]; + break; + } + } + if( pEnd==0 ){ + *pReset = ';'; + fprintf(stderr,"%s:%d: incomplete inline procedure definition\n", + zFilename, pFirst->nLine); + return 1; + } + pName = FindDeclName(pFirst,pEnd); + if( pName==0 ){ + fprintf(stderr,"%s:%d: malformed inline procedure definition\n", + zFilename, pFirst->nLine); + return 1; + } + +#ifdef DEBUG + if( debugMask & PARSER ){ + printf("**** Found inline routine: %.*s on line %d...\n", + pName->nText, pName->zText, pFirst->nLine); + PrintTokens(pFirst,pEnd); + printf("\n"); + } +#endif + pDecl = CreateDecl(pName->zText,pName->nText); + pDecl->pComment = pFirst->pComment; + DeclSetProperty(pDecl,TY_Subroutine); + pDecl->zDecl = TokensToString(pFirst,pEnd,";\n",0,0); + if( (flags & (PS_Static|PS_Local|PS_Local2)) ){ + DeclSetProperty(pDecl,DP_Local); + }else if( flags & (PS_Export|PS_Export2) ){ + DeclSetProperty(pDecl,DP_Export); + } + + if( flags & DP_Cplusplus ){ + DeclSetProperty(pDecl,DP_Cplusplus); + }else{ + DeclSetProperty(pDecl,DP_ExternCReqd); + } + + return 0; +} + +/* +** Determine if the tokens between pFirst and pEnd form a variable +** definition or a function prototype. Return TRUE if we are dealing +** with a variable defintion and FALSE for a prototype. +** +** pEnd is the token that ends the object. It can be either a ';' or +** a '='. If it is '=', then assume we have a variable definition. +** +** If pEnd is ';', then the determination is more difficult. We have +** to search for an occurrence of an ID followed immediately by '('. +** If found, we have a prototype. Otherwise we are dealing with a +** variable definition. +*/ +static int isVariableDef(Token *pFirst, Token *pEnd){ + if( pEnd && pEnd->zText[0]=='=' && + (pEnd->pPrev->nText!=8 || strncmp(pEnd->pPrev->zText,"operator",8)!=0) + ){ + return 1; + } + while( pFirst && pFirst!=pEnd && pFirst->pNext && pFirst->pNext!=pEnd ){ + if( pFirst->eType==TT_Id && pFirst->pNext->zText[0]=='(' ){ + return 0; + } + pFirst = pFirst->pNext; + } + return 1; +} + +/* +** Return TRUE if pFirst is the first token of a static assert. +*/ +static int isStaticAssert(Token *pFirst){ + if( (pFirst->nText==13 && strncmp(pFirst->zText, "static_assert", 13)==0) + || (pFirst->nText==14 && strncmp(pFirst->zText, "_Static_assert", 14)==0) + ){ + return 1; + }else{ + return 0; + } +} + +/* +** This routine is called whenever we encounter a ";" or "=". The stuff +** between pFirst and pLast constitutes either a typedef or a global +** variable definition. Do the right thing. +*/ +static int ProcessDecl(Token *pFirst, Token *pEnd, int flags){ + Token *pName; + Decl *pDecl; + int isLocal = 0; + int isVar; + int nErr = 0; + + if( pFirst==0 || pEnd==0 ){ + return 0; + } + if( flags & PS_Typedef ){ + if( (flags & (PS_Export2|PS_Local2))!=0 ){ + fprintf(stderr,"%s:%d: \"EXPORT\" or \"LOCAL\" ignored before typedef.\n", + zFilename, pFirst->nLine); + nErr++; + } + if( (flags & (PS_Interface|PS_Export|PS_Local|DP_Cplusplus))==0 ){ + /* It is illegal to duplicate a typedef in C (but OK in C++). + ** So don't record typedefs that aren't within a C++ file or + ** within #if INTERFACE..#endif */ + return nErr; + } + if( (flags & (PS_Interface|PS_Export|PS_Local))==0 && proto_static==0 ){ + /* Ignore typedefs that are not with "#if INTERFACE..#endif" unless + ** the "-local" command line option is used. */ + return nErr; + } + if( (flags & (PS_Interface|PS_Export))==0 ){ + /* typedefs are always local, unless within #if INTERFACE..#endif */ + isLocal = 1; + } + }else if( flags & (PS_Static|PS_Local2) ){ + if( proto_static==0 && (flags & PS_Local2)==0 ){ + /* Don't record static variables unless the "-local" command line + ** option was specified or the "LOCAL" keyword is used. */ + return nErr; + } + while( pFirst!=0 && pFirst->pNext!=pEnd && + ((pFirst->nText==6 && strncmp(pFirst->zText,"static",6)==0) + || (pFirst->nText==5 && strncmp(pFirst->zText,"LOCAL",6)==0)) + ){ + /* Lose the initial "static" or local from local variables. + ** We'll prepend "extern" later. */ + pFirst = pFirst->pNext; + isLocal = 1; + } + if( pFirst==0 || !isLocal ){ + return nErr; + } + }else if( flags & PS_Method ){ + /* Methods are declared by their class. Don't declare separately. */ + return nErr; + }else if( isStaticAssert(pFirst) ){ + return 0; + } + isVar = (flags & (PS_Typedef|PS_Method))==0 && isVariableDef(pFirst,pEnd); + if( isVar && (flags & (PS_Interface|PS_Export|PS_Local))!=0 + && (flags & PS_Extern)==0 ){ + fprintf(stderr,"%s:%d: Can't define a variable in this context\n", + zFilename, pFirst->nLine); + nErr++; + } + pName = FindDeclName(pFirst,pEnd->pPrev); + if( pName==0 ){ + if( pFirst->nText==4 && strncmp(pFirst->zText,"enum",4)==0 ){ + /* Ignore completely anonymous enums. See documentation section 3.8.1. */ + return nErr; + }else{ + fprintf(stderr,"%s:%d: Can't find a name for the object declared here.\n", + zFilename, pFirst->nLine); + return nErr+1; + } + } + +#ifdef DEBUG + if( debugMask & PARSER ){ + if( flags & PS_Typedef ){ + printf("**** Found typedef %.*s at line %d...\n", + pName->nText, pName->zText, pName->nLine); + }else if( isVar ){ + printf("**** Found variable %.*s at line %d...\n", + pName->nText, pName->zText, pName->nLine); + }else{ + printf("**** Found prototype %.*s at line %d...\n", + pName->nText, pName->zText, pName->nLine); + } + PrintTokens(pFirst,pEnd->pPrev); + printf(";\n"); + } +#endif + + pDecl = CreateDecl(pName->zText,pName->nText); + if( (flags & PS_Typedef) ){ + DeclSetProperty(pDecl, TY_Typedef); + }else if( isVar ){ + DeclSetProperty(pDecl,DP_ExternReqd | TY_Variable); + if( !(flags & DP_Cplusplus) ){ + DeclSetProperty(pDecl,DP_ExternCReqd); + } + }else{ + DeclSetProperty(pDecl, TY_Subroutine); + if( !(flags & DP_Cplusplus) ){ + DeclSetProperty(pDecl,DP_ExternCReqd); + } + } + pDecl->pComment = pFirst->pComment; + pDecl->zDecl = TokensToString(pFirst,pEnd->pPrev,";\n",0,0); + if( isLocal || (flags & (PS_Local|PS_Local2))!=0 ){ + DeclSetProperty(pDecl,DP_Local); + }else if( flags & (PS_Export|PS_Export2) ){ + DeclSetProperty(pDecl,DP_Export); + } + if( flags & DP_Cplusplus ){ + DeclSetProperty(pDecl,DP_Cplusplus); + } + return nErr; +} + +/* +** Push an if condition onto the if stack +*/ +static void PushIfMacro( + const char *zPrefix, /* A prefix, like "define" or "!" */ + const char *zText, /* The condition */ + int nText, /* Number of characters in zText */ + int nLine, /* Line number where this macro occurs */ + int flags /* Either 0, PS_Interface, PS_Export or PS_Local */ +){ + Ifmacro *pIf; + int nByte; + + nByte = sizeof(Ifmacro); + if( zText ){ + if( zPrefix ){ + nByte += strlen(zPrefix) + 2; + } + nByte += nText + 1; + } + pIf = SafeMalloc( nByte ); + if( zText ){ + pIf->zCondition = (char*)&pIf[1]; + if( zPrefix ){ + int nPrefix = (int)strlen(zPrefix); + memcpy(pIf->zCondition, zPrefix, nPrefix); + pIf->zCondition[nPrefix] = '('; + memcpy(&pIf->zCondition[nPrefix+1], zText, nText); + memcpy(&pIf->zCondition[nPrefix+nText+1], ")", 2); + }else{ + memcpy(pIf->zCondition, zText, nText); + pIf->zCondition[nText] = 0; + } + }else{ + pIf->zCondition = 0; + } + pIf->nLine = nLine; + pIf->flags = flags; + pIf->pNext = ifStack; + ifStack = pIf; +} + +/* +** This routine is called to handle all preprocessor directives. +** +** This routine will recompute the value of *pPresetFlags to be the +** logical or of all flags on all nested #ifs. The #ifs that set flags +** are as follows: +** +** conditional flag set +** ------------------------ -------------------- +** #if INTERFACE PS_Interface +** #if EXPORT_INTERFACE PS_Export +** #if LOCAL_INTERFACE PS_Local +** +** For example, if after processing the preprocessor token given +** by pToken there is an "#if INTERFACE" on the preprocessor +** stack, then *pPresetFlags will be set to PS_Interface. +*/ +static int ParsePreprocessor(Token *pToken, int flags, int *pPresetFlags){ + const char *zCmd; + int nCmd; + const char *zArg; + int nArg; + int nErr = 0; + Ifmacro *pIf; + + zCmd = &pToken->zText[1]; + while( isspace(*zCmd) && *zCmd!='\n' ){ + zCmd++; + } + if( !isalpha(*zCmd) ){ + return 0; + } + nCmd = 1; + while( isalpha(zCmd[nCmd]) ){ + nCmd++; + } + + if( nCmd==5 && strncmp(zCmd,"endif",5)==0 ){ + /* + ** Pop the if stack + */ + pIf = ifStack; + if( pIf==0 ){ + fprintf(stderr,"%s:%d: extra '#endif'.\n",zFilename,pToken->nLine); + return 1; + } + ifStack = pIf->pNext; + SafeFree(pIf); + }else if( nCmd==6 && strncmp(zCmd,"define",6)==0 ){ + /* + ** Record a #define if we are in PS_Interface or PS_Export + */ + Decl *pDecl; + if( !(flags & (PS_Local|PS_Interface|PS_Export)) ){ return 0; } + zArg = &zCmd[6]; + while( *zArg && isspace(*zArg) && *zArg!='\n' ){ + zArg++; + } + if( *zArg==0 || *zArg=='\n' ){ return 0; } + for(nArg=0; ISALNUM(zArg[nArg]); nArg++){} + if( nArg==0 ){ return 0; } + pDecl = CreateDecl(zArg,nArg); + pDecl->pComment = pToken->pComment; + DeclSetProperty(pDecl,TY_Macro); + pDecl->zDecl = SafeMalloc( pToken->nText + 2 ); + memcpy(pDecl->zDecl, pToken->zText, pToken->nText); + memcpy(&pDecl->zDecl[pToken->nText], "\n", 2); + if( flags & PS_Export ){ + DeclSetProperty(pDecl,DP_Export); + }else if( flags & PS_Local ){ + DeclSetProperty(pDecl,DP_Local); + } + }else if( nCmd==7 && strncmp(zCmd,"include",7)==0 ){ + /* + ** Record an #include if we are in PS_Interface or PS_Export + */ + Include *pInclude; + char *zIf; + + if( !(flags & (PS_Interface|PS_Export)) ){ return 0; } + zArg = &zCmd[7]; + while( *zArg && isspace(*zArg) ){ zArg++; } + for(nArg=0; !isspace(zArg[nArg]); nArg++){} + if( (zArg[0]=='"' && zArg[nArg-1]!='"') + ||(zArg[0]=='<' && zArg[nArg-1]!='>') + ){ + fprintf(stderr,"%s:%d: malformed #include statement.\n", + zFilename,pToken->nLine); + return 1; + } + zIf = GetIfString(); + if( zIf ){ + pInclude = SafeMalloc( sizeof(Include) + nArg*2 + strlen(zIf) + 10 ); + pInclude->zFile = (char*)&pInclude[1]; + pInclude->zLabel = &pInclude->zFile[nArg+1]; + memcpy(pInclude->zFile, zArg, nArg); + pInclude->zFile[nArg] = 0; + memcpy(pInclude->zLabel, zArg, nArg); + pInclude->zLabel[nArg] = ':'; + memcpy(&pInclude->zLabel[nArg+1], zIf, strlen(zIf)+1); + pInclude->zIf = &pInclude->zLabel[nArg+1]; + SafeFree(zIf); + }else{ + pInclude = SafeMalloc( sizeof(Include) + nArg + 1 ); + pInclude->zFile = (char*)&pInclude[1]; + memcpy(pInclude->zFile, zArg, nArg); + pInclude->zFile[nArg] = 0; + pInclude->zIf = 0; + pInclude->zLabel = pInclude->zFile; + } + pInclude->pNext = includeList; + includeList = pInclude; + }else if( nCmd==2 && strncmp(zCmd,"if",2)==0 ){ + /* + ** Push an #if. Watch for the special cases of INTERFACE + ** and EXPORT_INTERFACE and LOCAL_INTERFACE + */ + zArg = &zCmd[2]; + while( *zArg && isspace(*zArg) && *zArg!='\n' ){ + zArg++; + } + if( *zArg==0 || *zArg=='\n' ){ return 0; } + nArg = pToken->nText + (int)(pToken->zText - zArg); + if (pToken->zText[pToken->nText-1] == '\r') { nArg--; } + if( nArg==9 && strncmp(zArg,"INTERFACE",9)==0 ){ + PushIfMacro(0,0,0,pToken->nLine,PS_Interface); + }else if( nArg==16 && strncmp(zArg,"EXPORT_INTERFACE",16)==0 ){ + PushIfMacro(0,0,0,pToken->nLine,PS_Export); + }else if( nArg==15 && strncmp(zArg,"LOCAL_INTERFACE",15)==0 ){ + PushIfMacro(0,0,0,pToken->nLine,PS_Local); + }else if( nArg==15 && + strncmp(zArg,"MAKEHEADERS_STOPLOCAL_INTERFACE",15)==0 ){ + PushIfMacro(0,0,0,pToken->nLine,PS_Local); + }else{ + PushIfMacro(0,zArg,nArg,pToken->nLine,0); + } + }else if( nCmd==5 && strncmp(zCmd,"ifdef",5)==0 ){ + /* + ** Push an #ifdef. + */ + zArg = &zCmd[5]; + while( *zArg && isspace(*zArg) && *zArg!='\n' ){ + zArg++; + } + if( *zArg==0 || *zArg=='\n' ){ return 0; } + nArg = pToken->nText + (int)(pToken->zText - zArg); + if (pToken->zText[pToken->nText-1] == '\r') { nArg--; } + PushIfMacro("defined",zArg,nArg,pToken->nLine,0); + }else if( nCmd==6 && strncmp(zCmd,"ifndef",6)==0 ){ + /* + ** Push an #ifndef. + */ + zArg = &zCmd[6]; + while( *zArg && isspace(*zArg) && *zArg!='\n' ){ + zArg++; + } + if( *zArg==0 || *zArg=='\n' ){ return 0; } + nArg = pToken->nText + (int)(pToken->zText - zArg); + if (pToken->zText[pToken->nText-1] == '\r') { nArg--; } + PushIfMacro("!defined",zArg,nArg,pToken->nLine,0); + }else if( nCmd==4 && strncmp(zCmd,"else",4)==0 ){ + /* + ** Invert the #if on the top of the stack + */ + if( ifStack==0 ){ + fprintf(stderr,"%s:%d: '#else' without an '#if'\n",zFilename, + pToken->nLine); + return 1; + } + pIf = ifStack; + if( pIf->zCondition ){ + ifStack = ifStack->pNext; + PushIfMacro("!",pIf->zCondition,strlen(pIf->zCondition),pIf->nLine,0); + SafeFree(pIf); + }else{ + pIf->flags = 0; + } + }else{ + /* + ** This directive can be safely ignored + */ + return 0; + } + + /* + ** Recompute the preset flags + */ + *pPresetFlags = 0; + for(pIf = ifStack; pIf; pIf=pIf->pNext){ + *pPresetFlags |= pIf->flags; + } + + return nErr; +} + +/* +** Parse an entire file. Return the number of errors. +** +** pList is a list of tokens in the file. Whitespace tokens have been +** eliminated, and text with {...} has been collapsed into a +** single TT_Brace token. +** +** initFlags are a set of parse flags that should always be set for this +** file. For .c files this is normally 0. For .h files it is PS_Interface. +*/ +static int ParseFile(Token *pList, int initFlags){ + int nErr = 0; + Token *pStart = 0; + int flags = initFlags; + int presetFlags = initFlags; + int resetFlag = 0; + + includeList = 0; + while( pList ){ + switch( pList->eType ){ + case TT_EOF: + goto end_of_loop; + + case TT_Preprocessor: + nErr += ParsePreprocessor(pList,flags,&presetFlags); + pStart = 0; + presetFlags |= initFlags; + flags = presetFlags; + break; + + case TT_Other: + switch( pList->zText[0] ){ + case ';': + nErr += ProcessDecl(pStart,pList,flags); + pStart = 0; + flags = presetFlags; + break; + + case '=': + if( pList->pPrev->nText==8 + && strncmp(pList->pPrev->zText,"operator",8)==0 ){ + break; + } + nErr += ProcessDecl(pStart,pList,flags); + pStart = 0; + while( pList && pList->zText[0]!=';' ){ + pList = pList->pNext; + } + if( pList==0 ) goto end_of_loop; + flags = presetFlags; + break; + + case ':': + if( pList->zText[1]==':' ){ + flags |= PS_Method; + } + break; + + default: + break; + } + break; + + case TT_Braces: + nErr += ProcessProcedureDef(pStart,pList,flags); + pStart = 0; + flags = presetFlags; + break; + + case TT_Id: + if( pStart==0 ){ + pStart = pList; + flags = presetFlags; + } + resetFlag = 0; + switch( pList->zText[0] ){ + case 'c': + if( pList->nText==5 && strncmp(pList->zText,"class",5)==0 ){ + nErr += ProcessTypeDecl(pList,flags,&resetFlag); + } + break; + + case 'E': + if( pList->nText==6 && strncmp(pList->zText,"EXPORT",6)==0 ){ + flags |= PS_Export2; + /* pStart = 0; */ + } + break; + + case 'e': + if( pList->nText==4 && strncmp(pList->zText,"enum",4)==0 ){ + if( pList->pNext && pList->pNext->eType==TT_Braces ){ + pList = pList->pNext; + }else{ + nErr += ProcessTypeDecl(pList,flags,&resetFlag); + } + }else if( pList->nText==6 && strncmp(pList->zText,"extern",6)==0 ){ + pList = pList->pNext; + if( pList && pList->nText==3 && strncmp(pList->zText,"\"C\"",3)==0 ){ + pList = pList->pNext; + flags &= ~DP_Cplusplus; + }else{ + flags |= PS_Extern; + } + pStart = pList; + } + break; + + case 'i': + if( pList->nText==6 && strncmp(pList->zText,"inline",6)==0 + && (flags & PS_Static)==0 + ){ + nErr += ProcessInlineProc(pList,flags,&resetFlag); + } + break; + + case 'L': + if( pList->nText==5 && strncmp(pList->zText,"LOCAL",5)==0 ){ + flags |= PS_Local2; + pStart = pList; + } + break; + + case 'P': + if( pList->nText==6 && strncmp(pList->zText, "PUBLIC",6)==0 ){ + flags |= PS_Public; + pStart = pList; + }else if( pList->nText==7 && strncmp(pList->zText, "PRIVATE",7)==0 ){ + flags |= PS_Private; + pStart = pList; + }else if( pList->nText==9 && strncmp(pList->zText,"PROTECTED",9)==0 ){ + flags |= PS_Protected; + pStart = pList; + } + break; + + case 's': + if( pList->nText==6 && strncmp(pList->zText,"struct",6)==0 ){ + if( pList->pNext && pList->pNext->eType==TT_Braces ){ + pList = pList->pNext; + }else{ + nErr += ProcessTypeDecl(pList,flags,&resetFlag); + } + }else if( pList->nText==6 && strncmp(pList->zText,"static",6)==0 ){ + flags |= PS_Static; + } + break; + + case 't': + if( pList->nText==7 && strncmp(pList->zText,"typedef",7)==0 ){ + flags |= PS_Typedef; + } + break; + + case 'u': + if( pList->nText==5 && strncmp(pList->zText,"union",5)==0 ){ + if( pList->pNext && pList->pNext->eType==TT_Braces ){ + pList = pList->pNext; + }else{ + nErr += ProcessTypeDecl(pList,flags,&resetFlag); + } + } + break; + + default: + break; + } + if( resetFlag!=0 ){ + while( pList && pList->zText[0]!=resetFlag ){ + pList = pList->pNext; + } + if( pList==0 ) goto end_of_loop; + pStart = 0; + flags = presetFlags; + } + break; + + case TT_String: + case TT_Number: + break; + + default: + pStart = pList; + flags = presetFlags; + break; + } + pList = pList->pNext; + } + end_of_loop: + + /* Verify that all #ifs have a matching "#endif" */ + while( ifStack ){ + Ifmacro *pIf = ifStack; + ifStack = pIf->pNext; + fprintf(stderr,"%s:%d: This '#if' has no '#endif'\n",zFilename, + pIf->nLine); + SafeFree(pIf); + } + + return nErr; +} + +/* +** If the given Decl object has a non-null zExtra field, then the text +** of that zExtra field needs to be inserted in the middle of the +** zDecl field before the last "}" in the zDecl. This routine does that. +** If the zExtra is NULL, this routine is a no-op. +** +** zExtra holds extra method declarations for classes. The declarations +** have to be inserted into the class definition. +*/ +static void InsertExtraDecl(Decl *pDecl){ + int i; + String str; + + if( pDecl==0 || pDecl->zExtra==0 || pDecl->zDecl==0 ) return; + i = strlen(pDecl->zDecl) - 1; + while( i>0 && pDecl->zDecl[i]!='}' ){ i--; } + StringInit(&str); + StringAppend(&str, pDecl->zDecl, i); + StringAppend(&str, pDecl->zExtra, 0); + StringAppend(&str, &pDecl->zDecl[i], 0); + SafeFree(pDecl->zDecl); + SafeFree(pDecl->zExtra); + pDecl->zDecl = StrDup(StringGet(&str), 0); + StringReset(&str); + pDecl->zExtra = 0; +} + +/* +** Reset the DP_Forward and DP_Declared flags on all Decl structures. +** Set both flags for anything that is tagged as local and isn't +** in the file zFilename so that it won't be printing in other files. +*/ +static void ResetDeclFlags(char *zFilename){ + Decl *pDecl; + + for(pDecl = pDeclFirst; pDecl; pDecl = pDecl->pNext){ + DeclClearProperty(pDecl,DP_Forward|DP_Declared); + if( DeclHasProperty(pDecl,DP_Local) && pDecl->zFile!=zFilename ){ + DeclSetProperty(pDecl,DP_Forward|DP_Declared); + } + } +} + +/* +** Forward declaration of the ScanText() function. +*/ +static void ScanText(const char*, GenState *pState); + +/* +** The output in pStr is currently within an #if CONTEXT where context +** is equal to *pzIf. (*pzIf might be NULL to indicate that we are +** not within any #if at the moment.) We are getting ready to output +** some text that needs to be within the context of "#if NEW" where +** NEW is zIf. Make an appropriate change to the context. +*/ +static void ChangeIfContext( + const char *zIf, /* The desired #if context */ + GenState *pState /* Current state of the code generator */ +){ + if( zIf==0 ){ + if( pState->zIf==0 ) return; + StringAppend(pState->pStr,"#endif\n",0); + pState->zIf = 0; + }else{ + if( pState->zIf ){ + if( strcmp(zIf,pState->zIf)==0 ) return; + StringAppend(pState->pStr,"#endif\n",0); + pState->zIf = 0; + } + ScanText(zIf, pState); + if( pState->zIf!=0 ){ + StringAppend(pState->pStr,"#endif\n",0); + } + StringAppend(pState->pStr,"#if ",0); + StringAppend(pState->pStr,zIf,0); + StringAppend(pState->pStr,"\n",0); + pState->zIf = zIf; + } +} + +/* +** Add to the string pStr a #include of every file on the list of +** include files pInclude. The table pTable contains all files that +** have already been #included at least once. Don't add any +** duplicates. Update pTable with every new #include that is added. +*/ +static void AddIncludes( + Include *pInclude, /* Write every #include on this list */ + GenState *pState /* Current state of the code generator */ +){ + if( pInclude ){ + if( pInclude->pNext ){ + AddIncludes(pInclude->pNext,pState); + } + if( IdentTableInsert(pState->pTable,pInclude->zLabel,0) ){ + ChangeIfContext(pInclude->zIf,pState); + StringAppend(pState->pStr,"#include ",0); + StringAppend(pState->pStr,pInclude->zFile,0); + StringAppend(pState->pStr,"\n",1); + } + } +} + +/* +** Add to the string pStr a declaration for the object described +** in pDecl. +** +** If pDecl has already been declared in this file, detect that +** fact and abort early. Do not duplicate a declaration. +** +** If the needFullDecl flag is false and this object has a forward +** declaration, then supply the forward declaration only. A later +** call to CompleteForwardDeclarations() will finish the declaration +** for us. But if needFullDecl is true, we must supply the full +** declaration now. Some objects do not have a forward declaration. +** For those objects, we must print the full declaration now. +** +** Because it is illegal to duplicate a typedef in C, care is taken +** to insure that typedefs for the same identifier are only issued once. +*/ +static void DeclareObject( + Decl *pDecl, /* The thing to be declared */ + GenState *pState, /* Current state of the code generator */ + int needFullDecl /* Must have the full declaration. A forward + * declaration isn't enough */ +){ + Decl *p; /* The object to be declared */ + int flag; + int isCpp; /* True if generating C++ */ + int doneTypedef = 0; /* True if a typedef has been done for this object */ + + /* printf("BEGIN %s of %s\n",needFullDecl?"FULL":"PROTOTYPE",pDecl->zName);*/ + /* + ** For any object that has a forward declaration, go ahead and do the + ** forward declaration first. + */ + isCpp = (pState->flags & DP_Cplusplus) != 0; + for(p=pDecl; p; p=p->pSameName){ + if( p->zFwd ){ + if( !DeclHasProperty(p,DP_Forward) ){ + DeclSetProperty(p,DP_Forward); + if( strncmp(p->zFwd,"typedef",7)==0 ){ + if( doneTypedef ) continue; + doneTypedef = 1; + } + ChangeIfContext(p->zIf,pState); + StringAppend(pState->pStr,isCpp ? p->zFwdCpp : p->zFwd,0); + } + } + } + + /* + ** Early out if everything is already suitably declared. + ** + ** This is a very important step because it prevents us from + ** executing the code the follows in a recursive call to this + ** function with the same value for pDecl. + */ + flag = needFullDecl ? DP_Declared|DP_Forward : DP_Forward; + for(p=pDecl; p; p=p->pSameName){ + if( !DeclHasProperty(p,flag) ) break; + } + if( p==0 ){ + return; + } + + /* + ** Make sure we have all necessary #includes + */ + for(p=pDecl; p; p=p->pSameName){ + AddIncludes(p->pInclude,pState); + } + + /* + ** Go ahead an mark everything as being declared, to prevent an + ** infinite loop thru the ScanText() function. At the same time, + ** we decide which objects need a full declaration and mark them + ** with the DP_Flag bit. We are only able to use DP_Flag in this + ** way because we know we'll never execute this far into this + ** function on a recursive call with the same pDecl. Hence, recursive + ** calls to this function (through ScanText()) can never change the + ** value of DP_Flag out from under us. + */ + for(p=pDecl; p; p=p->pSameName){ + if( !DeclHasProperty(p,DP_Declared) + && (p->zFwd==0 || needFullDecl) + && p->zDecl!=0 + ){ + DeclSetProperty(p,DP_Forward|DP_Declared|DP_Flag); + }else{ + DeclClearProperty(p,DP_Flag); + } + } + + /* + ** Call ScanText() recursively (this routine is called from ScanText()) + ** to include declarations required to come before these declarations. + */ + for(p=pDecl; p; p=p->pSameName){ + if( DeclHasProperty(p,DP_Flag) ){ + if( p->zDecl[0]=='#' ){ + ScanText(&p->zDecl[1],pState); + }else{ + InsertExtraDecl(p); + ScanText(p->zDecl,pState); + } + } + } + + /* + ** Output the declarations. Do this in two passes. First + ** output everything that isn't a typedef. Then go back and + ** get the typedefs by the same name. + */ + for(p=pDecl; p; p=p->pSameName){ + if( DeclHasProperty(p,DP_Flag) && !DeclHasProperty(p,TY_Typedef) ){ + if( DeclHasAnyProperty(p,TY_Enumeration) ){ + if( doneTypedef ) continue; + doneTypedef = 1; + } + ChangeIfContext(p->zIf,pState); + if( !isCpp && DeclHasAnyProperty(p,DP_ExternReqd) ){ + StringAppend(pState->pStr,"extern ",0); + }else if( isCpp && DeclHasProperty(p,DP_Cplusplus|DP_ExternReqd) ){ + StringAppend(pState->pStr,"extern ",0); + }else if( isCpp && DeclHasAnyProperty(p,DP_ExternCReqd|DP_ExternReqd) ){ + StringAppend(pState->pStr,"extern \"C\" ",0); + } + InsertExtraDecl(p); + StringAppend(pState->pStr,p->zDecl,0); + if( !isCpp && DeclHasProperty(p,DP_Cplusplus) ){ + fprintf(stderr, + "%s: C code ought not reference the C++ object \"%s\"\n", + pState->zFilename, p->zName); + pState->nErr++; + } + DeclClearProperty(p,DP_Flag); + } + } + for(p=pDecl; p && !doneTypedef; p=p->pSameName){ + if( DeclHasProperty(p,DP_Flag) ){ + /* This has to be a typedef */ + doneTypedef = 1; + ChangeIfContext(p->zIf,pState); + InsertExtraDecl(p); + StringAppend(pState->pStr,p->zDecl,0); + } + } +} + +/* +** This routine scans the input text given, and appends to the +** string in pState->pStr the text of any declarations that must +** occur before the text in zText. +** +** If an identifier in zText is immediately followed by '*', then +** only forward declarations are needed for that identifier. If the +** identifier name is not followed immediately by '*', we must supply +** a full declaration. +*/ +static void ScanText( + const char *zText, /* The input text to be scanned */ + GenState *pState /* Current state of the code generator */ +){ + int nextValid = 0; /* True is sNext contains valid data */ + InStream sIn; /* The input text */ + Token sToken; /* The current token being examined */ + Token sNext; /* The next non-space token */ + + /* printf("BEGIN SCAN TEXT on %s\n", zText); */ + + sIn.z = zText; + sIn.i = 0; + sIn.nLine = 1; + while( sIn.z[sIn.i]!=0 ){ + if( nextValid ){ + sToken = sNext; + nextValid = 0; + }else{ + GetNonspaceToken(&sIn,&sToken); + } + if( sToken.eType==TT_Id ){ + int needFullDecl; /* True if we need to provide the full declaration, + ** not just the forward declaration */ + Decl *pDecl; /* The declaration having the name in sToken */ + + /* + ** See if there is a declaration in the database with the name given + ** by sToken. + */ + pDecl = FindDecl(sToken.zText,sToken.nText); + if( pDecl==0 ) continue; + + /* + ** If we get this far, we've found an identifier that has a + ** declaration in the database. Now see if we the full declaration + ** or just a forward declaration. + */ + GetNonspaceToken(&sIn,&sNext); + if( sNext.zText[0]=='*' ){ + needFullDecl = 0; + }else{ + needFullDecl = 1; + nextValid = sNext.eType==TT_Id; + } + + /* + ** Generate the needed declaration. + */ + DeclareObject(pDecl,pState,needFullDecl); + }else if( sToken.eType==TT_Preprocessor ){ + sIn.i -= sToken.nText - 1; + } + } + /* printf("END SCANTEXT\n"); */ +} + +/* +** Provide a full declaration to any object which so far has had only +** a forward declaration. +*/ +static void CompleteForwardDeclarations(GenState *pState){ + Decl *pDecl; + int progress; + + do{ + progress = 0; + for(pDecl=pDeclFirst; pDecl; pDecl=pDecl->pNext){ + if( DeclHasProperty(pDecl,DP_Forward) + && !DeclHasProperty(pDecl,DP_Declared) + ){ + DeclareObject(pDecl,pState,1); + progress = 1; + assert( DeclHasProperty(pDecl,DP_Declared) ); + } + } + }while( progress ); +} + +/* +** Generate an include file for the given source file. Return the number +** of errors encountered. +** +** if nolocal_flag is true, then we do not generate declarations for +** objected marked DP_Local. +*/ +static int MakeHeader(InFile *pFile, FILE *report, int nolocal_flag){ + int nErr = 0; + GenState sState; + String outStr; + IdentTable includeTable; + Ident *pId; + char *zNewVersion; + char *zOldVersion; + + if( pFile->zHdr==0 || *pFile->zHdr==0 ) return 0; + sState.pStr = &outStr; + StringInit(&outStr); + StringAppend(&outStr,zTopLine,nTopLine); + sState.pTable = &includeTable; + memset(&includeTable,0,sizeof(includeTable)); + sState.zIf = 0; + sState.nErr = 0; + sState.zFilename = pFile->zSrc; + sState.flags = pFile->flags & DP_Cplusplus; + ResetDeclFlags(nolocal_flag ? "no" : pFile->zSrc); + for(pId = pFile->idTable.pList; pId; pId=pId->pNext){ + Decl *pDecl = FindDecl(pId->zName,0); + if( pDecl ){ + DeclareObject(pDecl,&sState,1); + } + } + CompleteForwardDeclarations(&sState); + ChangeIfContext(0,&sState); + nErr += sState.nErr; + zOldVersion = ReadFile(pFile->zHdr); + zNewVersion = StringGet(&outStr); + if( report ) fprintf(report,"%s: ",pFile->zHdr); + if( zOldVersion==0 ){ + if( report ) fprintf(report,"updated\n"); + if( WriteFile(pFile->zHdr,zNewVersion) ){ + fprintf(stderr,"%s: Can't write to file\n",pFile->zHdr); + nErr++; + } + }else if( strncmp(zOldVersion,zTopLine,nTopLine)!=0 ){ + if( report ) fprintf(report,"error!\n"); + fprintf(stderr, + "%s: Can't overwrite this file because it wasn't previously\n" + "%*s generated by 'makeheaders'.\n", + pFile->zHdr, (int)strlen(pFile->zHdr), ""); + nErr++; + }else if( strcmp(zOldVersion,zNewVersion)!=0 ){ + if( report ) fprintf(report,"updated\n"); + if( WriteFile(pFile->zHdr,zNewVersion) ){ + fprintf(stderr,"%s: Can't write to file\n",pFile->zHdr); + nErr++; + } + }else if( report ){ + fprintf(report,"unchanged\n"); + } + SafeFree(zOldVersion); + IdentTableReset(&includeTable); + StringReset(&outStr); + return nErr; +} + +/* +** Generate a global header file -- a header file that contains all +** declarations. If the forExport flag is true, then only those +** objects that are exported are included in the header file. +*/ +static int MakeGlobalHeader(int forExport){ + GenState sState; + String outStr; + IdentTable includeTable; + Decl *pDecl; + + sState.pStr = &outStr; + StringInit(&outStr); + /* StringAppend(&outStr,zTopLine,nTopLine); */ + sState.pTable = &includeTable; + memset(&includeTable,0,sizeof(includeTable)); + sState.zIf = 0; + sState.nErr = 0; + sState.zFilename = "(all)"; + sState.flags = 0; + ResetDeclFlags(0); + for(pDecl=pDeclFirst; pDecl; pDecl=pDecl->pNext){ + if( forExport==0 || DeclHasProperty(pDecl,DP_Export) ){ + DeclareObject(pDecl,&sState,1); + } + } + ChangeIfContext(0,&sState); + printf("%s",StringGet(&outStr)); + IdentTableReset(&includeTable); + StringReset(&outStr); + return 0; +} + +#ifdef DEBUG +/* +** Return the number of characters in the given string prior to the +** first newline. +*/ +static int ClipTrailingNewline(char *z){ + int n = strlen(z); + while( n>0 && (z[n-1]=='\n' || z[n-1]=='\r') ){ n--; } + return n; +} + +/* +** Dump the entire declaration list for debugging purposes +*/ +static void DumpDeclList(void){ + Decl *pDecl; + + for(pDecl = pDeclFirst; pDecl; pDecl=pDecl->pNext){ + printf("**** %s from file %s ****\n",pDecl->zName,pDecl->zFile); + if( pDecl->zIf ){ + printf("If: [%.*s]\n",ClipTrailingNewline(pDecl->zIf),pDecl->zIf); + } + if( pDecl->zFwd ){ + printf("Decl: [%.*s]\n",ClipTrailingNewline(pDecl->zFwd),pDecl->zFwd); + } + if( pDecl->zDecl ){ + InsertExtraDecl(pDecl); + printf("Def: [%.*s]\n",ClipTrailingNewline(pDecl->zDecl),pDecl->zDecl); + } + if( pDecl->flags ){ + static struct { + int mask; + char *desc; + } flagSet[] = { + { TY_Class, "class" }, + { TY_Enumeration, "enum" }, + { TY_Structure, "struct" }, + { TY_Union, "union" }, + { TY_Variable, "variable" }, + { TY_Subroutine, "function" }, + { TY_Typedef, "typedef" }, + { TY_Macro, "macro" }, + { DP_Export, "export" }, + { DP_Local, "local" }, + { DP_Cplusplus, "C++" }, + }; + int i; + printf("flags:"); + for(i=0; i<sizeof(flagSet)/sizeof(flagSet[0]); i++){ + if( flagSet[i].mask & pDecl->flags ){ + printf(" %s", flagSet[i].desc); + } + } + printf("\n"); + } + if( pDecl->pInclude ){ + Include *p; + printf("includes:"); + for(p=pDecl->pInclude; p; p=p->pNext){ + printf(" %s",p->zFile); + } + printf("\n"); + } + } +} +#endif + +/* +** When the "-doc" command-line option is used, this routine is called +** to print all of the database information to standard output. +*/ +static void DocumentationDump(void){ + Decl *pDecl; + static struct { + int mask; + char flag; + } flagSet[] = { + { TY_Class, 'c' }, + { TY_Enumeration, 'e' }, + { TY_Structure, 's' }, + { TY_Union, 'u' }, + { TY_Variable, 'v' }, + { TY_Subroutine, 'f' }, + { TY_Typedef, 't' }, + { TY_Macro, 'm' }, + { DP_Export, 'x' }, + { DP_Local, 'l' }, + { DP_Cplusplus, '+' }, + }; + + for(pDecl = pDeclFirst; pDecl; pDecl=pDecl->pNext){ + int i; + int nLabel = 0; + char *zDecl; + char zLabel[50]; + for(i=0; i<sizeof(flagSet)/sizeof(flagSet[0]); i++){ + if( DeclHasProperty(pDecl,flagSet[i].mask) ){ + zLabel[nLabel++] = flagSet[i].flag; + } + } + if( nLabel==0 ) continue; + zLabel[nLabel] = 0; + InsertExtraDecl(pDecl); + zDecl = pDecl->zDecl; + if( zDecl==0 ) zDecl = pDecl->zFwd; + printf("%s %s %s %p %d %d %d %d %d\n", + pDecl->zName, + zLabel, + pDecl->zFile, + pDecl->pComment, + pDecl->pComment ? pDecl->pComment->nText+1 : 0, + pDecl->zIf ? (int)strlen(pDecl->zIf)+1 : 0, + zDecl ? (int)strlen(zDecl) : 0, + pDecl->pComment ? pDecl->pComment->nLine : 0, + pDecl->tokenCode.nText ? pDecl->tokenCode.nText+1 : 0 + ); + if( pDecl->pComment ){ + printf("%.*s\n",pDecl->pComment->nText, pDecl->pComment->zText); + } + if( pDecl->zIf ){ + printf("%s\n",pDecl->zIf); + } + if( zDecl ){ + printf("%s",zDecl); + } + if( pDecl->tokenCode.nText ){ + printf("%.*s\n",pDecl->tokenCode.nText, pDecl->tokenCode.zText); + } + } +} + +/* +** Given the complete text of an input file, this routine prints a +** documentation record for the header comment at the beginning of the +** file (if the file has a header comment.) +*/ +void PrintModuleRecord(const char *zFile, const char *zFilename){ + int i; + static int addr = 5; + while( isspace(*zFile) ){ zFile++; } + if( *zFile!='/' || zFile[1]!='*' ) return; + for(i=2; zFile[i] && (zFile[i-1]!='/' || zFile[i-2]!='*'); i++){} + if( zFile[i]==0 ) return; + printf("%s M %s %d %d 0 0 0 0\n%.*s\n", + zFilename, zFilename, addr, i+1, i, zFile); + addr += 4; +} + + +/* +** Given an input argument to the program, construct a new InFile +** object. +*/ +static InFile *CreateInFile(char *zArg, int *pnErr){ + int nSrc; + char *zSrc; + InFile *pFile; + int i; + + /* + ** Get the name of the input file to be scanned. The input file is + ** everything before the first ':' or the whole file if no ':' is seen. + ** + ** Except, on windows, ignore any ':' that occurs as the second character + ** since it might be part of the drive specifier. So really, the ":' has + ** to be the 3rd or later character in the name. This precludes 1-character + ** file names, which really should not be a problem. + */ + zSrc = zArg; + for(nSrc=2; zSrc[nSrc] && zArg[nSrc]!=':'; nSrc++){} + pFile = SafeMalloc( sizeof(InFile) ); + memset(pFile,0,sizeof(InFile)); + pFile->zSrc = StrDup(zSrc,nSrc); + + /* Figure out if we are dealing with C or C++ code. Assume any + ** file with ".c" or ".h" is C code and all else is C++. + */ + if( nSrc>2 && zSrc[nSrc-2]=='.' && (zSrc[nSrc-1]=='c' || zSrc[nSrc-1]=='h')){ + pFile->flags &= ~DP_Cplusplus; + }else{ + pFile->flags |= DP_Cplusplus; + } + + /* + ** If a separate header file is specified, use it + */ + if( zSrc[nSrc]==':' ){ + int nHdr; + char *zHdr; + zHdr = &zSrc[nSrc+1]; + for(nHdr=0; zHdr[nHdr]; nHdr++){} + pFile->zHdr = StrDup(zHdr,nHdr); + } + + /* Look for any 'c' or 'C' in the suffix of the file name and change + ** that character to 'h' or 'H' respectively. If no 'c' or 'C' is found, + ** then assume we are dealing with a header. + */ + else{ + int foundC = 0; + pFile->zHdr = StrDup(zSrc,nSrc); + for(i = nSrc-1; i>0 && pFile->zHdr[i]!='.'; i--){ + if( pFile->zHdr[i]=='c' ){ + foundC = 1; + pFile->zHdr[i] = 'h'; + }else if( pFile->zHdr[i]=='C' ){ + foundC = 1; + pFile->zHdr[i] = 'H'; + } + } + if( !foundC ){ + SafeFree(pFile->zHdr); + pFile->zHdr = 0; + } + } + + /* + ** If pFile->zSrc contains no 'c' or 'C' in its extension, it + ** must be a header file. In that case, we need to set the + ** PS_Interface flag. + */ + pFile->flags |= PS_Interface; + for(i=nSrc-1; i>0 && zSrc[i]!='.'; i--){ + if( zSrc[i]=='c' || zSrc[i]=='C' ){ + pFile->flags &= ~PS_Interface; + break; + } + } + + /* Done! + */ + return pFile; +} + +/* Local strcpy() clone to squelch an unwarranted warning from OpenBSD. */ +static void local_strcpy(char *dest, const char *src){ + while( (*(dest++) = *(src++))!=0 ){} +} + +/* MS-Windows and MS-DOS both have the following serious OS bug: the +** length of a command line is severely restricted. But this program +** occasionally requires long command lines. Hence the following +** work around. +** +** If the parameters "-f FILENAME" appear anywhere on the command line, +** then the named file is scanned for additional command line arguments. +** These arguments are substituted in place of the "FILENAME" argument +** in the original argument list. +** +** This first parameter to this routine is the index of the "-f" +** parameter in the argv[] array. The argc and argv are passed by +** pointer so that they can be changed. +** +** Parsing of the parameters in the file is very simple. Parameters +** can be separated by any amount of white-space (including newlines +** and carriage returns.) There are now quoting characters of any +** kind. The length of a token is limited to about 1000 characters. +*/ +static void AddParameters(int index, int *pArgc, char ***pArgv){ + int argc = *pArgc; /* The original argc value */ + char **argv = *pArgv; /* The original argv value */ + int newArgc; /* Value for argc after inserting new arguments */ + char **zNew = 0; /* The new argv after this routine is done */ + char *zFile; /* Name of the input file */ + int nNew = 0; /* Number of new entries in the argv[] file */ + int nAlloc = 0; /* Space allocated for zNew[] */ + int i; /* Loop counter */ + int n; /* Number of characters in a new argument */ + int c; /* Next character of input */ + int startOfLine = 1; /* True if we are where '#' can start a comment */ + FILE *in; /* The input file */ + char zBuf[1000]; /* A single argument is accumulated here */ + + if( index+1==argc ) return; + zFile = argv[index+1]; + in = fopen(zFile,"r"); + if( in==0 ){ + fprintf(stderr,"Can't open input file \"%s\"\n",zFile); + exit(1); + } + c = ' '; + while( c!=EOF ){ + while( c!=EOF && isspace(c) ){ + if( c=='\n' ){ + startOfLine = 1; + } + c = getc(in); + if( startOfLine && c=='#' ){ + while( c!=EOF && c!='\n' ){ + c = getc(in); + } + } + } + n = 0; + while( c!=EOF && !isspace(c) ){ + if( n<sizeof(zBuf)-1 ){ zBuf[n++] = c; } + startOfLine = 0; + c = getc(in); + } + zBuf[n] = 0; + if( n>0 ){ + nNew++; + if( nNew + argc > nAlloc ){ + if( nAlloc==0 ){ + nAlloc = 100 + argc; + zNew = malloc( sizeof(char*) * nAlloc ); + }else{ + nAlloc *= 2; + zNew = realloc( zNew, sizeof(char*) * nAlloc ); + } + } + if( zNew ){ + int j = nNew + index; + zNew[j] = malloc( n + 1 ); + if( zNew[j] ){ + local_strcpy( zNew[j], zBuf ); + } + } + } + } + fclose(in); + newArgc = argc + nNew - 1; + for(i=0; i<=index; i++){ + zNew[i] = argv[i]; + } + for(i=nNew + index + 1; i<newArgc; i++){ + zNew[i] = argv[i + 1 - nNew]; + } + zNew[newArgc] = 0; + *pArgc = newArgc; + *pArgv = zNew; +} + +#ifdef NOT_USED +/* +** Return the time that the given file was last modified. If we can't +** locate the file (because, for example, it doesn't exist), then +** return 0. +*/ +static unsigned int ModTime(const char *zFilename){ + unsigned int mTime = 0; + struct stat sStat; + if( stat(zFilename,&sStat)==0 ){ + mTime = sStat.st_mtime; + } + return mTime; +} +#endif + +/* +** Print a usage comment for this program. +*/ +static void Usage(const char *argv0, const char *argvN){ + fprintf(stderr,"%s: Illegal argument \"%s\"\n",argv0,argvN); + fprintf(stderr,"Usage: %s [options] filename...\n" + "Options:\n" + " -h Generate a single .h to standard output.\n" + " -H Like -h, but only output EXPORT declarations.\n" + " -v (verbose) Write status information to the screen.\n" + " -doc Generate no header files. Instead, output information\n" + " that can be used by an automatic program documentation\n" + " and cross-reference generator.\n" + " -local Generate prototypes for \"static\" functions and\n" + " procedures.\n" + " -f FILE Read additional command-line arguments from the file named\n" + " \"FILE\".\n" +#ifdef DEBUG + " -! MASK Set the debugging mask to the number \"MASK\".\n" +#endif + " -- Treat all subsequent comment-line parameters as filenames,\n" + " even if they begin with \"-\".\n", + argv0 + ); +} + +/* +** The following text contains a few simple #defines that we want +** to be available to every file. +*/ +static const char zInit[] = + "#define INTERFACE 0\n" + "#define EXPORT_INTERFACE 0\n" + "#define LOCAL_INTERFACE 0\n" + "#define EXPORT\n" + "#define LOCAL static\n" + "#define PUBLIC\n" + "#define PRIVATE\n" + "#define PROTECTED\n" +; + +#if TEST==0 +int main(int argc, char **argv){ + int i; /* Loop counter */ + int nErr = 0; /* Number of errors encountered */ + Token *pList; /* List of input tokens for one file */ + InFile *pFileList = 0;/* List of all input files */ + InFile *pTail = 0; /* Last file on the list */ + InFile *pFile; /* for looping over the file list */ + int h_flag = 0; /* True if -h is present. Output unified header */ + int H_flag = 0; /* True if -H is present. Output EXPORT header */ + int v_flag = 0; /* Verbose */ + int noMoreFlags; /* True if -- has been seen. */ + FILE *report; /* Send progress reports to this, if not NULL */ + + noMoreFlags = 0; + for(i=1; i<argc; i++){ + if( argv[i][0]=='-' && !noMoreFlags ){ + switch( argv[i][1] ){ + case 'h': h_flag = 1; break; + case 'H': H_flag = 1; break; + case 'v': v_flag = 1; break; + case 'd': doc_flag = 1; proto_static = 1; break; + case 'l': proto_static = 1; break; + case 'f': AddParameters(i, &argc, &argv); break; + case '-': noMoreFlags = 1; break; +#ifdef DEBUG + case '!': i++; debugMask = strtol(argv[i],0,0); break; +#endif + default: Usage(argv[0],argv[i]); return 1; + } + }else{ + pFile = CreateInFile(argv[i],&nErr); + if( pFile ){ + if( pFileList ){ + pTail->pNext = pFile; + pTail = pFile; + }else{ + pFileList = pTail = pFile; + } + } + } + } + if( h_flag && H_flag ){ + h_flag = 0; + } + if( v_flag ){ + report = (h_flag || H_flag) ? stderr : stdout; + }else{ + report = 0; + } + if( nErr>0 ){ + return nErr; + } + for(pFile=pFileList; pFile; pFile=pFile->pNext){ + char *zFile; + + zFilename = pFile->zSrc; + if( zFilename==0 ) continue; + zFile = ReadFile(zFilename); + if( zFile==0 ){ + fprintf(stderr,"Can't read input file \"%s\"\n",zFilename); + nErr++; + continue; + } + if( strncmp(zFile,zTopLine,nTopLine)==0 ){ + pFile->zSrc = 0; + }else{ + if( report ) fprintf(report,"Reading %s...\n",zFilename); + pList = TokenizeFile(zFile,&pFile->idTable); + if( pList ){ + nErr += ParseFile(pList,pFile->flags); + FreeTokenList(pList); + }else if( zFile[0]==0 ){ + fprintf(stderr,"Input file \"%s\" is empty.\n", zFilename); + nErr++; + }else{ + fprintf(stderr,"Errors while processing \"%s\"\n", zFilename); + nErr++; + } + } + if( !doc_flag ) SafeFree(zFile); + if( doc_flag ) PrintModuleRecord(zFile,zFilename); + } + if( nErr>0 ){ + return nErr; + } +#ifdef DEBUG + if( debugMask & DECL_DUMP ){ + DumpDeclList(); + return nErr; + } +#endif + if( doc_flag ){ + DocumentationDump(); + return nErr; + } + zFilename = "--internal--"; + pList = TokenizeFile(zInit,0); + if( pList==0 ){ + return nErr+1; + } + ParseFile(pList,PS_Interface); + FreeTokenList(pList); + if( h_flag || H_flag ){ + nErr += MakeGlobalHeader(H_flag); + }else{ + for(pFile=pFileList; pFile; pFile=pFile->pNext){ + if( pFile->zSrc==0 ) continue; + nErr += MakeHeader(pFile,report,0); + } + } + return nErr; +} +#endif diff --git a/tools/makeheaders.html b/tools/makeheaders.html new file mode 100644 index 0000000..043c678 --- /dev/null +++ b/tools/makeheaders.html @@ -0,0 +1,1150 @@ +<html> +<head><title>The Makeheaders Program</title></head> +<body bgcolor=white> +<h1 align=center>The Makeheaders Program</h1> + + +<p> +This document describes <em>makeheaders</em>, +a tool that automatically generates “<code>.h</code>” +files for a C or C++ programming project. +</p> + + +<h2>Table Of Contents</h2> + +<ul> +<li><a href="#H0002">1,0 Background</a> +<ul> +<li><a href="#H0003">1.1 Problems With The Traditional Approach</a> + +<li><a href="#H0004">1.2 The Makeheaders Solution</a> +</ul> +<li><a href="#H0005">2.0 Running The Makeheaders Program</a> + +<li><a href="#H0006">3.0 Preparing Source Files For Use With Makeheaders</a> +<ul> +<li><a href="#H0007">3.1 The Basic Setup</a> + +<li><a href="#H0008">3.2 What Declarations Get Copied</a> + +<li><a href="#H0009">3.3 How To Avoid Having To Write Any Header Files</a> + +<li><a href="#H0010">3.4 Designating Declarations For Export</a> + +<li><a href="#H0011">3.5 Local declarations processed by makeheaders</a> + +<li><a href="#H0012">3.6 Using Makeheaders With C++ Code</a> + +<li><a href="#H0013">3.7 Conditional Compilation</a> + +<li><a href="#H0014">3.8 Caveats</a> +</ul> +<li><a href="#H0015">4.0 Using Makeheaders To Generate Documentation</a> + +<li><a href="#H0016">5.0 Compiling The Makeheaders Program</a> + +<li><a href="#H0017">6.0 History</a> + +<li><a href="#H0018">7.0 Summary And Conclusion</a> +</ul><a name="H0002"></a> +<h2>1.0 Background</h2> + +<p> +A piece of C source code can be one of two things: +a <em>declaration</em> or a <em>definition</em>. +A declaration is source text that gives information to the +compiler but doesn't directly result in any code being generated. +A definition is source text that results in executable machine +instructions or initialization data. +(These two terms are sometimes used inconsistently by other authors. +In particular, many people reverse the meanings of these words when +discussing Pascal or Ada code. +The meanings described here are the same as used in the ANSI-C +standards document.) +</p> + +<p> +Declarations in C include things such as the following: +<ul> +<li> Typedefs. +<li> Structure, union and enumeration declarations. +<li> Function and procedure prototypes. +<li> Preprocessor macros and #defines. +<li> “<code>extern</code>” variable declarations. +</ul> +</p> + +<p> +Definitions in C, on the other hand, include these kinds of things: +<ul> +<li> Variable definitions. +<li> The bodies of functions and procedures. +<li> Initialization data. +</ul> +</p> + +<p> +The distinction between a declaration and a definition is common in +modern software engineering. +Another way of looking at the difference is that the declaration +is the <em>interface</em> and the definition is the <em>implementation</em>. +</p> + +<p> +In C programs, it has always been the tradition that declarations are +put in files with the “<code>.h</code>” suffix and definitions are +placed in “<code>.c</code>” files. +The .c files contain “<code>#include</code>” preprocessor statements +that cause the contents of .h files to be included as part of the +source code when the .c file is compiled. +In this way, the .h files define the interface to a subsystem and +the .c files define how the subsystem is implemented. +</p> + +<a name="H0003"></a> +<h3>1.1 Problems With The Traditional Approach</h3> + +<p> +As the art of computer programming continues to advance, and the size +and complexity of programs continues to swell, the traditional C +approach of placing declarations and definitions in separate files begins +to present the programmer with logistics and +maintenance problems. +To wit: +</p> + +<p> +<ol> +<p><li> +In large codes with many source files, it becomes difficult to determine +which .h files should be included in which .c files. +<p><li> +It is typically the case that a .h file will be forced to include +another .h files, which in turn might include other .h files, +and so forth. +The .c file must be recompiled when any of the .h files in this chain +are altered, but it can be difficult to determine what .h files are found +in the include chain. +A frequent Makefile error is to omit some .h files from a dependency +list even though those files are on the include file chain. +<p><li> +Some information is common to both the declaration and the definition of +an object in C, and so must be repeated in both the .h and the .c files +for that object. +In a large project, it can become increasingly difficult to keep the two +files in sync. +<p><li> +When a .c file includes a .h file and the .h files changes, the .c file +must be recompiled, even if the part of the .h file that changed is not +actually used by the .c file. +In a large program, it is generally the case that almost every .c file ends up +depending on one or two of the more important .h files, and so when those .h +files change, the entire program must be recompiled. +It also happens that those important .h files tend to be the ones that +change most frequently. +This means that the entire program must be recompiled frequently, +leading to a lengthy modify-compile-test cycle and a corresponding +decrease in programmer productivity. +<p><li> +The C programming language requires that declarations depending upon +each other must occur in a particular order. +In a program with complex, interwoven data structures, the correct +declaration order can become very difficult to determine manually, +especially when the declarations involved are spread out over several +files. +</ol> +</p> + +<a name="H0004"></a> +<h3>1.2 The Makeheaders Solution</h3> + +<p> +The makeheaders program is designed to ameliorate the problems associated +with the traditional C programming model by automatically generating +the interface information in the .h files from +interface information contained in other .h files and +from implementation information in the .c files. +When the makeheaders program is run, it scans the source +files for a project, +then generates a series of new .h files, one for each .c file. +The generated .h files contain exactly those declarations required by the +corresponding .c files, no more and no less. +</p> + +<p> +The makeheaders programming model overcomes all of the objections to the +traditional C programming model. +<ol> +<p><li> +Because all declarations needed by a .c file are contained in a +single .h file, there is never any question about what .h files +a .c will need to include. If the .c file is named +<code>alpha.c</code> then it must include only the single .h file +named <code>alpha.h</code>. +(The .c file might also use some include files from the standard +library, such as <code><stdio.h></code>, but that is another matter.) +<p><li> +The generated .h files do not include other .h files, and so there +are no include chains to worry about. +The file <code>alpha.c</code> depends on <code>alpha.h</code> and +nothing more. +<p><li> +There is still duplication in the .h and the .c file, but because +the duplicate information is automatically generated, it is no longer +a problem. +Simply rerun makeheaders to resynchronize everything. +<p><li> +The generated .h file contains the minimal set of declarations needed +by the .c file. +This means that when something changes, a minimal amount of recompilation +is required to produce an updated executable. +Experience has shown that this gives a dramatic improvement +in programmer productivity by facilitating a rapid modify-compile-test +cycle during development. +<p><li> +The makeheaders program automatically sorts declarations into the +correct order, completely eliminating the wearisome and error-prone +task of sorting declarations by hand. +</ol> +<p> + +<p> +In addition, the makeheaders program is fast and unintrusive. +It is a simple matter to incorporate makeheaders into a Makefile +so that makeheaders will be run automatically whenever the project +is rebuilt. +And the burden of running makeheaders is light. +It will easily process tens of thousands of lines of source +code per second. +</p> + +<a name="H0005"></a> +<h2>2.0 Running The Makeheaders Program</h2> + +<p> +The makeheaders program is very easy to run. +If you have a collection of C source code and include files in the working +directory, then you can run makeheaders to generate appropriate .h +files using the following command: +<pre> + makeheaders *.[ch] +</pre> +That's really all there is to it! +This command will generate one .h file for every .c file. +Any .h files that were generated by a prior run of makeheaders +are ignored, +but manually entered .h files +that contain structure declarations and so forth will be scanned and +the declarations will be copied into the generated .h files as +appropriate. +But if makeheaders sees that the .h file that it has generated is no +different from the .h file it generated last time, it doesn't update +the file. +This prevents the corresponding .c files from having to +be needlessly recompiled. +</p> + +<p> +There are several options to the makeheaders program that can +be used to alter its behavior. +The default behavior is to write a single .h file for each .c file and +to give the .h file the same base name as the .c file. +Instead of generating a whole mess of .h files, you can, if you choose, +generate a single big .h file that contains all declarations needed +by all the .c files. Do this using the -h option to makeheaders. +As follows: +<pre> + makeheaders -h *.[ch] >common.h +</pre> +With the -h option, the .h file is not actually written to a disk file but +instead appears on standard output, where you are free to redirect it +into the file of your choice. +</p> + +<p> +A similar option is -H. Like the lower-case -h option, big -H +generates a single include file on standard output. But unlike +small -h, the big -H only emits prototypes and declarations that +have been designated as “exportable”. +The idea is that -H will generate an include file that defines +the interface to a library. +More will be said about this in section 3.4. +</p> + +<p> +Sometimes you want the base name of the .c file and the .h file to +be different. +For example, suppose you want the include file for <code>alpha.c</code> +to be called <code>beta.h</code>. +In this case, you would invoke makeheaders as follows: +<pre> + makeheaders alpha.c:beta.h +</pre> +Any time a filename argument contains a colon, the name before the +colon is taken to be the name of the .c file and the name after the +colon is taken to be the name of the .h file. +You can't use the shell's wildcard mechanism with this approach, but that +normally isn't a problem in Makefiles, which is where this stuff +comes in handy. +</p> + +<p> +If you want a particular file to be scanned by makeheaders but you +don't want makeheaders to generate a header file for that file, +then you can supply an empty header filename, like this: +<pre> + makeheaders alpha.c beta.c gamma.c: +</pre> +In this example, makeheaders will scan the three files named +“<code>alpha.c</code>”, +“<code>beta.c</code>” and +“<code>gamma.c</code>” +but because of the colon on the end of third filename +it will only generate headers for the first two files. +Unfortunately, +it is not possible to get makeheaders to process any file whose +name contains a colon. +</p> + +<p> +In a large project, the length of the command line for makeheaders +can become very long. +If the operating system doesn't support long command lines +(example: DOS and Win32) you may not be able to list all of the +input files in the space available. +In that case, you can use the “<code>-f</code>” option followed +by the name of a file to cause makeheaders to read command line +options and filename from the file instead of from the command line. +For example, you might prepare a file named “<code>mkhdr.dat</code>” +that contains text like this: +<pre> + src/alpha.c:hdr/alpha.h + src/beta.c:hdr/beta.h + src/gamma.c:hdr/gamma.h + ... +</pre> +Then invoke makeheaders as follows: +<pre> + makeheaders -f mkhdr.dat +</pre> +</p> + +<p> +The “<code>-local</code>” option causes makeheaders to +generate of prototypes for “<code>static</code>” functions and +procedures. +Such prototypes are normally omitted. +</p> + +<p> +Finally, makeheaders also includes a “<code>-doc</code>” option. +This command line option prevents makeheaders from generating any +headers at all. +Instead, makeheaders will write to standard output +information about every definition and declaration that it encounters +in its scan of source files. +The information output includes the type of the definition or +declaration and any comment that precedes the definition or +declaration. +The output is in a format that can be easily parsed, and is +intended to be read by another program that will generate +documentation about the program. +We'll talk more about this feature later. +</p> + +<p> +If you forget what command line options are available, or forget +their exact name, you can invoke makeheaders using an unknown +command line option (like “<code>--help</code>” or +“<code>-?</code>”) +and it will print a summary of the available options on standard +error. +If you need to process a file whose name begins with +“<code>-</code>”, +you can prepend a “<code>./</code>” to its name in order to get it +accepted by the command line parser. +Or, you can insert the special option “<code>--</code>” on the +command line to cause all subsequent command line arguments to be treated as +filenames even if their names begin with “<code>-</code>”. +</p> + +<a name="H0006"></a> +<h2>3.0 Preparing Source Files For Use With Makeheaders</h2> + +<p> +Very little has to be done to prepare source files for use with +makeheaders since makeheaders will read and understand ordinary +C code. +But it is important that you structure your files in a way that +makes sense in the makeheaders context. +This section will describe several typical uses of makeheaders. +</p> + +<a name="H0007"></a> +<h3>3.1 The Basic Setup</h3> + +<p> +The simplest way to use makeheaders is to put all definitions in +one or more .c files and all structure and type declarations in +separate .h files. +The only restriction is that you should take care to chose basenames +for your .h files that are different from the basenames for your +.c files. +Recall that if your .c file is named (for example) +“<code>alpha.c</code>” +makeheaders will attempt to generate a corresponding header file +named “<code>alpha.h</code>”. +For that reason, you don't want to use that name for +any of the .h files you write since that will prevent makeheaders +from generating the .h file automatically. +</p> + +<p> +The structure of a .c file intended for use with makeheaders is very +simple. +All you have to do is add a single “<code>#include</code>” to the +top of the file that sources the header file that makeheaders will generate. +Hence, the beginning of a source file named “<code>alpha.c</code>” +might look something like this: +</p> + +<pre> + /* + * Introductory comment... + */ + #include "alpha.h" + + /* The rest of your code... */ +</pre> + +<p> +Your manually generated header files require no special attention at all. +Code them as you normally would. +However, makeheaders will work better if you omit the +“<code>#if</code>” statements people often put around the outside of +header files that prevent the files from being included more than once. +For example, to create a header file named “<code>beta.h</code>”, +many people will habitually write the following: + +<pre> + #ifndef BETA_H + #define BETA_H + + /* declarations for beta.h go here */ + + #endif +</pre> + +You can forego this cleverness with makeheaders. +Remember that the header files you write will never really be +included by any C code. +Instead, makeheaders will scan your header files to extract only +those declarations that are needed by individual .c files and then +copy those declarations to the .h files corresponding to the .c files. +Hence, the “<code>#if</code>” wrapper serves no useful purpose. +But it does make makeheaders work harder, forcing it to put +the statements + +<pre> + #if !defined(BETA_H) + #endif +</pre> + +around every declaration that it copies out of your header file. +No ill effect should come of this, but neither is there any benefit. +</p> + +<p> +Having prepared your .c and .h files as described above, you can +cause makeheaders to generate its .h files using the following simple +command: + +<pre> + makeheaders *.[ch] +</pre> + +The makeheaders program will scan all of the .c files and all of the +manually written .h files and then automatically generate .h files +corresponding to all .c files. +</p> + +<p> +Note that +the wildcard expression used in the above example, +“<code>*.[ch]</code>”, +will expand to include all .h files in the current directory, both +those entered manually be the programmer and others generated automatically +by a prior run of makeheaders. +But that is not a problem. +The makeheaders program will recognize and ignore any files it +has previously generated that show up on its input list. +</p> + +<a name="H0008"></a> +<h3>3.2 What Declarations Get Copied</h3> + +<p> +The following list details all of the code constructs that makeheaders +will extract and place in +the automatically generated .h files: +</p> + +<ul> +<p><li> +When a function is defined in any .c file, a prototype of that function +is placed in the generated .h file of every .c file that +calls the function.</p> + +<P>If the “<code>static</code>” keyword of C appears at the +beginning of the function definition, the prototype is suppressed. +If you use the “<code>LOCAL</code>” keyword where you would normally +say “<code>static</code>”, then a prototype is generated, but it +will only appear in the single header file that corresponds to the +source file containing the function. For example, if the file +<code>alpha.c</code> contains the following: +<pre> + LOCAL int testFunc(void){ + return 0; + } +</pre> +Then the header file <code>alpha.h</code> will contain +<pre> + #define LOCAL static + LOCAL int testFunc(void); +</pre> +However, no other generated header files will contain a prototype for +<code>testFunc()</code> since the function has only file scope.</p> + +<p>When the “<code>LOCAL</code>” keyword is used, makeheaders will +also generate a #define for LOCAL, like this: +<pre> + #define LOCAL static +</pre> +so that the C compiler will know what it means.</p> + +<p>If you invoke makeheaders with a “<code>-local</code>” +command-line option, then it treats the “<code>static</code>” +keyword like “<code>LOCAL</code>” and generates prototypes in the +header file that corresponds to the source file containing the function +definition.</p> + +<p><li> +When a global variable is defined in a .c file, an +“<code>extern</code>” +declaration of that variable is placed in the header of every +.c file that uses the variable. +</p> + +<p><li> +When a structure, union or enumeration declaration or a +function prototype or a C++ class declaration appears in a +manually produced .h file, that declaration is copied into the +automatically generated +.h files of all .c files that use the structure, union, enumeration, +function or class. +But declarations that appear in a +.c file are considered private to that .c file and are not copied into +any automatically generated files. +</p> + +<p><li> +All #defines and typedefs that appear in manually produced .h files +are copied into automatically generated .h files as needed. +Similar constructs that appear in .c files are considered private to +those files and are not copied. +</p> + +<p><li> +When a structure, union or enumeration declaration appears in a .h +file, makeheaders will automatically +generate a typedef that allows the declaration to be referenced without +the “<code>struct</code>”, “<code>union</code>” or +“<code>enum</code>” qualifier. +In other words, if makeheaders sees the code: +<pre> + struct Examp { /* ... */ }; +</pre> +it will automatically generate a corresponding typedef like this: +<pre> + typedef struct Examp Examp; +</pre> +</p> + +<p><li> +Makeheaders generates an error message if it encounters a function or +variable definition within a .h file. +The .h files are supposed to contain only interface, not implementation. +C compilers will not enforce this convention, but makeheaders does. +</ul> + +<p> +As a final note, we observe that automatically generated declarations +are ordered as required by the ANSI-C programming language. +If the declaration of some structure “<code>X</code>” requires a +prior declaration of another structure “<code>Y</code>”, then Y will +appear first in the generated headers. +</p> + +<a name="H0009"></a> +<h3>3.3 How To Avoid Having To Write Any Header Files</h3> + +<p> +In my experience, large projects work better if all of the manually +written code is placed in .c files and all .h files are generated +automatically. +This is slightly different from the traditional C method of placing +the interface in .h files and the implementation in .c files, but +it is a refreshing change that brings a noticeable improvement to the +coding experience. +Others, I believe, share this view since I've +noticed recent languages (ex: java, tcl, perl, awk) tend to +support the one-file approach to coding as the only option. +</p> + +<p> +The makeheaders program supports putting both +interface and implementation into the same source file. +But you do have to tell makeheaders which part of the source file is the +interface and which part is the implementation. +Makeheaders has to know this in order to be able to figure out whether or +not structures declarations, typedefs, #defines and so forth should +be copied into the generated headers of other source files. +</p> + +<p> +You can instruct makeheaders to treat any part of a .c file as if +it were a .h file by enclosing that part of the .c file within: +<pre> + #if INTERFACE + #endif +</pre> +Thus any structure definitions that appear after the +“<code>#if INTERFACE</code>” but before the corresponding +“<code>#endif</code>” are eligible to be copied into the +automatically generated +.h files of other .c files. +</p> + +<p> +If you use the “<code>#if INTERFACE</code>” mechanism in a .c file, +then the generated header for that .c file will contain a line +like this: +<pre> + #define INTERFACE 0 +</pre> +In other words, the C compiler will never see any of the text that +defines the interface. +But makeheaders will copy all necessary definitions and declarations +into the .h file it generates, so .c files will compile as if the +declarations were really there. +This approach has the advantage that you don't have to worry with +putting the declarations in the correct ANSI-C order -- makeheaders +will do that for you automatically. +</p> + +<p> +Note that you don't have to use this approach exclusively. +You can put some declarations in .h files and others within the +“<code>#if INTERFACE</code>” regions of .c files. +Makeheaders treats all declarations alike, no matter where they +come from. +You should also note that a single .c file can contain as many +“<code>#if INTERFACE</code>” regions as desired. +</p> + +<a name="H0010"></a> +<h3>3.4 Designating Declarations For Export</h3> + +<p> +In a large project, one will often construct a hierarchy of +interfaces. +For example, you may have a group of 20 or so files that form +a library used in several other parts of the system. +Each file in this library will present two interfaces. +One interface will be the routines and data structures it is +willing to share with other files in the same library, and the +second interface is those routines and data structures it wishes +to make available to other subsystems. +(The second interface is normally a subset of the first.) +Ordinary C does not provide support for a tiered interface +like this, but makeheaders does. +</p> + +<p> +Using makeheaders, it is possible to designate routines and data +structures as being for “<code>export</code>”. +Exported objects are visible not only to other files within the +same library or subassembly but also to other +libraries and subassemblies in the larger program. +By default, makeheaders only makes objects visible to other members +of the same library. +</p> + +<p> +That isn't the complete truth, actually. +The semantics of C are such that once an object becomes visible +outside of a single source file, it is also visible to any user +of the library that is made from the source file. +Makeheaders can not prevent outsiders from using non-exported resources, +but it can discourage the practice by refusing to provide prototypes +and declarations for the services it does not want to export. +Thus the only real effect of the making an object exportable is +to include it in the output makeheaders generates when it is run +using the -H command line option. +This is not a perfect solution, but it works well in practice. +</p> + +<p> +But trouble quickly arises when we attempt to devise a mechanism for +telling makeheaders which prototypes it should export and which it should +keep local. +The built-in “<code>static</code>” keyword of C works well for +prohibiting prototypes from leaving a single source file, but because C doesn't +support a linkage hierarchy, there is nothing in the C language to help us. +We'll have to invite our own keyword: “<code>EXPORT</code>” +</p> + +<p> +Makeheaders allows the EXPORT keyword to precede any function or +procedure definition. +The routine following the EXPORT keyword is then eligable to appear +in the header file generated using the -H command line option. +Note that if a .c file contains the EXPORT keyword, makeheaders will +put the macro +<pre> + #define EXPORT +</pre> +in the header file it generates for the .c file so that the EXPORT keyword +will never be seen by the C compiler. +</p> + +<p> +But the EXPORT keyword only works for function and procedure definitions. +For structure, union and enum definitions, typedefs, #defines and +class declarations, a second mechanism is used. +Just as any declarations or definition contained within +<pre> + #if INTERFACE + #endif +</pre> +are visible to all files within the library, any declarations +or definitions within +<pre> + #if EXPORT_INTERFACE + #endif +</pre> +will become part of the exported interface. +The “<code>#if EXPORT_INTERFACE</code>” mechanism can be used in +either .c or .h files. +(The “<code>#if INTERFACE</code>” can also be used in both .h and +.c files, but since it's use in a .h file would be redundant, we haven't +mentioned it before.) +</p> + +<a name="H0011"></a> +<h3>3.5 Local declarations processed by makeheaders</h3> + +<p> +Structure declarations and typedefs that appear in .c files are normally +ignored by makeheaders. +Such declarations are only intended for use by the source file in which +they appear and so makeheaders doesn't need to copy them into any +generated header files. +We call such declarations “<code>private</code>”. +</p> + +<p> +Sometimes it is convenient to have makeheaders sort a sequence +of private declarations into the correct order for us automatically. +Or, we could have static functions and procedures for which we would like +makeheaders to generate prototypes, but the arguments to these +functions and procedures uses private declarations. +In both of these cases, we want makeheaders to be aware of the +private declarations and copy them into the local header file, +but we don't want makeheaders to propagate the +declarations outside of the file in which they are declared. +</p> + +<p> +When this situation arises, enclose the private declarations +within +<pre> + #if LOCAL_INTERFACE + #endif +</pre> +A “<code>LOCAL_INTERFACE</code>” block works very much like the +“<code>INTERFACE</code>” and +“<code>EXPORT_INTERFACE</code>” +blocks described above, except that makeheaders insures that the +objects declared in a LOCAL_INTERFACE are only visible to the +file containing the LOCAL_INTERFACE. +</p> + +<a name="H0012"></a> +<h3>3.6 Using Makeheaders With C++ Code</h3> + +<p> +You can use makeheaders to generate header files for C++ code, in +addition to C. +Makeheaders will recognize and copy both “<code>class</code>” +declarations +and inline function definitions, and it knows not to try to generate +prototypes for methods. +</p> + +<p> +In fact, makeheaders is smart enough to be used in projects that employ +a mixture of C and C++. +For example, if a C function is called from within a C++ code module, +makeheaders will know to prepend the text +<pre> + extern "C" +</pre> +to the prototype for that function in the C++ header file. +Going the other way, +if you try to call a C++ function from within C, an +appropriate error message is issued, since C++ routines can not +normally be called by C code (due to fact that most C++ compilers +use name mangling to facilitate type-safe linkage.) +</p> + +<p> +No special command-line options are required to use makeheaders with +C++ input. Makeheaders will recognize that its source code is C++ +by the suffix on the source code filename. Simple ".c" or ".h" suffixes +are assumed to be ANSI-C. Anything else, including ".cc", ".C" and +".cpp" is assumed to be C++. +The name of the header file generated by makeheaders is derived from +the name of the source file by converting every "c" to "h" and +every "C" to "H" in the suffix of the filename. +Thus the C++ source +file “<code>alpha.cpp</code>” will induce makeheaders to +generate a header file named “<code>alpha.hpp</code>”. +</p> + +<p> +Makeheaders augments class definitions by inserting prototypes to +methods where appropriate. If a method definition begins with one +of the special keywords <b>PUBLIC</b>, <b>PROTECTED</b>, or +<b>PRIVATE</b> (in upper-case to distinguish them from the regular +C++ keywords with the same meaning) then a prototype for that +method will be inserted into the class definition. If none of +these keywords appear, then the prototype is not inserted. For +example, in the following code, the constructor is not explicitly +declared in the class definition but makeheaders will add it there +because of the PUBLIC keyword that appears before the constructor +definition. +</p> + +<blockquote><pre> +#if INTERFACE +class Example1 { +private: + int v1; +}; +#endif +PUBLIC Example1::Example1(){ + v1 = 0; +} +</pre></blockquote> + +<p> +The code above is equivalent to the following: +</p> + +<blockquote><pre> +#if INTERFACE +class Example1 { +private: + int v1; +public: + Example1(); +}; +#endif +Example1::Example1(){ + v1 = 0; +} +</pre></blockquote> + +<p> +The first form is preferred because only a single declaration of +the constructor is required. The second form requires two declarations, +one in the class definition and one on the definition of the constructor. +</p> + +<h4>3.6.1 C++ Limitations</h4> + +<p> +Makeheaders does not understand more recent +C++ syntax such as templates and namespaces. +Perhaps these issues will be addressed in future revisions. +</p> + +<a name="H0013"></a> +<h3>3.7 Conditional Compilation</h3> + +<p> +The makeheaders program understands and tracks the conditional +compilation constructs in the source code files it scans. +Hence, if the following code appears in a source file +<pre> + #ifdef UNIX + # define WORKS_WELL 1 + #else + # define WORKS_WELL 0 + #endif +</pre> +then the next patch of code will appear in the generated header for +every .c file that uses the WORKS_WELL constant: +<pre> + #if defined(UNIX) + # define WORKS_WELL 1 + #endif + #if !defined(UNIX) + # define WORKS_WELL 0 + #endif +</pre> +The conditional compilation constructs can be nested to any depth. +Makeheaders also recognizes the special case of +<pre> + #if 0 + #endif +</pre> +and treats the enclosed text as a comment. +</p> + +<a name="H0014"></a> +<h3>3.8 Caveats</h3> + +<p> +The makeheaders system is designed to be robust +but it is possible for a devious programmer to fool the system, +usually with unhelpful consequences. +This subsection is a guide to helping you avoid trouble. +</p> + +<p> +Makeheaders does not understand the old K&R style of function +and procedure definitions. +It only understands the modern ANSI-C style, and will probably +become very confused if it encounters an old K&R function. +Therefore you should take care to avoid putting K&R function definitions +in your code. +</p> + +<p> +Makeheaders does not understand when you define more than one +global variable with the same type separated by a comma. +In other words, makeheaders does not understand this: +<pre> + int a = 4, b = 5; +</pre> +The makeheaders program wants every variable to have its own +definition. Like this: +<pre> + int a = 4; + int b = 5; +</pre> +Notice that this applies to global variables only, not to variables +you declare inside your functions. +Since global variables ought to be exceedingly rare, and since it is +good style to declare them separately anyhow, this restriction is +not seen as a terrible hardship. +</p> + +<p> +Makeheaders does not support defining an enumerated or aggregate type in +the same statement as a variable declaration. None of the following +statements work completely: +<pre> +struct {int field;} a; +struct Tag {int field;} b; +struct Tag c; +</pre> +Instead, define types separately from variables: +<pre> +#if INTERFACE +struct Tag {int field;}; +#endif +Tag a; +Tag b; /* No more than one variable per declaration. */ +Tag c; /* So must put each on its own line. */ +</pre> +See <a href="#H0008">3.2 What Declarations Get Copied</a> for details, +including on the automatic typedef. +</p> + +<p> +The makeheaders program processes its source file prior to sending +those files through the C preprocessor. +Hence, if you hide important structure information in preprocessor defines, +makeheaders might not be able to successfully extract the information +it needs from variables, functions and procedure definitions. +For example, if you write this: +<pre> + #define BEGIN { + #define END } +</pre> +at the beginning of your source file, and then try to create a function +definition like this: +<pre> + char *StrDup(const char *zSrc) + BEGIN + /* Code here */ + END +</pre> +then makeheaders won't be able to find the end of the function definition +and bad things are likely to happen. +</p> + +<p> +For most projects the code constructs that makeheaders cannot +handle are very rare. +As long as you avoid excessive cleverness, makeheaders will +probably be able to figure out what you want and will do the right +thing. +</p> + +<p> +Makeheaders has limited understanding of enums. In particular, it does +not realize the significance of enumerated values, so the enum is not +emitted in the header files when its enumerated values are used unless +the name associated with the enum is also used. Moreover, enums can be +completely anonymous, e.g. “<code>enum {X, Y, Z};</code>”. +Makeheaders ignores such enums so they can at least be used within a +single source file. Makeheaders expects you to use #define constants +instead. If you want enum features that #define lacks, and you need the +enum in the interface, bypass makeheaders and write a header file by +hand, or teach makeheaders to emit the enum definition when any of the +enumerated values are used, rather than only when the top-level name (if +any) is used. +</p> + +<a name="H0015"></a> +<h2>4.0 Using Makeheaders To Generate Documentation</h2> + +<p> +Many people have observed the advantages of generating program +documentation directly from the source code: +<ul> +<li> Less effort is involved. It is easier to write a program than + it is to write a program and a document. +<li> The documentation is more likely to agree with the code. + When documentation is derived directly from the code, or is + contained in comments immediately adjacent to the code, it is much + more likely to be correct than if it is contained in a separate + unrelated file in a different part of the source tree. +<li> Information is kept in only one place. When a change occurs + in the code, it is not necessary to make a corresponding change + in a separate document. Just rerun the documentation generator. +</ul> +The makeheaders program does not generate program documentation itself. +But you can use makeheaders to parse the program source code, extract +the information that is relevant to the documentation and to pass this +information to another tool to do the actual documentation preparation. +</p> + +<p> +When makeheaders is run with the “<code>-doc</code>” option, it +emits no header files at all. +Instead, it does a complete dump of its internal tables to standard +output in a form that is easily parsed. +This output can then be used by another program (the implementation +of which is left as an exercise to the reader) that will use the +information to prepare suitable documentation. +</p> + +<p> +The “<code>-doc</code>” option causes makeheaders to print +information to standard output about all of the following objects: +<ul> +<li> C++ class declarations +<li> Structure and union declarations +<li> Enumerations +<li> Typedefs +<li> Procedure and function definitions +<li> Global variables +<li> Preprocessor macros (ex: “<code>#define</code>”) +</ul> +For each of these objects, the following information is output: +<ul> +<li> The name of the object. +<li> The type of the object. (Structure, typedef, macro, etc.) +<li> Flags to indicate if the declaration is exported (contained within + an EXPORT_INTERFACE block) or local (contained with LOCAL_INTERFACE). +<li> A flag to indicate if the object is declared in a C++ file. +<li> The name of the file in which the object was declared. +<li> The complete text of any block comment that precedes the declarations. +<li> If the declaration occurred inside a preprocessor conditional + (“<code>#if</code>”) then the text of that conditional is + provided. +<li> The complete text of a declaration for the object. +</ul> +The exact output format will not be described here. +It is simple to understand and parse and should be obvious to +anyone who inspects some sample output. +</p> + +<a name="H0016"></a> +<h2>5.0 Compiling The Makeheaders Program</h2> + +<p> +The source code for makeheaders is a single file of ANSI-C code, +approximately 3000 lines in length. +The program makes only modest demands of the system and C library +and should compile without alteration on most ANSI C compilers +and on most operating systems. +It is known to compile using several variations of GCC for Unix +as well as Cygwin32 and MSVC 5.0 for Win32. +</p> + +<a name="H0017"></a> +<h2>6.0 History</h2> + +<p> +The makeheaders program was first written by D. Richard Hipp +(also the original author of +<a href="https://sqlite.org/">SQLite</a> and +<a href="https://fossil-scm.org/">Fossil</a>) in 1993. +Hipp open-sourced the project immediately, but it never caught +on with any other developers and it continued to be used mostly +by Hipp himself for over a decade. When Hipp was first writing +the Fossil version control system in 2006 and 2007, he used +makeheaders on that project to help simplify the source code. +As the popularity of Fossil increased, the makeheaders +that was incorporated into the Fossil source tree became the +"official" makeheaders implementation. +</p> + +<p> +As this paragraph is being composed (2016-11-05), Fossil is the +only project known to Hipp that is still using makeheaders. On +the other hand, makeheaders has served the Fossil project well and +there are no plans remove it. +</p> + +<a name="H0018"></a> +<h2>7.0 Summary And Conclusion</h2> + +<p> +The makeheaders program will automatically generate a minimal header file +for each of a set of C source and header files, and will +generate a composite header file for the entire source file suite, +for either internal or external use. +It can also be used as the parser in an automated program +documentation system. +</p> + +<p> +The makeheaders program has been in use since 1994, +in a wide variety of projects under both UNIX and Win32. +In every project where it has been used, makeheaders has proven +to be a very helpful aid +in the construction and maintenance of large C codes. +In at least two cases, makeheaders has facilitated development +of programs that would have otherwise been all but impossible +due to their size and complexity. +</p> +</body> +</html> diff --git a/tools/makemake.tcl b/tools/makemake.tcl new file mode 100755 index 0000000..d6f75af --- /dev/null +++ b/tools/makemake.tcl @@ -0,0 +1,2159 @@ +#!/usr/bin/tclsh +# +# ### Run this Tcl script EVERY time you modify it in any way! ### +# ### It must be run from the directory it lives in so that ### +# ### directories resolve properly! ### +# +# This Tcl script generates make files for various platforms. The makefiles +# then need to be committed. +# +# If you modify this file then: +# +# 1. cd tools; tclsh makemake.tcl +# +# 2. if errors are reported, fix them and go to step 1 +# +# 3. if "fossil diff" reports changes in any of the generated +# files, commit the changed files to the repo +# +# Files generated include: +# +# src/main.mk # makefile for all unix systems +# win/Makefile.mingw # makefile for mingw on windows +# win/Makefile.* # makefiles for other windows compilers +# +# Add new source files by listing the files (without their .c suffix) +# in the "src" variable. Add new resource files to the "extra_files" +# variable. There are other variables that you can alter, down to +# the "STOP HERE" comment. The stuff below "STOP HERE" should rarely need +# to change. After modification, go to step 1 above. +# +# Delete unused source files in the "src" variable, then go to step 1 above. +# +############################################################################# + +# $srcDir is used to set the target source dir in several places. Not +# all code-generation bits use $srcDir and instead hard-code, so +# replacing it only here (should it ever changes) is not sufficient. +# +set srcDir ../src +# Directory $srcDirExt houses single-file source code solutions which +# are imported directly into the fossil source tree. +set srcDirExt ../extsrc + +# Basenames of all source files that get preprocessed using +# "translate" and "makeheaders". To add new C-language source files to the +# project, simply add the basename to this list and rerun this script. +# +# Set the separate extra_files variable further down for how to add non-C +# files, such as string and BLOB resources. +# + +set src { + add + ajax + alerts + allrepo + attach + backlink + backoffice + bag + bisect + blob + branch + browse + builtin + bundle + cache + capabilities + captcha + cgi + chat + checkin + checkout + clearsign + clone + color + comformat + configure + content + cookies + db + delta + deltacmd + deltafunc + descendants + diff + diffcmd + dispatch + doc + encode + etag + event + extcgi + export + file + fileedit + finfo + foci + forum + fshell + fusefs + fuzz + glob + graph + gzip + hname + hook + http + http_socket + http_transport + import + info + interwiki + json + json_artifact + json_branch + json_config + json_diff + json_dir + json_finfo + json_login + json_query + json_report + json_status + json_tag + json_timeline + json_user + json_wiki + leaf + loadctrl + login + lookslike + main + manifest + markdown + markdown_html + match + md5 + merge + merge3 + moderate + name + org + patch + path + piechart + pikchrshow + pivot + popen + pqueue + printf + publish + purge + rebuild + regexp + repolist + report + robot + rss + schema + search + security_audit + setup + setupuser + sha1 + sha1hard + sha3 + shun + sitemap + skins + smtp + sqlcmd + stash + stat + statrep + style + sync + tag + tar + terminal + th_main + timeline + tkt + tktsetup + undo + unicode + unversioned + update + url + user + utf8 + util + verify + vfile + wiki + wikiformat + winfile + winhttp + xfer + xfersetup + xsystem + zip + http_ssl +} + +# Source files which live under $srcDirExt, but only those for which +# we need to run makeheaders. External sources which have their own +# header files must not be in this list. +set src_ext { + pikchr +} + +# Additional resource files that get built into the executable. +# These paths are all resolved from the src/ directory, so must +# be relative to that. +set extra_files { + diff.tcl + merge.tcl + markdown.md + wiki.wiki + *.js + default.css + style.*.css + ../skins/*/*.txt + sounds/*.wav + alerts/*.wav + ../extsrc/pikchr.wasm + ../extsrc/pikchr*.js +} + +# Options used to compile the included SQLite library. +# +set SQLITE_OPTIONS { + -DNDEBUG=1 + -DSQLITE_DQS=0 + -DSQLITE_THREADSAFE=0 + -DSQLITE_DEFAULT_MEMSTATUS=0 + -DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1 + -DSQLITE_LIKE_DOESNT_MATCH_BLOBS + -DSQLITE_OMIT_DECLTYPE + -DSQLITE_OMIT_DEPRECATED + -DSQLITE_OMIT_PROGRESS_CALLBACK + -DSQLITE_OMIT_SHARED_CACHE + -DSQLITE_OMIT_LOAD_EXTENSION + -DSQLITE_MAX_EXPR_DEPTH=0 + -DSQLITE_ENABLE_LOCKING_STYLE=0 + -DSQLITE_DEFAULT_FILE_FORMAT=4 + -DSQLITE_ENABLE_DBSTAT_VTAB + -DSQLITE_ENABLE_EXPLAIN_COMMENTS + -DSQLITE_ENABLE_FTS4 + -DSQLITE_ENABLE_FTS5 + -DSQLITE_ENABLE_MATH_FUNCTIONS + -DSQLITE_ENABLE_PERCENTILE + -DSQLITE_ENABLE_SETLK_TIMEOUT + -DSQLITE_ENABLE_STMTVTAB + -DSQLITE_HAVE_ZLIB + -DSQLITE_ENABLE_DBPAGE_VTAB + -DSQLITE_TRUSTED_SCHEMA=0 + -DHAVE_USLEEP +} +#lappend SQLITE_OPTIONS -DSQLITE_ENABLE_FTS3=1 +#lappend SQLITE_OPTIONS -DSQLITE_ENABLE_STAT4 +#lappend SQLITE_OPTIONS -DSQLITE_WIN32_NO_ANSI +#lappend SQLITE_OPTIONS -DSQLITE_WINNT_MAX_PATH_CHARS=4096 + +# Options used to compile the Pikchr library. +# +set PIKCHR_OPTIONS { + -DPIKCHR_TOKEN_LIMIT=10000 +} + +# Options used to compile the included SQLite shell. +# +set SHELL_OPTIONS [concat $SQLITE_OPTIONS { + -Dmain=sqlite3_shell + -DSQLITE_SHELL_IS_UTF8=1 + -DSQLITE_OMIT_LOAD_EXTENSION=1 + -DUSE_SYSTEM_SQLITE=$(USE_SYSTEM_SQLITE) + -DSQLITE_SHELL_DBNAME_PROC=sqlcmd_get_dbname + -DSQLITE_SHELL_INIT_PROC=sqlcmd_init_proc + -DSQLITE_PS_APPDEF=sqlcmd_ps_appdef +}] + +# Options used to compile the included SQLite shell on Windows. +# +set SHELL_WIN32_OPTIONS $SHELL_OPTIONS +lappend SHELL_WIN32_OPTIONS -Daccess=file_access +lappend SHELL_WIN32_OPTIONS -Dsystem=fossil_system +lappend SHELL_WIN32_OPTIONS -Dgetenv=fossil_getenv +lappend SHELL_WIN32_OPTIONS -Dfopen=fossil_fopen + +# STOP HERE. +# Unless the build procedures changes, you should not have to edit anything +# below this line. +############################################################################# + +# Name of the final application +# +set name fossil + +# The "writeln" command sends output to the target makefile. +# +proc writeln {args} { + global output_file + if {[lindex $args 0]=="-nonewline"} { + puts -nonewline $output_file [lindex $args 1] + } else { + puts $output_file [lindex $args 0] + } +} + +# Expand any wildcards in "extra_files" +set new_extra_files {} +foreach file $extra_files { + # we need $file to resolve from $srcDir, but simply prepending + # $srcDir to each name breaks how the names are stringified and + # looked up from C. + set cwd [pwd] + cd $srcDir + foreach x [glob $file] { # -nocomplain flag? + lappend new_extra_files $x + } + cd $cwd +} +set extra_files $new_extra_files + +############################################################################## +############################################################################## +############################################################################## +# Start by generating the "main.mk" makefile used for all unix systems. +# +set mainMk $srcDir/main.mk +puts "building $mainMk" +set output_file [open $mainMk w] +fconfigure $output_file -translation binary + +writeln {# +############################################################################## +# WARNING: DO NOT EDIT, AUTOMATICALLY GENERATED FILE (SEE "tools/makemake.tcl") +############################################################################## +# +# This file is automatically generated. Instead of editing this +# file, edit "makemake.tcl" then run "tclsh makemake.tcl" +# to regenerate this file. +# +# This file is included by primary Makefile. +# + +XBCC = $(BCC) $(BCCFLAGS) +XTCC = $(TCC) $(CFLAGS_INCLUDE) -I$(OBJDIR) $(TCCFLAGS) + +TESTFLAGS = -quiet +} +writeln -nonewline "SRC =" +foreach s [lsort $src] { + writeln -nonewline " \\\n \$(SRCDIR)/$s.c" +} +writeln "\n" +writeln -nonewline "EXTRA_FILES =" +foreach s [lsort $extra_files] { + writeln -nonewline " \\\n \$(SRCDIR)/$s" +} +writeln "\n" +writeln -nonewline "TRANS_SRC =" +foreach s [lsort $src] { + writeln -nonewline " \\\n \$(OBJDIR)/${s}_.c" +} +writeln "\n" +writeln -nonewline "OBJ =" +foreach s [lsort $src] { + writeln -nonewline " \\\n \$(OBJDIR)/$s.o" +} + +writeln [string map [list \ + <<<SQLITE_OPTIONS>>> [join $SQLITE_OPTIONS " \\\n "] \ + <<<SHELL_OPTIONS>>> [join $SHELL_OPTIONS " \\\n "] \ + <<<PIKCHR_OPTIONS>>> [join $PIKCHR_OPTIONS " \\\n "] \ + <<<NEXT_LINE>>> \\] { +all: $(APPNAME) + +install: all + mkdir -p $(INSTALLDIR) + cp $(APPNAME) $(INSTALLDIR) + +codecheck: $(TRANS_SRC) $(OBJDIR)/codecheck1 + $(OBJDIR)/codecheck1 $(TRANS_SRC) + +$(OBJDIR)/translate: $(SRCDIR_tools)/translate.c + -mkdir -p $(OBJDIR) + $(XBCC) -o $(OBJDIR)/translate $(SRCDIR_tools)/translate.c + +$(OBJDIR)/makeheaders: $(SRCDIR_tools)/makeheaders.c + -mkdir -p $(OBJDIR) + $(XBCC) -o $(OBJDIR)/makeheaders $(SRCDIR_tools)/makeheaders.c + +$(OBJDIR)/mkindex: $(SRCDIR_tools)/mkindex.c + -mkdir -p $(OBJDIR) + $(XBCC) -o $(OBJDIR)/mkindex $(SRCDIR_tools)/mkindex.c + +$(OBJDIR)/mkbuiltin: $(SRCDIR_tools)/mkbuiltin.c + -mkdir -p $(OBJDIR) + $(XBCC) -o $(OBJDIR)/mkbuiltin $(SRCDIR_tools)/mkbuiltin.c + +$(OBJDIR)/mkversion: $(SRCDIR_tools)/mkversion.c + -mkdir -p $(OBJDIR) + $(XBCC) -o $(OBJDIR)/mkversion $(SRCDIR_tools)/mkversion.c + +$(OBJDIR)/codecheck1: $(SRCDIR_tools)/codecheck1.c + -mkdir -p $(OBJDIR) + $(XBCC) -o $(OBJDIR)/codecheck1 $(SRCDIR_tools)/codecheck1.c + +# Run the test suite. +# Other flags that can be included in TESTFLAGS are: +# +# -halt Stop testing after the first failed test +# -keep Keep the temporary workspace for debugging +# -prot Write a detailed log of the tests to the file ./prot +# -verbose Include even more details in the output +# -quiet Hide most output from the terminal +# -strict Treat known bugs as failures +# +# TESTFLAGS can also include names of specific test files to limit +# the run to just those test cases. +# +test: $(APPNAME) + $(TCLSH) $(SRCDIR)/../test/tester.tcl $(APPNAME) $(TESTFLAGS) + +$(OBJDIR)/VERSION.h: $(SRCDIR)/../manifest.uuid $(SRCDIR)/../manifest $(SRCDIR)/../VERSION $(OBJDIR)/mkversion $(OBJDIR)/phony.h + $(OBJDIR)/mkversion $(SRCDIR)/../manifest.uuid <<<NEXT_LINE>>> + $(SRCDIR)/../manifest <<<NEXT_LINE>>> + $(SRCDIR)/../VERSION >$(OBJDIR)/VERSION.h + +$(OBJDIR)/phony.h: + # Force rebuild of VERSION.h every time we run "make" + +# Setup the options used to compile the included SQLite library. +SQLITE_OPTIONS = <<<SQLITE_OPTIONS>>> + +# Setup the options used to compile the included SQLite shell. +SHELL_OPTIONS = <<<SHELL_OPTIONS>>> + +# Setup the options used to compile the included Pikchr formatter. +PIKCHR_OPTIONS = <<<PIKCHR_OPTIONS>>> + +# The USE_SYSTEM_SQLITE variable may be undefined, set to 0 or 1. +# If it is set to 1, then there is no need to build or link +# the sqlite3.o object. Instead, the system SQLite will be linked +# using -lsqlite3. +# +# Closely related is SQLITE3_ORIGIN, with the same numeric mapping plus +# a value of 2 means that we are building a client-provided sqlite3.c. +SQLITE3_OBJ.0 = $(OBJDIR)/sqlite3.o +SQLITE3_OBJ.1 = $(OBJDIR)/sqlite3-see.o +# SQLITE3_OBJ.2 is set by the configure process +SQLITE3_OBJ. = $(SQLITE3_OBJ.0) +SQLITE3_OBJ = $(SQLITE3_OBJ.$(SQLITE3_ORIGIN)) + +# The USE_LINENOISE variable may be undefined, set to 0, or set +# to 1. If it is set to 0, then there is no need to build or link +# the linenoise.o object. +LINENOISE_DEF.0 = +LINENOISE_DEF.1 = -DHAVE_LINENOISE=2 +LINENOISE_DEF. = $(LINENOISE_DEF.0) +LINENOISE_OBJ.0 = +LINENOISE_OBJ.1 = $(OBJDIR)/linenoise.o +LINENOISE_OBJ. = $(LINENOISE_OBJ.0) + +# The USE_SEE variable may be undefined, 0 or 1. If undefined or 0, +# in-tree SQLite is used. If 1, then sqlite3-see.c (not part of the +# source tree) is used and extra flags are provided to enable the +# SQLite Encryption Extension. +SQLITE3_SRC.0 = $(SRCDIR_extsrc)/sqlite3.c +SQLITE3_SRC.1 = $(SRCDIR_extsrc)/sqlite3-see.c +# SQLITE3_SRC.2 is set by top-level configure/makefile process. +SQLITE3_SRC. = $(SRCDIR_extsrc)/sqlite3.c +SQLITE3_SRC = $(SQLITE3_SRC.$(SQLITE3_ORIGIN)) +SQLITE3_SHELL_SRC.0 = $(SRCDIR_extsrc)/shell.c +SQLITE3_SHELL_SRC.1 = $(SRCDIR_extsrc)/shell-see.c +# SQLITE3_SHELL_SRC.2 comes from the configure process +SQLITE3_SHELL_SRC. = $(SRCDIR_extsrc)/shell.c +SQLITE3_SHELL_SRC = $(SQLITE3_SHELL_SRC.$(SQLITE3_ORIGIN)) +SEE_FLAGS.0 = +SEE_FLAGS.1 = -DSQLITE_HAS_CODEC -DSQLITE_SHELL_DBKEY_PROC=fossil_key +SEE_FLAGS. = +SEE_FLAGS = $(SEE_FLAGS.$(USE_SEE)) +}] + +writeln [string map [list <<<NEXT_LINE>>> \\] { +EXTRAOBJ = <<<NEXT_LINE>>> + $(SQLITE3_OBJ.$(SQLITE3_ORIGIN)) <<<NEXT_LINE>>> + $(LINENOISE_OBJ.$(USE_LINENOISE)) <<<NEXT_LINE>>> + $(OBJDIR)/pikchr.o <<<NEXT_LINE>>> + $(OBJDIR)/shell.o <<<NEXT_LINE>>> + $(OBJDIR)/th.o <<<NEXT_LINE>>> + $(OBJDIR)/th_lang.o <<<NEXT_LINE>>> + $(OBJDIR)/th_tcl.o <<<NEXT_LINE>>> + $(OBJDIR)/cson_amalgamation.o +}] + +writeln { +$(APPNAME): $(OBJDIR)/headers $(OBJDIR)/codecheck1 $(EXTRAOBJ) $(OBJ) + $(OBJDIR)/codecheck1 $(TRANS_SRC) + $(TCC) $(TCCFLAGS) -o $(APPNAME) $(EXTRAOBJ) $(OBJ) $(LIB) + +# This rule prevents make from using its default rules to try build +# an executable named "manifest" out of the file named "manifest.c" +# +$(SRCDIR)/../manifest: + # noop + +clean: + -rm -rf $(OBJDIR)/* $(APPNAME) + +} + +set mhargs {} +foreach s [lsort $src] { + append mhargs "\$(OBJDIR)/${s}_.c:\$(OBJDIR)/$s.h <<<NEXT_LINE>>>" + set extra_h($s) { } +} +foreach s [lsort $src_ext] { + append mhargs "\$(SRCDIR_extsrc)/${s}.c:\$(OBJDIR)/$s.h <<<NEXT_LINE>>>" + set extra_h($s) { } +} +append mhargs "\$(SRCDIR_extsrc)/sqlite3.h <<<NEXT_LINE>>>" +append mhargs "\$(SRCDIR)/th.h <<<NEXT_LINE>>>" +#append mhargs "\$(SRCDIR_extsrc)/cson_amalgamation.h <<<NEXT_LINE>>>" +append mhargs "\$(OBJDIR)/VERSION.h " +set mhargs [string map [list <<<NEXT_LINE>>> \\\n\t] $mhargs] +writeln "\$(OBJDIR)/page_index.h: \$(TRANS_SRC) \$(OBJDIR)/mkindex" +writeln "\t\$(OBJDIR)/mkindex \$(TRANS_SRC) >\$@\n" + +writeln "\$(OBJDIR)/builtin_data.h: \$(OBJDIR)/mkbuiltin \$(EXTRA_FILES)" +writeln "\t\$(OBJDIR)/mkbuiltin --prefix \$(SRCDIR)/ \$(EXTRA_FILES) >\$@\n" + +writeln "\$(OBJDIR)/headers:\t\$(OBJDIR)/page_index.h \$(OBJDIR)/builtin_data.h \$(OBJDIR)/makeheaders \$(OBJDIR)/VERSION.h" +writeln "\t\$(OBJDIR)/makeheaders $mhargs" +writeln "\ttouch \$(OBJDIR)/headers" +writeln "\$(OBJDIR)/headers: Makefile" +writeln "\$(OBJDIR)/json.o \$(OBJDIR)/json_artifact.o \$(OBJDIR)/json_branch.o \$(OBJDIR)/json_config.o \$(OBJDIR)/json_diff.o \$(OBJDIR)/json_dir.o \$(OBJDIR)/json_finfo.o \$(OBJDIR)/json_login.o \$(OBJDIR)/json_query.o \$(OBJDIR)/json_report.o \$(OBJDIR)/json_status.o \$(OBJDIR)/json_tag.o \$(OBJDIR)/json_timeline.o \$(OBJDIR)/json_user.o \$(OBJDIR)/json_wiki.o : \$(SRCDIR)/json_detail.h" + +writeln "Makefile:" +set extra_h(dispatch) " \$(OBJDIR)/page_index.h " +set extra_h(builtin) " \$(OBJDIR)/builtin_data.h " + +foreach s [lsort $src] { + writeln "\$(OBJDIR)/${s}_.c:\t\$(SRCDIR)/$s.c \$(OBJDIR)/translate" + writeln "\t\$(OBJDIR)/translate \$(SRCDIR)/$s.c >\$@\n" + writeln "\$(OBJDIR)/$s.o:\t\$(OBJDIR)/${s}_.c \$(OBJDIR)/$s.h$extra_h($s)\$(SRCDIR)/config.h" + writeln "\t\$(XTCC) -o \$(OBJDIR)/$s.o -c \$(OBJDIR)/${s}_.c\n" + writeln "\$(OBJDIR)/$s.h:\t\$(OBJDIR)/headers\n" +} + +writeln "\$(SQLITE3_OBJ):\t\$(SQLITE3_SRC)" +writeln "\t-mkdir -p \$(OBJDIR)" +writeln "\t\$(XTCC) \$(SQLITE_OPTIONS) \$(SQLITE_CFLAGS) \$(SEE_FLAGS) \\" +writeln "\t\t-c \$(SQLITE3_SRC) -o \$@" + +writeln "\$(OBJDIR)/shell.o:\t\$(SQLITE3_SHELL_SRC) \$(SRCDIR_extsrc)/sqlite3.h" +writeln "\t-mkdir -p \$(OBJDIR)" +writeln "\t\$(XTCC) \$(SHELL_OPTIONS) \$(SHELL_CFLAGS) \$(SEE_FLAGS) \$(LINENOISE_DEF.\$(USE_LINENOISE)) -c \$(SQLITE3_SHELL_SRC) -o \$@\n" + +writeln "\$(OBJDIR)/linenoise.o:\t\$(SRCDIR_extsrc)/linenoise.c \$(SRCDIR_extsrc)/linenoise.h" +writeln "\t-mkdir -p \$(OBJDIR)" +writeln "\t\$(XTCC) -c \$(SRCDIR_extsrc)/linenoise.c -o \$@\n" + +writeln "\$(OBJDIR)/th.o:\t\$(SRCDIR)/th.c" +writeln "\t-mkdir -p \$(OBJDIR)\n" +writeln "\t\$(XTCC) -c \$(SRCDIR)/th.c -o \$@\n" + +writeln "\$(OBJDIR)/th_lang.o:\t\$(SRCDIR)/th_lang.c" +writeln "\t-mkdir -p \$(OBJDIR)\n" +writeln "\t\$(XTCC) -c \$(SRCDIR)/th_lang.c -o \$@\n" + +writeln "\$(OBJDIR)/th_tcl.o:\t\$(SRCDIR)/th_tcl.c" +writeln "\t-mkdir -p \$(OBJDIR)\n" +writeln "\t\$(XTCC) -c \$(SRCDIR)/th_tcl.c -o \$@\n" + +writeln [string map [list <<<NEXT_LINE>>> \\] { +$(OBJDIR)/pikchr.o: $(SRCDIR_extsrc)/pikchr.c $(OBJDIR)/mkversion + $(XTCC) $(PIKCHR_OPTIONS) -c $(SRCDIR_extsrc)/pikchr.c -o $@ + +$(OBJDIR)/cson_amalgamation.o: $(SRCDIR_extsrc)/cson_amalgamation.c $(OBJDIR)/mkversion + $(XTCC) -c $(SRCDIR_extsrc)/cson_amalgamation.c -o $@ + +$(SRCDIR_extsrc)/pikchr.js: $(SRCDIR_extsrc)/pikchr.c $(MAKEFILE_LIST) + $(EMCC_WRAPPER) -o $@ $(EMCC_OPT) --no-entry <<<NEXT_LINE>>> + -sEXPORTED_RUNTIME_METHODS=cwrap,ccall,setValue,getValue,stackSave,stackAlloc,stackRestore <<<NEXT_LINE>>> + -sEXPORTED_FUNCTIONS=_pikchr,_pikchr_version $(SRCDIR_extsrc)/pikchr.c <<<NEXT_LINE>>> + -sENVIRONMENT=web <<<NEXT_LINE>>> + -sMODULARIZE <<<NEXT_LINE>>> + -sEXPORT_NAME=initPikchrModule <<<NEXT_LINE>>> + --minify 0 + $(TCLSH) $(TOPDIR)/tools/randomize-js-names.tcl $(SRCDIR_extsrc) + @chmod -x $(SRCDIR_extsrc)/pikchr.wasm +wasm: $(SRCDIR_extsrc)/pikchr.js + +# +# compile_commands.json support... +# +# We have to avoid applying compile_commands support to the in-tree +# tools, as those compile with BCC, which may differ from TCC. +# e.g. BCC might be gcc (which does not support -MJ ...) while TCC is +# clang (which does). +# +# What follows is more verbose than strictly necessary because we're +# limited to POSIX make syntax. +all: +compile-commands-dir.yes = $(OBJDIR) +compile-commands-dir.no = +compile-commands-dir = $(compile-commands-dir.$(MAKE_COMPILATION_DB)) +compile-command-args.yes = -MJ $(TOPDIR)/$(compile-commands-dir)/$(@F:.o=.o.json) +compile-command-args.no = +TCCFLAGS += $(compile-command-args.$(MAKE_COMPILATION_DB)) +compile_commands.json = $(TOPDIR)/compile_commands.json +# compile_commands.json is a concatenation of the .o.json files +# generated by the compilation process via TCCFLAGS. We have a +# potential race condition in parallel builds, where a .o.json file is +# not yet written to completion before compile_commands.json is +# processed. How to resolve that in a way compatible with POSIX make +# is unclear. +# +# This obscure sed bit ensures that the resulting JSON array does not +# have a trailing comma. +$(compile_commands.json): $(OBJ) + @-rm -f $@ + @{ echo '['; cat $(compile-commands-dir)/*.o.json | tr '\n' ' ' | sed -e 's/, $$//'; echo ']'; } > $@ + @echo "Generated $@" +compile-commands.no: +compile-commands.yes: $(compile_commands.json) +all: compile-commands.$(MAKE_COMPILATION_DB) +clean: compile-commands-clean +compile-commands-clean: + rm -fr $(compile_commands.json) + +# +# End compile_commands.json support +# + +# +# The list of all the targets that do not correspond to real files. This stops +# 'make' from getting confused when someone makes an error in a rule. +# + +.PHONY: all install test clean +.PHONY: compile-commands-clean compile-commands-dir +}] + +close $output_file +# +# End of the main.mk output +############################################################################## +############################################################################## +############################################################################## +# Begin win/Makefile.mingw output +# +puts "building ../win/Makefile.mingw" +set output_file [open ../win/Makefile.mingw w] +fconfigure $output_file -translation binary + +writeln {#!/usr/bin/make +# +############################################################################## +# WARNING: DO NOT EDIT, AUTOMATICALLY GENERATED FILE (SEE "tools/makemake.tcl") +############################################################################## +# +# This file is automatically generated. Instead of editing this +# file, edit "makemake.tcl" then run "tclsh makemake.tcl" +# to regenerate this file. +# +# This is a makefile for use on Cygwin/Darwin/FreeBSD/Linux/Windows using +# MinGW or MinGW-w64. +# +# Some of the special options which can be passed to make +# USE_WINDOWS=1 if building under a windows command prompt +# X64=1 if using an unprefixed 64-bit mingw compiler +# + +#### Select one of MinGW, MinGW-w64 (32-bit) or MinGW-w64 (64-bit) compilers. +# By default, this is an empty string (i.e. use the native compiler). +# +PREFIX = +# PREFIX = mingw32- +# PREFIX = i686-pc-mingw32- +# PREFIX = i686-w64-mingw32- +# PREFIX = x86_64-w64-mingw32- + +#### The toplevel directory of the source tree. Fossil can be built +# in a directory that is separate from the source tree. Just change +# the following to point from the build directory to the src/ folder. +# +SRCDIR = src +SRCDIR_extsrc = extsrc +SRCDIR_tools = tools + +#### The directory into which object code files should be written. +# +OBJDIR = wbld + +#### C compiler for use in building executables that will run on +# the platform that is doing the build. This is used to compile +# code-generator programs as part of the build process. See TCC +# and TCCEXE below for the C compiler for building the finished +# binary. +# +BCCEXE = gcc + +#### C Compiler and options for use in building executables that +# will run on the platform that is doing the build. This is used +# to compile code-generator programs as part of the build process. +# See TCC below for the C compiler for building the finished binary. +# +BCC = $(BCCEXE) + +#### Enable compiling with debug symbols (much larger binary) +# +# FOSSIL_ENABLE_SYMBOLS = 1 + +#### Enable JSON (https://www.json.org) support using "cson" +# +# FOSSIL_ENABLE_JSON = 1 + +#### Enable HTTPS support via OpenSSL (links to libssl and libcrypto) +# +# FOSSIL_ENABLE_SSL = 1 + +#### Automatically build OpenSSL when building Fossil (causes rebuild +# issues when building incrementally). +# +# FOSSIL_BUILD_SSL = 1 + +#### Enable relative paths in external diff/gdiff +# +# FOSSIL_ENABLE_EXEC_REL_PATHS = 1 + +#### Enable TH1 scripts in embedded documentation files +# +# FOSSIL_ENABLE_TH1_DOCS = 1 + +#### Enable hooks for commands and web pages via TH1 +# +# FOSSIL_ENABLE_TH1_HOOKS = 1 + +#### Enable scripting support via Tcl/Tk +# +# FOSSIL_ENABLE_TCL = 1 + +#### Load Tcl using the stubs library mechanism +# +# FOSSIL_ENABLE_TCL_STUBS = 1 + +#### Load Tcl using the private stubs mechanism +# +# FOSSIL_ENABLE_TCL_PRIVATE_STUBS = 1 + +#### Use 'system' SQLite +# +# USE_SYSTEM_SQLITE = 1 + +#### Use POSIX memory APIs from "sys/mman.h" +# +# USE_MMAN_H = 1 + +#### Use the SQLite Encryption Extension +# +# USE_SEE = 1 + +#### Use the Tcl source directory instead of the install directory? +# This is useful when Tcl has been compiled statically with MinGW. +# +FOSSIL_TCL_SOURCE = 1 + +#### Check if the workaround for the MinGW command line handling needs to +# be enabled by default. This check may be somewhat fragile due to the +# use of "findstring". +# +ifndef MINGW_IS_32BIT_ONLY +ifeq (,$(findstring w64-mingw32,$(PREFIX))) +MINGW_IS_32BIT_ONLY = 1 +endif +endif + +#### The directories where the zlib include and library files are located. +# +ZINCDIR = $(SRCDIR)/../compat/zlib +ZLIBDIR = $(SRCDIR)/../compat/zlib + +#### Make an attempt to detect if Fossil is being built for the x64 processor +# architecture. This check may be somewhat fragile due to "findstring". +# +ifndef X64 +ifneq (,$(findstring x86_64-w64-mingw32,$(PREFIX))) +X64 = 1 +endif +endif + +#### Determine if the optimized assembly routines provided with zlib should be +# used, taking into account whether zlib is actually enabled and the target +# processor architecture. +# +ifndef X64 +SSLCONFIG = mingw +ZLIBCONFIG = +ZLIBTARGETS = +else +SSLCONFIG = mingw64 +ZLIBCONFIG = +ZLIBTARGETS = +endif + +#### Disable creation of the OpenSSL shared libraries. Also, disable support +# for SSLv3 (i.e. thereby forcing the use of TLS). +# +SSLCONFIG += no-ssl3 no-weak-ssl-ciphers no-shared + +#### When using zlib, make sure that OpenSSL is configured to use the zlib +# that Fossil knows about (i.e. the one within the source tree). +# +SSLCONFIG += --with-zlib-lib=$(PWD)/$(ZLIBDIR) --with-zlib-include=$(PWD)/$(ZLIBDIR) zlib + +#### The directories where the OpenSSL include and library files are located. +# +OPENSSLDIR = $(SRCDIR)/../compat/openssl +OPENSSLINCDIR = $(OPENSSLDIR)/include +OPENSSLLIBDIR = $(OPENSSLDIR) + +#### Either the directory where the Tcl library is installed or the Tcl +# source code directory resides (depending on the value of the macro +# FOSSIL_TCL_SOURCE). If this points to the Tcl install directory, +# this directory must have "include" and "lib" sub-directories. If +# this points to the Tcl source code directory, this directory must +# have "generic" and "win" sub-directories. The recommended usage +# here is to use the Sysinternals junction tool to create a hard +# link between a "tcl-8.x" sub-directory of the Fossil source code +# directory and the target Tcl directory. This removes the need to +# hard-code the necessary paths in this Makefile. +# +TCLDIR = $(SRCDIR)/../compat/tcl-8.6 + +#### The Tcl source code directory. This defaults to the same value as +# TCLDIR macro (above), which may not be correct. This value will +# only be used if the FOSSIL_TCL_SOURCE macro is defined. +# +TCLSRCDIR = $(TCLDIR) + +#### The Tcl include and library directories. These values will only be +# used if the FOSSIL_TCL_SOURCE macro is not defined. +# +TCLINCDIR = $(TCLDIR)/include +TCLLIBDIR = $(TCLDIR)/lib + +#### Tcl: Which Tcl library do we want to use (8.4, 8.5, 8.6, etc)? +# +ifdef FOSSIL_ENABLE_TCL_STUBS +ifndef FOSSIL_ENABLE_TCL_PRIVATE_STUBS +LIBTCL = -ltclstub86 +endif +TCLTARGET = libtclstub86.a +else +LIBTCL = -ltcl86 +TCLTARGET = binaries +endif + +#### C compiler for use in building executables that will run on the +# target platform. This is usually the same as BCCEXE, unless you +# are cross-compiling. This C compiler builds the finished binary +# for fossil. See BCC and BCCEXE above for the C compiler for +# building intermediate code-generator tools. +# +TCCEXE = gcc + +#### C compiler and options for use in building executables that will +# run on the target platform. This is usually the almost the same +# as BCC, unless you are cross-compiling. This C compiler builds +# the finished binary for fossil. The BCC compiler above is used +# for building intermediate code-generator tools. +# +TCC = $(PREFIX)$(TCCEXE) -Wall -Wdeclaration-after-statement +TCC += -I$(SRCDIR_extsrc) + +#### Add the necessary command line options to build with debugging +# symbols, if enabled. +# +ifdef FOSSIL_ENABLE_SYMBOLS +TCC += -g +else +TCC += -Os +endif + +TCC += -L$(ZLIBDIR) -I$(ZINCDIR) + +#### Compile resources for use in building executables that will run +# on the target platform. +# +RCC = $(PREFIX)windres -I$(SRCDIR) -I$(ZINCDIR) +RCC += -I$(SRCDIR_extsrc) + +# With HTTPS support +ifdef FOSSIL_ENABLE_SSL +TCC += -L$(OPENSSLLIBDIR) -I$(OPENSSLINCDIR) +RCC += -I$(OPENSSLINCDIR) +endif + +# With Tcl support +ifdef FOSSIL_ENABLE_TCL +ifdef FOSSIL_TCL_SOURCE +TCC += -L$(TCLSRCDIR)/win -I$(TCLSRCDIR)/generic -I$(TCLSRCDIR)/win +RCC += -I$(TCLSRCDIR)/generic -I$(TCLSRCDIR)/win +else +TCC += -L$(TCLLIBDIR) -I$(TCLINCDIR) +RCC += -I$(TCLINCDIR) +endif +endif + +# With MinGW command line handling workaround +ifdef MINGW_IS_32BIT_ONLY +TCC += -DBROKEN_MINGW_CMDLINE=1 +RCC += -DBROKEN_MINGW_CMDLINE=1 +endif + +# With HTTPS support +ifdef FOSSIL_ENABLE_SSL +TCC += -DFOSSIL_ENABLE_SSL=1 +RCC += -DFOSSIL_ENABLE_SSL=1 +endif + +# With relative paths in external diff/gdiff +ifdef FOSSIL_ENABLE_EXEC_REL_PATHS +TCC += -DFOSSIL_ENABLE_EXEC_REL_PATHS=1 +RCC += -DFOSSIL_ENABLE_EXEC_REL_PATHS=1 +endif + +# With TH1 embedded docs support +ifdef FOSSIL_ENABLE_TH1_DOCS +TCC += -DFOSSIL_ENABLE_TH1_DOCS=1 +RCC += -DFOSSIL_ENABLE_TH1_DOCS=1 +endif + +# With TH1 hook support +ifdef FOSSIL_ENABLE_TH1_HOOKS +TCC += -DFOSSIL_ENABLE_TH1_HOOKS=1 +RCC += -DFOSSIL_ENABLE_TH1_HOOKS=1 +endif + +# With Tcl support +ifdef FOSSIL_ENABLE_TCL +TCC += -DFOSSIL_ENABLE_TCL=1 +RCC += -DFOSSIL_ENABLE_TCL=1 +# Either statically linked or via stubs +ifdef FOSSIL_ENABLE_TCL_STUBS +TCC += -DFOSSIL_ENABLE_TCL_STUBS=1 -DUSE_TCL_STUBS +RCC += -DFOSSIL_ENABLE_TCL_STUBS=1 -DUSE_TCL_STUBS +ifdef FOSSIL_ENABLE_TCL_PRIVATE_STUBS +TCC += -DFOSSIL_ENABLE_TCL_PRIVATE_STUBS=1 +RCC += -DFOSSIL_ENABLE_TCL_PRIVATE_STUBS=1 +endif +else +TCC += -DSTATIC_BUILD +RCC += -DSTATIC_BUILD +endif +endif + +# With JSON support +ifdef FOSSIL_ENABLE_JSON +TCC += -DFOSSIL_ENABLE_JSON=1 +RCC += -DFOSSIL_ENABLE_JSON=1 +endif + +# With "sys/mman.h" support +ifdef USE_MMAN_H +TCC += -DUSE_MMAN_H=1 +RCC += -DUSE_MMAN_H=1 +endif + +# With SQLite Encryption Extension support +ifdef USE_SEE +TCC += -DUSE_SEE=1 +RCC += -DUSE_SEE=1 +endif + +#### The option -static has no effect on MinGW(-w64), only dynamic +# executables can be built when linking with MSVCRT. OpenSSL +# (optional) and zlib (required) however are always linked in +# statically. Therefore, the FOSSIL_DYNAMIC_BUILD option does +# not really apply to MinGW (i.e. since ALL external libraries +# are NOT linked dynamically). +# +# LIB = -static + +#### MinGW: If available, use the Unicode capable runtime startup code. +# +ifndef MINGW_IS_32BIT_ONLY +LIB += -municode +endif + +#### SQLite: If enabled, use the system SQLite library. +# +ifdef USE_SYSTEM_SQLITE +LIB += -lsqlite3 +endif + +#### OpenSSL: Add the necessary libraries required, if enabled. +# +ifdef FOSSIL_ENABLE_SSL +LIB += -lssl -lcrypto -lgdi32 -lcrypt32 +endif + +#### Tcl: Add the necessary libraries required, if enabled. +# +ifdef FOSSIL_ENABLE_TCL +LIB += $(LIBTCL) +endif + +#### Extra arguments for linking the finished binary. Fossil needs +# to link against the Z-Lib compression library. There are no +# other mandatory dependencies. +# +LIB += -lmingwex + +#### zlib is required. +# +LIB += -lz + +#### These libraries MUST appear in the same order as they do for Tcl +# or linking with it will not work (exact reason unknown). +# +ifdef FOSSIL_ENABLE_TCL +ifdef FOSSIL_ENABLE_TCL_STUBS +LIB += -lkernel32 -lws2_32 +else +LIB += -lnetapi32 -lkernel32 -luser32 -ladvapi32 -lws2_32 +endif +else +LIB += -lkernel32 -lws2_32 +endif + +#### Library required for DNS lookups. +# +LIB += -ldnsapi + +#### Tcl shell for use in running the fossil test suite. This is only +# used for testing. +# +TCLSH = tclsh + +#### Nullsoft installer MakeNSIS location +# +MAKENSIS = "$(PROGRAMFILES)\NSIS\MakeNSIS.exe" + +#### Inno Setup executable location +# +INNOSETUP = "$(PROGRAMFILES)\Inno Setup 5\ISCC.exe" + +#### Include a configuration file that can override any one of these settings. +# +-include config.w32 + +# STOP HERE +# You should not need to change anything below this line +#-------------------------------------------------------- +XBCC = $(BCC) $(CFLAGS) +XTCC = $(TCC) $(CFLAGS) -I. -I$(SRCDIR) +} +writeln -nonewline "SRC =" +foreach s [lsort $src] { + writeln -nonewline " \\\n \$(SRCDIR)/$s.c" +} +writeln "\n" +writeln -nonewline "EXTRA_FILES =" +foreach s [lsort $extra_files] { + writeln -nonewline " \\\n \$(SRCDIR)/$s" +} +writeln "\n" +writeln -nonewline "TRANS_SRC =" +foreach s [lsort $src] { + writeln -nonewline " \\\n \$(OBJDIR)/${s}_.c" +} +writeln "\n" +writeln -nonewline "OBJ =" +foreach s [lsort $src] { + writeln -nonewline " \\\n \$(OBJDIR)/$s.o" +} +writeln "\n" +writeln "APPNAME = ${name}.exe" +writeln "APPTARGETS =" +writeln { +#### If the USE_WINDOWS variable exists, it is assumed that we are building +# inside of a Windows-style shell; otherwise, it is assumed that we are +# building inside of a Unix-style shell. Note that the "move" command is +# broken when attempting to use it from the Windows shell via MinGW make +# because the SHELL variable is only used for certain commands that are +# recognized internally by make. +# +ifdef USE_WINDOWS +TRANSLATE = $(subst /,\,$(OBJDIR)/translate.exe) +MAKEHEADERS = $(subst /,\,$(OBJDIR)/makeheaders.exe) +MKINDEX = $(subst /,\,$(OBJDIR)/mkindex.exe) +MKBUILTIN = $(subst /,\,$(OBJDIR)/mkbuiltin.exe) +MKVERSION = $(subst /,\,$(OBJDIR)/mkversion.exe) +CODECHECK1 = $(subst /,\,$(OBJDIR)/codecheck1.exe) +CAT = type +CP = copy +GREP = find +MV = copy +RM = del /Q +MKDIR = -mkdir +RMDIR = rmdir /S /Q +else +TRANSLATE = $(OBJDIR)/translate.exe +MAKEHEADERS = $(OBJDIR)/makeheaders.exe +MKINDEX = $(OBJDIR)/mkindex.exe +MKBUILTIN = $(OBJDIR)/mkbuiltin.exe +MKVERSION = $(OBJDIR)/mkversion.exe +CODECHECK1 = $(OBJDIR)/codecheck1.exe +CAT = cat +CP = cp +GREP = grep +MV = mv +RM = rm -f +MKDIR = -mkdir -p +RMDIR = rm -rf +endif} + +writeln { +all: $(OBJDIR) $(APPNAME) + +$(OBJDIR)/fossil.o: $(SRCDIR)/../win/fossil.rc $(OBJDIR)/VERSION.h +ifdef USE_WINDOWS + $(CP) $(subst /,\,$(SRCDIR)\..\win\fossil.rc) $(subst /,\,$(OBJDIR)) + $(CP) $(subst /,\,$(SRCDIR)\..\win\fossil.ico) $(subst /,\,$(OBJDIR)) + $(CP) $(subst /,\,$(SRCDIR)\..\win\fossil.exe.manifest) $(subst /,\,$(OBJDIR)) +else + $(CP) $(SRCDIR)/../win/fossil.rc $(OBJDIR) + $(CP) $(SRCDIR)/../win/fossil.ico $(OBJDIR) + $(CP) $(SRCDIR)/../win/fossil.exe.manifest $(OBJDIR) +endif + $(RCC) $(OBJDIR)/fossil.rc -o $(OBJDIR)/fossil.o + +install: $(OBJDIR) $(APPNAME) +ifdef USE_WINDOWS + $(MKDIR) $(subst /,\,$(INSTALLDIR)) + $(CP) $(subst /,\,$(APPNAME)) $(subst /,\,$(INSTALLDIR)) +else + $(MKDIR) $(INSTALLDIR) + $(CP) $(APPNAME) $(INSTALLDIR) +endif + +$(OBJDIR): +ifdef USE_WINDOWS + $(MKDIR) $(subst /,\,$(OBJDIR)) +else + $(MKDIR) $(OBJDIR) +endif + +$(TRANSLATE): $(SRCDIR_tools)/translate.c + $(XBCC) -o $@ $(SRCDIR_tools)/translate.c + +$(MAKEHEADERS): $(SRCDIR_tools)/makeheaders.c + $(XBCC) -o $@ $(SRCDIR_tools)/makeheaders.c + +$(MKINDEX): $(SRCDIR_tools)/mkindex.c + $(XBCC) -o $@ $(SRCDIR_tools)/mkindex.c + +$(MKBUILTIN): $(SRCDIR_tools)/mkbuiltin.c + $(XBCC) -o $@ $(SRCDIR_tools)/mkbuiltin.c + +$(MKVERSION): $(SRCDIR_tools)/mkversion.c + $(XBCC) -o $@ $(SRCDIR_tools)/mkversion.c + +$(CODECHECK1): $(SRCDIR_tools)/codecheck1.c + $(XBCC) -o $@ $(SRCDIR_tools)/codecheck1.c + +# WARNING. DANGER. Running the test suite modifies the repository the +# build is done from, i.e. the checkout belongs to. Do not sync/push +# the repository after running the tests. +test: $(OBJDIR) $(APPNAME) + $(TCLSH) $(SRCDIR)/../test/tester.tcl $(APPNAME) + +$(OBJDIR)/VERSION.h: $(SRCDIR)/../manifest.uuid $(SRCDIR)/../manifest $(MKVERSION) $(OBJDIR)/phony.h + $(MKVERSION) $(SRCDIR)/../manifest.uuid $(SRCDIR)/../manifest $(SRCDIR)/../VERSION >$@ + +$(OBJDIR)/phony.h: + # Force rebuild of VERSION.h every time "make" is run + +# The USE_SYSTEM_SQLITE variable may be undefined, set to 0 or 1. +# If it is set to 1, then there is no need to build or link +# the sqlite3.o object. Instead, the system SQLite will be linked +# using -lsqlite3. +# +# Closely related is SQLITE3_ORIGIN, with the same 0/1 mapping, +# plus a value of 2 means that we are building a client-provided +# sqlite3.c. +SQLITE3_OBJ.0 = $(OBJDIR)/sqlite3.o +SQLITE3_OBJ.1 = $(OBJDIR)/sqlite3-see.o +# SQLITE3_OBJ.2 is set by the configure process +SQLITE3_OBJ. = $(SQLITE3_OBJ.0) +SQLITE3_OBJ = $(SQLITE3_OBJ.$(SQLITE3_ORIGIN)) + +# The USE_SEE variable may be undefined, 0 or 1. If undefined or 0, +# in-tree SQLite is used. If 1, then sqlite3-see.c (not part of the +# source tree) is used and extra flags are provided to enable the +# SQLite Encryption Extension. +SQLITE3_SRC.0 = $(SRCDIR_extsrc)/sqlite3.c +SQLITE3_SRC.1 = $(SRCDIR_extsrc)/sqlite3-see.c +# SQLITE3_SRC.2 is set by top-level configure/makefile process. +SQLITE3_SRC. = $(SRCDIR_extsrc)/sqlite3.c +SQLITE3_SRC = $(SQLITE3_SRC.$(SQLITE3_ORIGIN)) +SQLITE3_SHELL_SRC.0 = $(SRCDIR_extsrc)/shell.c +SQLITE3_SHELL_SRC.1 = $(SRCDIR_extsrc)/shell-see.c +# SQLITE3_SHELL_SRC.2 comes from the configure process +SQLITE3_SHELL_SRC. = $(SRCDIR_extsrc)/shell.c +SQLITE3_SHELL_SRC = $(SQLITE3_SHELL_SRC.$(SQLITE3_ORIGIN)) +SEE_FLAGS.0 = +SEE_FLAGS.1 = -DSQLITE_HAS_CODEC -DSQLITE_SHELL_DBKEY_PROC=fossil_key +SEE_FLAGS. = +SEE_FLAGS = $(SEE_FLAGS.$(USE_SEE)) +} + +writeln [string map [list <<<NEXT_LINE>>> \\] { +EXTRAOBJ = <<<NEXT_LINE>>> + $(SQLITE3_OBJ.$(SQLITE3_ORIGIN)) <<<NEXT_LINE>>> + $(OBJDIR)/pikchr.o <<<NEXT_LINE>>> + $(OBJDIR)/shell.o <<<NEXT_LINE>>> + $(OBJDIR)/th.o <<<NEXT_LINE>>> + $(OBJDIR)/th_lang.o <<<NEXT_LINE>>> + $(OBJDIR)/th_tcl.o <<<NEXT_LINE>>> + $(OBJDIR)/cson_amalgamation.o +}] + +writeln { +zlib: $(ZLIBTARGETS) + $(MAKE) -C $(ZLIBDIR) PREFIX=$(PREFIX) CC=$(PREFIX)$(TCCEXE) $(ZLIBCONFIG) -f win32/Makefile.gcc libz.a + +clean-zlib: + $(MAKE) -C $(ZLIBDIR) PREFIX=$(PREFIX) CC=$(PREFIX)$(TCCEXE) -f win32/Makefile.gcc clean + +BLDTARGETS = zlib + +openssl: $(BLDTARGETS) + cd $(OPENSSLLIBDIR);./Configure --cross-compile-prefix=$(PREFIX) $(SSLCONFIG) + sed -i -e 's/^PERL=C:\\.*$$/PERL=perl.exe/i' $(OPENSSLLIBDIR)/Makefile + $(MAKE) -C $(OPENSSLLIBDIR) PREFIX=$(PREFIX) CC=$(PREFIX)$(TCCEXE) build_libs + +clean-openssl: + $(MAKE) -C $(OPENSSLLIBDIR) PREFIX=$(PREFIX) CC=$(PREFIX)$(TCCEXE) clean + +tcl: + cd $(TCLSRCDIR)/win;./configure + $(MAKE) -C $(TCLSRCDIR)/win PREFIX=$(PREFIX) CC=$(PREFIX)$(TCCEXE) $(TCLTARGET) + +clean-tcl: + $(MAKE) -C $(TCLSRCDIR)/win PREFIX=$(PREFIX) CC=$(PREFIX)$(TCCEXE) distclean + +APPTARGETS += $(BLDTARGETS) + +ifdef FOSSIL_BUILD_SSL +APPTARGETS += openssl +endif + +$(APPNAME): $(APPTARGETS) $(OBJDIR)/headers $(CODECHECK1) $(EXTRAOBJ) $(OBJ) $(OBJDIR)/fossil.o + $(CODECHECK1) $(TRANS_SRC) + $(TCC) -o $@ $(EXTRAOBJ) $(OBJ) $(OBJDIR)/fossil.o $(LIB) + +# This rule prevents make from using its default rules to try build +# an executable named "manifest" out of the file named "manifest.c" +# +$(SRCDIR)/../manifest: + # noop + +clean: +ifdef USE_WINDOWS + $(RM) $(subst /,\,$(APPNAME)) + $(RMDIR) $(subst /,\,$(OBJDIR)) +else + $(RM) $(APPNAME) + $(RMDIR) $(OBJDIR) +endif + +setup: $(OBJDIR) $(APPNAME) + $(MAKENSIS) ./setup/fossil.nsi + +innosetup: $(OBJDIR) $(APPNAME) + $(INNOSETUP) ./setup/fossil.iss -DAppVersion=$(shell $(CAT) ./VERSION) +} + +set mhargs {} +foreach s [lsort $src] { + if {[string length $mhargs] > 0} {append mhargs " <<<NEXT_LINE>>>"} + append mhargs "\$(OBJDIR)/${s}_.c:\$(OBJDIR)/$s.h" + set extra_h($s) { } +} +foreach s [lsort $src_ext] { + if {[string length $mhargs] > 0} {append mhargs " <<<NEXT_LINE>>>"} + append mhargs "\$(SRCDIR_extsrc)/${s}.c:\$(OBJDIR)/$s.h" + set extra_h($s) { } +} +append mhargs " <<<NEXT_LINE>>>\$(SRCDIR_extsrc)/sqlite3.h" +append mhargs " <<<NEXT_LINE>>>\$(SRCDIR)/th.h" +append mhargs " <<<NEXT_LINE>>>\$(OBJDIR)/VERSION.h" +set mhargs [string map [list <<<NEXT_LINE>>> \\\n\t] $mhargs] +writeln "\$(OBJDIR)/page_index.h: \$(TRANS_SRC) \$(MKINDEX)" +writeln "\t\$(MKINDEX) \$(TRANS_SRC) >\$@\n" + +writeln "\$(OBJDIR)/builtin_data.h:\t\$(MKBUILTIN) \$(EXTRA_FILES)" +writeln "\t\$(MKBUILTIN) --prefix \$(SRCDIR)/ \$(EXTRA_FILES) >\$@\n" + +writeln "\$(OBJDIR)/headers:\t\$(OBJDIR)/page_index.h \$(OBJDIR)/builtin_data.h \$(MAKEHEADERS) \$(OBJDIR)/VERSION.h" +writeln "\t\$(MAKEHEADERS) $mhargs" +writeln "\techo Done >\$(OBJDIR)/headers\n" +writeln "\$(OBJDIR)/headers: Makefile\n" +writeln "Makefile:\n" +set extra_h(main) " \$(OBJDIR)/page_index.h " +set extra_h(builtin) " \$(OBJDIR)/builtin_data.h " + +foreach s [lsort $src] { + writeln "\$(OBJDIR)/${s}_.c:\t\$(SRCDIR)/$s.c \$(TRANSLATE)" + writeln "\t\$(TRANSLATE) \$(SRCDIR)/$s.c >\$@\n" + writeln "\$(OBJDIR)/$s.o:\t\$(OBJDIR)/${s}_.c \$(OBJDIR)/$s.h$extra_h($s)\$(SRCDIR)/config.h" + writeln "\t\$(XTCC) -o \$(OBJDIR)/$s.o -c \$(OBJDIR)/${s}_.c\n" + writeln "\$(OBJDIR)/${s}.h:\t\$(OBJDIR)/headers\n" +} + +writeln {MINGW_OPTIONS = -D_HAVE__MINGW_H +} + +set SQLITE_WIN32_OPTIONS $SQLITE_OPTIONS +lappend SQLITE_WIN32_OPTIONS -DSQLITE_WIN32_NO_ANSI + +set MINGW_SQLITE_OPTIONS $SQLITE_WIN32_OPTIONS +lappend MINGW_SQLITE_OPTIONS {$(MINGW_OPTIONS)} +lappend MINGW_SQLITE_OPTIONS -DSQLITE_USE_MALLOC_H +lappend MINGW_SQLITE_OPTIONS -DSQLITE_USE_MSIZE + +set MINGW_PIKCHR_OPTIONS $PIKCHR_OPTIONS + +set j " \\\n " +writeln "SQLITE_OPTIONS = [join $MINGW_SQLITE_OPTIONS $j]\n" +writeln "SHELL_OPTIONS = [join $SHELL_WIN32_OPTIONS $j]\n" +writeln "PIKCHR_OPTIONS = [join $MINGW_PIKCHR_OPTIONS $j]\n" + +writeln "\$(SQLITE3_OBJ):\t\$(SQLITE3_SRC) \$(SRCDIR)/../win/Makefile.mingw" +writeln "\t\$(XTCC) \$(SQLITE_OPTIONS) \$(SQLITE_CFLAGS) \$(SEE_FLAGS) \\" +writeln "\t\t-c \$(SQLITE3_SRC) -o \$@\n" + +writeln "\$(OBJDIR)/cson_amalgamation.o:\t\$(SRCDIR_extsrc)/cson_amalgamation.c" +writeln "\t\$(XTCC) -c \$(SRCDIR_extsrc)/cson_amalgamation.c -o \$@\n" +writeln "\$(OBJDIR)/json.o \$(OBJDIR)/json_artifact.o \$(OBJDIR)/json_branch.o \$(OBJDIR)/json_config.o \$(OBJDIR)/json_diff.o \$(OBJDIR)/json_dir.o \$(OBJDIR)/jsos_finfo.o \$(OBJDIR)/json_login.o \$(OBJDIR)/json_query.o \$(OBJDIR)/json_report.o \$(OBJDIR)/json_status.o \$(OBJDIR)/json_tag.o \$(OBJDIR)/json_timeline.o \$(OBJDIR)/json_user.o \$(OBJDIR)/json_wiki.o : \$(SRCDIR)/json_detail.h\n" + +writeln "\$(OBJDIR)/shell.o:\t\$(SQLITE3_SHELL_SRC) \$(SRCDIR_extsrc)/sqlite3.h \$(SRCDIR)/../win/Makefile.mingw" +writeln "\t\$(XTCC) \$(SHELL_OPTIONS) \$(SHELL_CFLAGS) \$(SEE_FLAGS) -c \$(SQLITE3_SHELL_SRC) -o \$@\n" + +writeln "\$(OBJDIR)/th.o:\t\$(SRCDIR)/th.c" +writeln "\t\$(XTCC) -c \$(SRCDIR)/th.c -o \$@\n" + +writeln "\$(OBJDIR)/th_lang.o:\t\$(SRCDIR)/th_lang.c" +writeln "\t\$(XTCC) -c \$(SRCDIR)/th_lang.c -o \$@\n" + +writeln "\$(OBJDIR)/th_tcl.o:\t\$(SRCDIR)/th_tcl.c" +writeln "\t\$(XTCC) -c \$(SRCDIR)/th_tcl.c -o \$@\n" + +writeln "\$(OBJDIR)/pikchr.o:\t\$(SRCDIR_extsrc)/pikchr.c" +writeln "\t\$(XTCC) \$(PIKCHR_OPTIONS) -c \$(SRCDIR_extsrc)/pikchr.c -o \$@\n" + +close $output_file +# +# End of the win/Makefile.mingw output +############################################################################## +############################################################################## +############################################################################## +# Begin win/Makefile.dmc output +# +puts "building ../win/Makefile.dmc" +set output_file [open ../win/Makefile.dmc w] +fconfigure $output_file -translation binary + +writeln {# +############################################################################## +# WARNING: DO NOT EDIT, AUTOMATICALLY GENERATED FILE (SEE "tools/makemake.tcl") +############################################################################## +# +# This file is automatically generated. Instead of editing this +# file, edit "makemake.tcl" then run "tclsh makemake.tcl" +# to regenerate this file. +# +B = .. +SRCDIR = $B\src +SRCDIR_extsrc = $B\extsrc +SRCDIR_tools = $B\tools +OBJDIR = . +O = .obj +E = .exe + + +# Maybe DMDIR, SSL or INCL needs adjustment +DMDIR = c:\DM +INCL = -I. -I$(SRCDIR) -I$(SRCDIR_extsrc) -I$B\win\include -I$(DMDIR)\extra\include + +#SSL = -DFOSSIL_ENABLE_SSL=1 +SSL = + +CFLAGS = -o +BCC = $(DMDIR)\bin\dmc $(CFLAGS) +TCC = $(DMDIR)\bin\dmc $(CFLAGS) $(DMCDEF) $(SSL) $(INCL) +LIBS = $(DMDIR)\extra\lib\ zlib wsock32 advapi32 dnsapi +} +writeln "SQLITE_OPTIONS = [join $SQLITE_OPTIONS { }]\n" +writeln "SHELL_OPTIONS = [join $SHELL_WIN32_OPTIONS { }]\n" +writeln "PIKCHR_OPTIONS = [join $PIKCHR_OPTIONS { }]\n" +writeln -nonewline "SRC =" +foreach s [lsort $src] { + writeln -nonewline " ${s}_.c" +} +writeln "\n" +writeln -nonewline "OBJ = " +foreach s [lsort $src] { + writeln -nonewline "\$(OBJDIR)\\$s\$O " +} +writeln "\$(OBJDIR)\\shell\$O \$(OBJDIR)\\sqlite3\$O \$(OBJDIR)\\th\$O \$(OBJDIR)\\th_lang\$O" +writeln { + +RC=$(DMDIR)\bin\rcc +RCFLAGS=-32 -w1 -I$(SRCDIR) /D__DMC__ + +APPNAME = $(OBJDIR)\fossil$(E) + +all: $(APPNAME) + +$(APPNAME) : translate$E mkindex$E codecheck1$E headers $(OBJ) $(OBJDIR)\link + cd $(OBJDIR) + codecheck1$E $(SRC) + $(DMDIR)\bin\link @link + +$(OBJDIR)\fossil.res: $B\win\fossil.rc + $(RC) $(RCFLAGS) -o$@ $** + +$(OBJDIR)\link: $B\win\Makefile.dmc $(OBJDIR)\fossil.res} +writeln -nonewline "\t+echo " +foreach s [lsort $src] { + writeln -nonewline "$s " +} +writeln "shell sqlite3 th th_lang > \$@" +writeln "\t+echo fossil >> \$@" +writeln "\t+echo fossil >> \$@" +writeln "\t+echo \$(LIBS) >> \$@" +writeln "\t+echo. >> \$@" +writeln "\t+echo fossil >> \$@" + +writeln { +translate$E: $(SRCDIR_tools)\translate.c + $(BCC) -o$@ $** + +makeheaders$E: $(SRCDIR_tools)\makeheaders.c + $(BCC) -o$@ $** + +mkindex$E: $(SRCDIR_tools)\mkindex.c + $(BCC) -o$@ $** + +mkbuiltin$E: $(SRCDIR_tools)\mkbuiltin.c + $(BCC) -o$@ $** + +mkversion$E: $(SRCDIR_tools)\mkversion.c + $(BCC) -o$@ $** + +codecheck1$E: $(SRCDIR_tools)\codecheck1.c + $(BCC) -o$@ $** + +$(OBJDIR)\shell$O : $(SRCDIR_extsrc)\shell.c + $(TCC) -o$@ -c $(SHELL_OPTIONS) $(SQLITE_OPTIONS) $(SHELL_CFLAGS) $** + +$(OBJDIR)\sqlite3$O : $(SRCDIR_extsrc)\sqlite3.c + $(TCC) -o$@ -c $(SQLITE_OPTIONS) $(SQLITE_CFLAGS) $** + +$(OBJDIR)\th$O : $(SRCDIR)\th.c + $(TCC) -o$@ -c $** + +$(OBJDIR)\th_lang$O : $(SRCDIR)\th_lang.c + $(TCC) -o$@ -c $** + +$(OBJDIR)\cson_amalgamation.h : $(SRCDIR_extsrc)\cson_amalgamation.h + cp $@ $@ + +VERSION.h : mkversion$E $B\manifest.uuid $B\manifest $B\VERSION + +$** > $@ + +page_index.h: mkindex$E $(SRC) + +$** > $@ + +builtin_data.h: mkbuiltin$E $(EXTRA_FILES) + mkbuiltin$E --prefix $(SRCDIR)/ $(EXTRA_FILES) > $@ + +clean: + -del $(OBJDIR)\*.obj + -del *.obj *_.c *.h *.map + +realclean: + -del $(APPNAME) translate$E mkindex$E makeheaders$E mkversion$E codecheck1$E mkbuiltin$E + +$(OBJDIR)\json$O : $(SRCDIR)\json_detail.h +$(OBJDIR)\json_artifact$O : $(SRCDIR)\json_detail.h +$(OBJDIR)\json_branch$O : $(SRCDIR)\json_detail.h +$(OBJDIR)\json_config$O : $(SRCDIR)\json_detail.h +$(OBJDIR)\json_diff$O : $(SRCDIR)\json_detail.h +$(OBJDIR)\json_dir$O : $(SRCDIR)\json_detail.h +$(OBJDIR)\json_finfo$O : $(SRCDIR)\json_detail.h +$(OBJDIR)\json_login$O : $(SRCDIR)\json_detail.h +$(OBJDIR)\json_query$O : $(SRCDIR)\json_detail.h +$(OBJDIR)\json_report$O : $(SRCDIR)\json_detail.h +$(OBJDIR)\json_status$O : $(SRCDIR)\json_detail.h +$(OBJDIR)\json_tag$O : $(SRCDIR)\json_detail.h +$(OBJDIR)\json_timeline$O : $(SRCDIR)\json_detail.h +$(OBJDIR)\json_user$O : $(SRCDIR)\json_detail.h +$(OBJDIR)\json_wiki$O : $(SRCDIR)\json_detail.h + +} +foreach s [lsort $src] { + writeln "\$(OBJDIR)\\$s\$O : ${s}_.c ${s}.h" + writeln "\t\$(TCC) -o\$@ -c ${s}_.c\n" + writeln "${s}_.c : \$(SRCDIR)\\$s.c" + writeln "\t+translate\$E \$** > \$@\n" +} + +writeln -nonewline "headers: makeheaders\$E page_index.h builtin_data.h VERSION.h\n\t +makeheaders\$E " +foreach s [lsort $src] { + writeln -nonewline "${s}_.c:$s.h " +} +foreach s [lsort $src_ext] { + writeln -nonewline "\$(SRCDIR_extsrc)\\${s}.c:$s.h " +} +writeln "\$(SRCDIR_extsrc)\\sqlite3.h \$(SRCDIR)\\th.h VERSION.h \$(SRCDIR_extsrc)\\cson_amalgamation.h" +writeln "\t@copy /Y nul: headers" + +close $output_file +# +# End of the win/Makefile.dmc output +############################################################################## +############################################################################## +############################################################################## +# Begin win/Makefile.msc output +# +puts "building ../win/Makefile.msc" +set output_file [open ../win/Makefile.msc w] +fconfigure $output_file -translation binary + +writeln {# +############################################################################## +# WARNING: DO NOT EDIT, AUTOMATICALLY GENERATED FILE (SEE "tools/makemake.tcl") +############################################################################## +# +# +# This file is automatically generated. Instead of editing this +# file, edit "makemake.tcl" then run "tclsh makemake.tcl" +# to regenerate this file. +# +B = .. +SRCDIR = $(B)\src +SRCDIR_extsrc = $(B)\extsrc +SRCDIR_tools = $(B)\tools +T = . +OBJDIR = $(T) +OX = $(OBJDIR) +O = .obj +E = .exe +P = .pdb +DBGOPTS = /Od + +INSTALLDIR = . +!ifdef DESTDIR +INSTALLDIR = $(DESTDIR)\$(INSTALLDIR) +!endif + +# When building out of source, this Makefile needs to know the path to the base +# top-level directory for this project. Pass it on NMAKE command line via make +# variable B: +# NMAKE /f "path\to\this\Makefile" B="path/to/fossil/root" +# +# NOTE: Make sure B path has no trailing backslash, UNIX-style path is OK too. +# +!if !exist("$(B)\.fossil-settings") +!error Please specify path to project base directory: B="path/to/fossil" +!endif + +# Perl is only necessary if OpenSSL support is enabled and it is built from +# source code. The PERLDIR environment variable, if it exists, should point +# to the directory containing the main Perl executable specified here (i.e. +# "perl.exe"). +PERL = perl.exe + +# Enable use of available compiler optimizations? +!ifndef OPTIMIZATIONS +OPTIMIZATIONS = 2 +!endif + +# Enable debugging symbols? +!ifndef DEBUG +DEBUG = 0 +!endif +!ifdef FOSSIL_DEBUG +DEBUG = 1 +!endif + +# Build the OpenSSL libraries? +!ifndef FOSSIL_BUILD_SSL +FOSSIL_BUILD_SSL = 0 +!endif + +# Build the included zlib library? +!ifndef FOSSIL_BUILD_ZLIB +FOSSIL_BUILD_ZLIB = 1 +!endif + +# Link everything except SQLite dynamically? +!ifndef FOSSIL_DYNAMIC_BUILD +FOSSIL_DYNAMIC_BUILD = 0 +!endif + +# Enable relative paths in external diff/gdiff? +!ifndef FOSSIL_ENABLE_EXEC_REL_PATHS +FOSSIL_ENABLE_EXEC_REL_PATHS = 0 +!endif + +# Enable the JSON API? +!ifndef FOSSIL_ENABLE_JSON +FOSSIL_ENABLE_JSON = 0 +!endif + +# Enable OpenSSL support? +!ifndef FOSSIL_ENABLE_SSL +FOSSIL_ENABLE_SSL = 0 +!endif + +# Enable the Tcl integration subsystem? +!ifndef FOSSIL_ENABLE_TCL +FOSSIL_ENABLE_TCL = 0 +!endif + +# Enable TH1 scripts in embedded documentation files? +!ifndef FOSSIL_ENABLE_TH1_DOCS +FOSSIL_ENABLE_TH1_DOCS = 0 +!endif + +# Enable TH1 hooks for commands and web pages? +!ifndef FOSSIL_ENABLE_TH1_HOOKS +FOSSIL_ENABLE_TH1_HOOKS = 0 +!endif + +# Enable support for Windows XP with Visual Studio 201x? +!ifndef FOSSIL_ENABLE_WINXP +FOSSIL_ENABLE_WINXP = 0 +!endif + +# Enable support for the SQLite Encryption Extension? +!ifndef USE_SEE +USE_SEE = 0 +!endif + +!if $(FOSSIL_ENABLE_SSL)!=0 +SSLDIR = $(B)\compat\openssl +SSLINCDIR = $(SSLDIR)\include +!if $(FOSSIL_DYNAMIC_BUILD)!=0 +SSLLIBDIR = $(SSLDIR) +!else +SSLLIBDIR = $(SSLDIR) +!endif +SSLLIB = libssl.lib libcrypto.lib user32.lib gdi32.lib crypt32.lib +!if "$(PLATFORM)"=="amd64" || "$(PLATFORM)"=="x64" +!message Using 'x64' platform for OpenSSL... +SSLCONFIG = VC-WIN64A no-asm no-ssl3 no-weak-ssl-ciphers +!if $(FOSSIL_DYNAMIC_BUILD)!=0 +SSLCONFIG = $(SSLCONFIG) shared +!else +SSLCONFIG = $(SSLCONFIG) no-shared +!endif +!elseif "$(PLATFORM)"=="ia64" +!message Using 'ia64' platform for OpenSSL... +SSLCONFIG = VC-WIN64I no-asm no-ssl3 no-weak-ssl-ciphers +!if $(FOSSIL_DYNAMIC_BUILD)!=0 +SSLCONFIG = $(SSLCONFIG) shared +!else +SSLCONFIG = $(SSLCONFIG) no-shared +!endif +!else +!message Assuming 'x86' platform for OpenSSL... +SSLCONFIG = VC-WIN32 no-asm no-ssl3 no-weak-ssl-ciphers +!if $(FOSSIL_DYNAMIC_BUILD)!=0 +SSLCONFIG = $(SSLCONFIG) shared +!else +SSLCONFIG = $(SSLCONFIG) no-shared +!endif +!endif +!endif + +!if $(FOSSIL_ENABLE_TCL)!=0 +TCLDIR = $(B)\compat\tcl-8.6 +TCLSRCDIR = $(TCLDIR) +TCLINCDIR = $(TCLSRCDIR)\generic +!endif + +# zlib options +ZINCDIR = $(B)\compat\zlib +ZLIBDIR = $(B)\compat\zlib + +!if $(FOSSIL_DYNAMIC_BUILD)!=0 +ZLIB = zdll.lib +!else +ZLIB = zlib.lib +!endif + +INCL = /I. /I"$(OX)" /I"$(SRCDIR)" /I"$(SRCDIR_extsrc)" /I"$(B)\win\include" + +INCL = $(INCL) /I"$(ZINCDIR)" + +!if $(FOSSIL_ENABLE_SSL)!=0 +INCL = $(INCL) /I"$(SSLINCDIR)" +!endif + +!if $(FOSSIL_ENABLE_TCL)!=0 +INCL = $(INCL) /I"$(TCLINCDIR)" +!endif + +CFLAGS = /nologo /W2 /WX /utf-8 +LDFLAGS = + +CFLAGS = $(CFLAGS) /D_CRT_SECURE_NO_DEPRECATE /D_CRT_SECURE_NO_WARNINGS +CFLAGS = $(CFLAGS) /D_CRT_NONSTDC_NO_DEPRECATE /D_CRT_NONSTDC_NO_WARNINGS + +!if $(FOSSIL_DYNAMIC_BUILD)!=0 +LDFLAGS = $(LDFLAGS) /MANIFEST +!else +LDFLAGS = $(LDFLAGS) /NODEFAULTLIB:msvcrt /MANIFEST:NO +!endif + +!if $(FOSSIL_ENABLE_WINXP)!=0 +XPCFLAGS = $(XPCFLAGS) /D_WIN32_WINNT=0x0501 /D_USING_V110_SDK71_=1 +CFLAGS = $(CFLAGS) $(XPCFLAGS) +# +# NOTE: For regular builds, /OSVERSION defaults to the /SUBSYSTEM version and +# explicit initialization is redundant, but is required for post-built edits. +# +!if "$(PLATFORM)"=="amd64" || "$(PLATFORM)"=="x64" +XPLDFLAGS = $(XPLDFLAGS) /OSVERSION:5.02 /SUBSYSTEM:CONSOLE,5.02 +!else +XPLDFLAGS = $(XPLDFLAGS) /OSVERSION:5.01 /SUBSYSTEM:CONSOLE,5.01 +!endif +LDFLAGS = $(LDFLAGS) $(XPLDFLAGS) +# +# NOTE: Only XPCFLAGS is forwarded to the OpenSSL configuration, and XPLDFLAGS +# is applied in a separate post-build step, see below for more information. +# +!if $(FOSSIL_ENABLE_SSL)!=0 +SSLCONFIG = $(SSLCONFIG) $(XPCFLAGS) +!endif +!endif + +!if $(FOSSIL_DYNAMIC_BUILD)!=0 +!if $(DEBUG)!=0 +CRTFLAGS = /MDd +!else +CRTFLAGS = /MD +!endif +!else +!if $(DEBUG)!=0 +CRTFLAGS = /MTd +!else +CRTFLAGS = /MT +!endif +!endif + +!if $(OPTIMIZATIONS)>3 +RELOPTS = /Os +!elseif $(OPTIMIZATIONS)>2 +RELOPTS = /Ox +!elseif $(OPTIMIZATIONS)>1 +RELOPTS = /O2 +!elseif $(OPTIMIZATIONS)>0 +RELOPTS = /O1 +!else +RELOPTS = +!endif + +!if $(DEBUG)!=0 +CFLAGS = $(CFLAGS) /Zi $(CRTFLAGS) $(DBGOPTS) /DFOSSIL_DEBUG /DTH_MEMDEBUG +LDFLAGS = $(LDFLAGS) /DEBUG +!else +CFLAGS = $(CFLAGS) $(CRTFLAGS) $(RELOPTS) +!endif + +BCC = $(CC) $(CFLAGS) +TCC = $(CC) /c $(CFLAGS) $(MSCDEF) $(INCL) +RCC = $(RC) /D_WIN32 /D_MSC_VER $(MSCDEF) $(INCL) +MTC = mt +LIBS = ws2_32.lib advapi32.lib dnsapi.lib +LIBDIR = + +!if $(FOSSIL_DYNAMIC_BUILD)!=0 +TCC = $(TCC) /DFOSSIL_DYNAMIC_BUILD=1 +RCC = $(RCC) /DFOSSIL_DYNAMIC_BUILD=1 +!endif + +LIBS = $(LIBS) $(ZLIB) +LIBDIR = $(LIBDIR) /LIBPATH:"$(ZLIBDIR)" + +!if $(FOSSIL_ENABLE_JSON)!=0 +TCC = $(TCC) /DFOSSIL_ENABLE_JSON=1 +RCC = $(RCC) /DFOSSIL_ENABLE_JSON=1 +!endif + +!if $(FOSSIL_ENABLE_SSL)!=0 +TCC = $(TCC) /DFOSSIL_ENABLE_SSL=1 +RCC = $(RCC) /DFOSSIL_ENABLE_SSL=1 +LIBS = $(LIBS) $(SSLLIB) +LIBDIR = $(LIBDIR) /LIBPATH:"$(SSLLIBDIR)" +!endif + +!if $(FOSSIL_ENABLE_EXEC_REL_PATHS)!=0 +TCC = $(TCC) /DFOSSIL_ENABLE_EXEC_REL_PATHS=1 +RCC = $(RCC) /DFOSSIL_ENABLE_EXEC_REL_PATHS=1 +!endif + +!if $(FOSSIL_ENABLE_TH1_DOCS)!=0 +TCC = $(TCC) /DFOSSIL_ENABLE_TH1_DOCS=1 +RCC = $(RCC) /DFOSSIL_ENABLE_TH1_DOCS=1 +!endif + +!if $(FOSSIL_ENABLE_TH1_HOOKS)!=0 +TCC = $(TCC) /DFOSSIL_ENABLE_TH1_HOOKS=1 +RCC = $(RCC) /DFOSSIL_ENABLE_TH1_HOOKS=1 +!endif + +!if $(FOSSIL_ENABLE_TCL)!=0 +TCC = $(TCC) /DFOSSIL_ENABLE_TCL=1 +RCC = $(RCC) /DFOSSIL_ENABLE_TCL=1 +TCC = $(TCC) /DFOSSIL_ENABLE_TCL_STUBS=1 +RCC = $(RCC) /DFOSSIL_ENABLE_TCL_STUBS=1 +TCC = $(TCC) /DFOSSIL_ENABLE_TCL_PRIVATE_STUBS=1 +RCC = $(RCC) /DFOSSIL_ENABLE_TCL_PRIVATE_STUBS=1 +TCC = $(TCC) /DUSE_TCL_STUBS=1 +RCC = $(RCC) /DUSE_TCL_STUBS=1 +!endif + +!if $(USE_SEE)!=0 +TCC = $(TCC) /DUSE_SEE=1 +RCC = $(RCC) /DUSE_SEE=1 +!endif +} +regsub -all {[-]D} [join $SQLITE_WIN32_OPTIONS { }] {/D} MSC_SQLITE_OPTIONS +set j " \\\n " +writeln "SQLITE_OPTIONS = [join $MSC_SQLITE_OPTIONS $j]\n" + +set j " \\\n " +writeln [string map {-D /D} \ + "SHELL_OPTIONS = [join $SHELL_WIN32_OPTIONS $j]"]\n + +regsub -all {[-]D} [join $PIKCHR_OPTIONS { }] {/D} MSC_PIKCHR_OPTIONS +set j " \\\n " +writeln "PIKCHR_OPTIONS = [join $MSC_PIKCHR_OPTIONS $j]\n" + +writeln -nonewline "SRC = " +set i 0 +foreach s [lsort $src] { + if {$i > 0} { + writeln " \\" + writeln -nonewline " " + } + writeln -nonewline "\"\$(OX)\\${s}_.c\""; incr i +} +foreach s [lsort $src_ext] { + writeln " \\" + writeln -nonewline " " + writeln -nonewline "\"\$(SRCDIR_extsrc)\\${s}.c\""; incr i +} +writeln "\n" +writeln -nonewline "EXTRA_FILES = " +set i 0 +foreach s [lsort $extra_files] { + if {$i > 0} { + writeln " \\" + writeln -nonewline " " + } + set s [regsub -all / $s \\] + writeln -nonewline "\"\$(SRCDIR)\\${s}\""; incr i +} +writeln "\n" +set AdditionalObj [list shell sqlite3 th th_lang th_tcl cson_amalgamation pikchr] +writeln -nonewline "OBJ = " +set i 0 +foreach s [lsort [concat $src $AdditionalObj]] { + if {$i > 0} { + writeln " \\" + writeln -nonewline " " + } + writeln -nonewline "\"\$(OX)\\$s\$O\""; incr i +} +if {$i > 0} { + writeln " \\" +} +writeln -nonewline " \"\$(OX)\\fossil.res\"\n\n" +writeln [string map [list <<<NEXT_LINE>>> \\] { +!ifndef BASEAPPNAME +BASEAPPNAME = fossil +!endif + +APPNAME = $(OX)\$(BASEAPPNAME)$(E) +PDBNAME = $(OX)\$(BASEAPPNAME)$(P) +APPTARGETS = + +all: "$(OX)" "$(APPNAME)" + +$(BASEAPPNAME): "$(APPNAME)" + +$(BASEAPPNAME)$(E): "$(APPNAME)" + +install: "$(APPNAME)" + echo F | xcopy /Y "$(APPNAME)" "$(INSTALLDIR)"\* +!if $(DEBUG)!=0 + echo F | xcopy /Y "$(PDBNAME)" "$(INSTALLDIR)"\* +!endif + +$(OX): + @-mkdir $@ + +zlib: + @echo Building zlib from "$(ZLIBDIR)"... +!if $(FOSSIL_ENABLE_WINXP)!=0 + @pushd "$(ZLIBDIR)" && $(MAKE) /f win32\Makefile.msc $(ZLIB) "CC=cl $(XPCFLAGS)" "LD=link $(XPLDFLAGS)" && popd +!else + @pushd "$(ZLIBDIR)" && $(MAKE) /f win32\Makefile.msc $(ZLIB) && popd +!endif + +clean-zlib: + @pushd "$(ZLIBDIR)" && $(MAKE) /f win32\Makefile.msc clean && popd + +!if $(FOSSIL_ENABLE_SSL)!=0 +openssl: + @echo Building OpenSSL from "$(SSLDIR)"... +!if $(FOSSIL_ENABLE_WINXP)!=0 + @echo Passing XPCFLAGS = [ $(XPCFLAGS) ] to the OpenSSL configuration... +!endif +!ifdef PERLDIR + @pushd "$(SSLDIR)" && "$(PERLDIR)\$(PERL)" Configure $(SSLCONFIG) && popd +!else + @pushd "$(SSLDIR)" && "$(PERL)" Configure $(SSLCONFIG) && popd +!endif + @pushd "$(SSLDIR)" && $(MAKE) && popd +!if $(FOSSIL_ENABLE_WINXP)!=0 && $(FOSSIL_DYNAMIC_BUILD)!=0 +# +# NOTE: Appending custom linker flags to the OpenSSL default linker flags is +# somewhat difficult, as summarized in this Fossil Forum post: +# +# https://fossil-scm.org/forum/forumpost/a9a2d6af28b +# +# Therefore the custom linker flags required for Windows XP dynamic builds are +# applied in a separate post-build step. +# +# If the build stops here, or if the custom linker flags are outside the scope +# of `editbin` or `link /EDIT` (i.e. additional libraries), consider tweaking +# the OpenSSL makefile by hand. +# +# Also note that this step changes the subsystem for the OpenSSL DLLs from +# WINDOWS to CONSOLE, but which has no effect on DLLs. +# + @echo Applying XPLDFLAGS = [ $(XPLDFLAGS) ] to the OpenSSL DLLs... + @for /F "usebackq delims=" %F in (`dir /A:-D/B "$(SSLDIR)\*.dll" 2^>nul`) <<<NEXT_LINE>>> + do @( <<<NEXT_LINE>>> + echo %F & <<<NEXT_LINE>>> + link /EDIT /NOLOGO $(XPLDFLAGS) "$(SSLDIR)\%F" || exit 1 <<<NEXT_LINE>>> + ) +!endif + +clean-openssl: + @pushd "$(SSLDIR)" && $(MAKE) clean && popd +!endif + +!if $(FOSSIL_BUILD_ZLIB)!=0 +APPTARGETS = $(APPTARGETS) zlib +!endif + +!if $(FOSSIL_ENABLE_SSL)!=0 +!if $(FOSSIL_BUILD_SSL)!=0 +APPTARGETS = $(APPTARGETS) openssl +!endif +!endif + +"$(APPNAME)" : $(APPTARGETS) "$(OBJDIR)\translate$E" "$(OBJDIR)\mkindex$E" "$(OBJDIR)\codecheck1$E" "$(OX)\headers" $(OBJ) "$(OX)\linkopts" + "$(OBJDIR)\codecheck1$E" $(SRC) + link $(LDFLAGS) /OUT:$@ /PDB:$(@D)\ $(LIBDIR) Wsetargv.obj "$(OX)\fossil.res" @"$(OX)\linkopts" + if exist "$(B)\win\fossil.exe.manifest" <<<NEXT_LINE>>> + $(MTC) -nologo -manifest "$(B)\win\fossil.exe.manifest" -outputresource:$@;1 + +"$(OX)\linkopts": "$(B)\win\Makefile.msc"}] +set redir {>} +foreach s [lsort [concat $src $AdditionalObj]] { + writeln "\techo \"\$(OX)\\$s.obj\" $redir \$@" + set redir {>>} +} +set redir {>>} +writeln "\techo \$(LIBS) $redir \$@" +writeln { +"$(OBJDIR)\translate$E": "$(SRCDIR_tools)\translate.c" + $(BCC) /Fe$@ /Fo$(@D)\ /Fd$(@D)\ $** + +"$(OBJDIR)\makeheaders$E": "$(SRCDIR_tools)\makeheaders.c" + $(BCC) /Fe$@ /Fo$(@D)\ /Fd$(@D)\ $** + +"$(OBJDIR)\mkindex$E": "$(SRCDIR_tools)\mkindex.c" + $(BCC) /Fe$@ /Fo$(@D)\ /Fd$(@D)\ $** + +"$(OBJDIR)\mkbuiltin$E": "$(SRCDIR_tools)\mkbuiltin.c" + $(BCC) /Fe$@ /Fo$(@D)\ /Fd$(@D)\ $** + +"$(OBJDIR)\mkversion$E": "$(SRCDIR_tools)\mkversion.c" + $(BCC) /Fe$@ /Fo$(@D)\ /Fd$(@D)\ $** + +"$(OBJDIR)\codecheck1$E": "$(SRCDIR_tools)\codecheck1.c" + $(BCC) /Fe$@ /Fo$(@D)\ /Fd$(@D)\ $** + +!if $(USE_SEE)!=0 +SEE_FLAGS = /DSQLITE_HAS_CODEC=1 /DSQLITE_SHELL_DBKEY_PROC=fossil_key +SQLITE3_SHELL_SRC = $(SRCDIR)\shell-see.c +SQLITE3_SRC = $(SRCDIR_extsrc)\sqlite3-see.c +!else +SEE_FLAGS = +SQLITE3_SHELL_SRC = $(SRCDIR_extsrc)\shell.c +SQLITE3_SRC = $(SRCDIR_extsrc)\sqlite3.c +!endif + +"$(OX)\shell$O" : "$(SQLITE3_SHELL_SRC)" "$(B)\win\Makefile.msc" + $(TCC) /Fo$@ /Fd$(@D)\ $(SHELL_OPTIONS) $(SQLITE_OPTIONS) $(SHELL_CFLAGS) $(SEE_FLAGS) -c "$(SQLITE3_SHELL_SRC)" + +"$(OX)\sqlite3$O" : "$(SQLITE3_SRC)" "$(B)\win\Makefile.msc" + $(TCC) /Fo$@ /Fd$(@D)\ -c $(SQLITE_OPTIONS) $(SQLITE_CFLAGS) $(SEE_FLAGS) "$(SQLITE3_SRC)" + +"$(OX)\th$O" : "$(SRCDIR)\th.c" + $(TCC) /Fo$@ /Fd$(@D)\ -c $** + +"$(OX)\th_lang$O" : "$(SRCDIR)\th_lang.c" + $(TCC) /Fo$@ /Fd$(@D)\ -c $** + +"$(OX)\th_tcl$O" : "$(SRCDIR)\th_tcl.c" + $(TCC) /Fo$@ /Fd$(@D)\ -c $** + +"$(OX)\pikchr$O" : "$(SRCDIR_extsrc)\pikchr.c" + $(TCC) $(PIKCHR_OPTIONS) /Fo$@ /Fd$(@D)\ -c $** + +"$(OX)\VERSION.h" : "$(OBJDIR)\mkversion$E" "$(B)\manifest.uuid" "$(B)\manifest" "$(B)\VERSION" "$(B)\phony.h" + "$(OBJDIR)\mkversion$E" "$(B)\manifest.uuid" "$(B)\manifest" "$(B)\VERSION" > $@ + +"$(B)\phony.h" : + rem Force rebuild of VERSION.h whenever nmake is run + +"$(OX)\cson_amalgamation$O" : "$(SRCDIR_extsrc)\cson_amalgamation.c" + $(TCC) /Fo$@ /Fd$(@D)\ -c $** + +"$(OX)\page_index.h": "$(OBJDIR)\mkindex$E" $(SRC) + $** > $@ + +"$(OX)\builtin_data.h": "$(OBJDIR)\mkbuiltin$E" "$(OX)\builtin_data.reslist" + "$(OBJDIR)\mkbuiltin$E" --prefix "$(SRCDIR)/" --reslist "$(OX)\builtin_data.reslist" > $@ + +cleanx: + -del "$(OX)\*.obj" 2>NUL + -del "$(OBJDIR)\*.obj" 2>NUL + -del "$(OX)\*_.c" 2>NUL + -del "$(OX)\*.h" 2>NUL + -del "$(OX)\*.ilk" 2>NUL + -del "$(OX)\*.map" 2>NUL + -del "$(OX)\*.res" 2>NUL + -del "$(OX)\*.reslist" 2>NUL + -del "$(OX)\headers" 2>NUL + -del "$(OX)\linkopts" 2>NUL + -del "$(OX)\vc*.pdb" 2>NUL + +clean: cleanx + -del "$(APPNAME)" 2>NUL + -del "$(PDBNAME)" 2>NUL + -del "$(OBJDIR)\translate$E" 2>NUL + -del "$(OBJDIR)\translate$P" 2>NUL + -del "$(OBJDIR)\mkindex$E" 2>NUL + -del "$(OBJDIR)\mkindex$P" 2>NUL + -del "$(OBJDIR)\makeheaders$E" 2>NUL + -del "$(OBJDIR)\makeheaders$P" 2>NUL + -del "$(OBJDIR)\mkversion$E" 2>NUL + -del "$(OBJDIR)\mkversion$P" 2>NUL + -del "$(OBJDIR)\mkcss$E" 2>NUL + -del "$(OBJDIR)\mkcss$P" 2>NUL + -del "$(OBJDIR)\codecheck1$E" 2>NUL + -del "$(OBJDIR)\codecheck1$P" 2>NUL + -del "$(OBJDIR)\mkbuiltin$E" 2>NUL + -del "$(OBJDIR)\mkbuiltin$P" 2>NUL + +realclean: clean + +"$(OBJDIR)\json$O" : "$(SRCDIR)\json_detail.h" +"$(OBJDIR)\json_artifact$O" : "$(SRCDIR)\json_detail.h" +"$(OBJDIR)\json_branch$O" : "$(SRCDIR)\json_detail.h" +"$(OBJDIR)\json_config$O" : "$(SRCDIR)\json_detail.h" +"$(OBJDIR)\json_diff$O" : "$(SRCDIR)\json_detail.h" +"$(OBJDIR)\json_dir$O" : "$(SRCDIR)\json_detail.h" +"$(OBJDIR)\json_finfo$O" : "$(SRCDIR)\json_detail.h" +"$(OBJDIR)\json_login$O" : "$(SRCDIR)\json_detail.h" +"$(OBJDIR)\json_query$O" : "$(SRCDIR)\json_detail.h" +"$(OBJDIR)\json_report$O" : "$(SRCDIR)\json_detail.h" +"$(OBJDIR)\json_status$O" : "$(SRCDIR)\json_detail.h" +"$(OBJDIR)\json_tag$O" : "$(SRCDIR)\json_detail.h" +"$(OBJDIR)\json_timeline$O" : "$(SRCDIR)\json_detail.h" +"$(OBJDIR)\json_user$O" : "$(SRCDIR)\json_detail.h" +"$(OBJDIR)\json_wiki$O" : "$(SRCDIR)\json_detail.h" +} + +writeln {"$(OX)\builtin_data.reslist": $(EXTRA_FILES) "$(B)\win\Makefile.msc"} +set redir {>} +foreach s [lsort $extra_files] { + writeln "\techo \"\$(SRCDIR)\\${s}\" $redir \$@" + set redir {>>} +} + +foreach s [lsort $src] { + set extra_h($s) {} +} +set extra_h(builtin) " \"\$(OX)\\builtin_data.h\"" +set extra_h(dispatch) " \"\$(OX)\\page_index.h\"" + +writeln "" +foreach s [lsort $src] { + writeln "\"\$(OX)\\$s\$O\" : \"\$(OX)\\${s}_.c\" \"\$(OX)\\${s}.h\"$extra_h($s)" + writeln "\t\$(TCC) /Fo\$@ /Fd\$(@D)\\ -c \"\$(OX)\\${s}_.c\"\n" + writeln "\"\$(OX)\\${s}_.c\" : \"\$(SRCDIR)\\$s.c\"" + writeln "\t\"\$(OBJDIR)\\translate\$E\" \$** > \$@\n" +} + +writeln "\"\$(OX)\\fossil.res\" : \"\$(B)\\win\\fossil.rc\"" +writeln "\t\$(RCC) /fo \$@ \$**\n" + +writeln "\"\$(OX)\\headers\": \"\$(OBJDIR)\\makeheaders\$E\" \"\$(OX)\\page_index.h\" \"\$(OX)\\builtin_data.h\" \"\$(OX)\\VERSION.h\"" +writeln -nonewline "\t\"\$(OBJDIR)\\makeheaders\$E\" " +set i 0 +foreach s [lsort $src] { + if {$i > 0} { + writeln " \\" + writeln -nonewline "\t\t\t" + } + writeln -nonewline "\"\$(OX)\\${s}_.c\":\"\$(OX)\\$s.h\""; incr i +} +foreach s [lsort $src_ext] { + writeln " \\" + writeln -nonewline "\t\t\t" + writeln -nonewline "\"\$(SRCDIR_extsrc)\\${s}.c\":\"\$(OX)\\$s.h\""; incr i +} +writeln " \\\n\t\t\t\"\$(SRCDIR_extsrc)\\sqlite3.h\" \\" +writeln "\t\t\t\"\$(SRCDIR)\\th.h\" \\" +writeln "\t\t\t\"\$(OX)\\VERSION.h\" \\" +writeln "\t\t\t\"\$(SRCDIR_extsrc)\\cson_amalgamation.h\"" +writeln "\t@copy /Y nul: $@" + + +close $output_file +# +# End of the win/Makefile.msc output +############################################################################## +############################################################################## diff --git a/tools/man_page_command_list.tcl b/tools/man_page_command_list.tcl new file mode 100755 index 0000000..3c73506 --- /dev/null +++ b/tools/man_page_command_list.tcl @@ -0,0 +1,68 @@ +#!/usr/bin/env tclsh +# man_page_command_list.tcl - generates common command list for fossil.1 + +# Tunable configuration. +set columns 5 +set width 15 + +# The only supported command-line argument is the optional output filename. +if {[llength $argv] == 1} { + set file [lindex $argv 0] +} + +# Get list of common commands. +set commands [exec fossil help] +regsub -nocase {.*?\nfrequently used commands:.*\n} $commands {} commands +regsub -nocase {\nthis is fossil version.*} $commands {} commands +regsub -all {\s+} $commands " " commands +set commands [lsort $commands] + +# Compute number of rows. +set rows [expr {([llength $commands] + $columns - 1) / $columns}] + +# Generate text one line at a time. +set text {} +for {set row 0} {$row < $rows} {incr row} { + # Separate rows with line break. + if {$row} { + append text .br\n + } + + # Generate the row of commands. + for {set col 0} {$col < $columns} {incr col} { + set i [expr {$col * $rows + $row}] + if {$i < [llength $commands]} { + append text [format %-*s $width [lindex $commands $i]] + } + } + append text \n +} + +# Strip trailing whitespace from each line. +regsub -all {\s+\n} $text \n text + +# Output text. +if {[info exists file]} { + # If a filename was specified, read the file for use as a template. + set chan [open $file] + set data [read $chan] + close $chan + + # Locate the part of the file to replace. + if {[regexp -indices {\n\.SH Common COMMANDs:\n\n(.*?)\n\.SH} $data\ + _ range]} { + # If found, replace with the updated command list. + set chan [open $file w] + puts -nonewline $chan [string replace $data\ + [lindex $range 0] [lindex $range 1] $text] + close $chan + } else { + # If not found, abort. + error "could not find command list in man file \"$file\"" + } +} else { + # If no filename was specified, write to stdout. + puts $text +} + +# vim: set sts=4 sw=4 tw=80 et ft=tcl: diff --git a/tools/mkbuiltin.c b/tools/mkbuiltin.c new file mode 100644 index 0000000..33ac0a4 --- /dev/null +++ b/tools/mkbuiltin.c @@ -0,0 +1,415 @@ +/* +** Copyright (c) 2014 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 is a stand-alone utility program that is part of the Fossil build +** process. This program reads files named on the command line and converts +** them into ANSI-C static char array variables. Output is written onto +** standard output. +** +** Additionally, the input files may be listed in a separate list file (one +** resource name per line, optionally enclosed in double quotes). Pass the list +** via '--reslist <the-list-file>' option. Both lists, from the command line and +** the list file, are merged; duplicate file names skipped from processing. +** This option is useful to get around the command line length limitations +** under some OS, like Windows. +** +** The makefiles use this utility to package various resources (large scripts, +** GIF images, etc) that are separate files in the source code as byte +** arrays in the resulting executable. +*/ +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <ctype.h> + +/* +** Read the entire content of the file named zFilename into memory obtained +** from malloc() and return a pointer to that memory. Write the size of the +** file into *pnByte. +*/ +static unsigned char *read_file(const char *zFilename, int *pnByte){ + FILE *in; + unsigned char *z; + int nByte; + int got; + in = fopen(zFilename, "rb"); + if( in==0 ){ + return 0; + } + fseek(in, 0, SEEK_END); + *pnByte = nByte = ftell(in); + fseek(in, 0, SEEK_SET); + z = malloc( nByte+1 ); + if( z==0 ){ + fprintf(stderr, "failed to allocate %d bytes\n", nByte+1); + exit(1); + } + got = fread(z, 1, nByte, in); + fclose(in); + z[got] = 0; + return z; +} + +#ifndef FOSSIL_DEBUG +/* +** Try to compress a javascript file by removing unnecessary whitespace. +** +** Warning: This compression routine does not necessarily work for any +** arbitrary Javascript source file. But it should work ok for the +** well-behaved source files in this project. +*/ +static void compressJavascript(unsigned char *z, int *pn){ + int n = *pn; + int i, j, k; + for(i=j=0; i<n; i++){ + unsigned char c = z[i]; + if( c=='/' && (i==0 || z[i-1]!=':')){ + if( z[i+1]=='*' ){ + while( j>0 && (z[j-1]==' ' || z[j-1]=='\t') ){ j--; } + for(k=i+3; k<n && (z[k]!='/' || z[k-1]!='*'); k++){} + i = k; + continue; + }else if( z[i+1]=='/' ){ + while( j>0 && (z[j-1]==' ' || z[j-1]=='\t') ){ j--; } + for(k=i+2; k<n && z[k]!='\n'; k++){} + i = k-1; + continue; + } + } + if( c=='\n' ){ + if( j==0 ) continue; + while( j>0 && isspace(z[j-1]) ) j--; + z[j++] = '\n'; + while( i+1<n && isspace(z[i+1]) ) i++; + continue; + } + z[j++] = c; + } + z[j] = 0; + *pn = j; +} +#endif /* FOSSIL_DEBUG */ + +/* +** There is an instance of the following for each file translated. +*/ +typedef struct Resource Resource; +struct Resource { + char *zName; + int nByte; + int idx; +}; + +typedef struct ResourceList ResourceList; +struct ResourceList { + Resource *aRes; + int nRes; + char *buf; + long bufsize; +}; + + +Resource *read_reslist(char *name, ResourceList *list){ +#define RESLIST_BUF_MAXBYTES (1L<<20) /* 1 MB of text */ + FILE *in; + long filesize = 0L; + long linecount = 0L; + char *p = 0; + char *pb = 0; + + memset(list, 0, sizeof(*list)); + + if( (in = fopen(name, "rb"))==0 ){ + return list->aRes; + } + fseek(in, 0L, SEEK_END); + filesize = ftell(in); + rewind(in); + + if( filesize > RESLIST_BUF_MAXBYTES ){ + fprintf(stderr, "List file [%s] must be smaller than %ld bytes\n", name, + RESLIST_BUF_MAXBYTES); + return list->aRes; + } + list->bufsize = filesize; + list->buf = (char *)calloc((list->bufsize + 2), sizeof(list->buf[0])); + if( list->buf==0 ){ + fprintf(stderr, "failed to allocated %ld bytes\n", list->bufsize + 1); + list->bufsize = 0L; + return list->aRes; + } + filesize = fread(list->buf, sizeof(list->buf[0]),list->bufsize, in); + if ( filesize!=list->bufsize ){ + fprintf(stderr, "failed to read [%s]\n", name); + return list->aRes; + } + fclose(in); + + /* + ** append an extra newline (if missing) for a correct line count + */ + if( list->buf[list->bufsize-1]!='\n' ) list->buf[list->bufsize]='\n'; + + linecount = 0L; + for( p = strchr(list->buf, '\n'); + p && p <= &list->buf[list->bufsize-1]; + p = strchr(++p, '\n') ){ + ++linecount; + } + + list->aRes = (Resource *)calloc(linecount+1, sizeof(list->aRes[0])); + for( pb = list->buf, p = strchr(pb, '\n'); + p && p <= &list->buf[list->bufsize-1]; + pb = ++p, p = strchr(pb, '\n') ){ + + char *path = pb; + char *pe = p - 1; + + /* strip leading and trailing whitespace */ + while( path < p && isspace(*path) ) ++path; + while( pe > path && isspace(*pe) ){ + *pe = '\0'; + --pe; + } + + /* strip outer quotes */ + while( path < p && *path=='\"') ++path; + while( pe > path && *pe=='\"' ){ + *pe = '\0'; + --pe; + } + *p = '\0'; + + /* skip empty path */ + if( *path ){ + list->aRes[list->nRes].zName = path; + ++(list->nRes); + } + } + return list->aRes; +} + +void free_reslist(ResourceList *list){ + if( list ){ + if( list->buf ) free(list->buf); + if( list->aRes) free(list->aRes); + memset(list, 0, sizeof(*list)); + } +} + +/* +** Compare two Resource objects for sorting purposes. They sort +** in zName order so that Fossil can search for resources using +** a binary search. +*/ +typedef int (*QsortCompareFunc)(const void *, const void*); + +static int compareResource(const Resource *a, const Resource *b){ + return strcmp(a->zName, b->zName); +} + +int remove_duplicates(ResourceList *list){ + char dupNameAsc[64] = "\255"; + char dupNameDesc[64] = ""; + Resource dupResAsc; + Resource dupResDesc; + Resource *pDupRes; + int dupcount = 0; + int i; + + if( list->nRes==0 ){ + return list->nRes; + } + + /* + ** scan for duplicates and assign their names to a string that would sort to + ** the bottom, then re-sort and truncate the duplicates + */ + memset(dupNameAsc, dupNameAsc[0], sizeof(dupNameAsc)-2); + memset(dupNameDesc, dupNameDesc[0], sizeof(dupNameDesc)-2); + memset(&dupResAsc, 0, sizeof(dupResAsc)); + dupResAsc.zName = dupNameAsc; + memset(&dupResDesc, 0, sizeof(dupResDesc)); + dupResDesc.zName = dupNameDesc; + pDupRes = (compareResource(&dupResAsc, &dupResDesc) > 0 + ? &dupResAsc : &dupResDesc); + + qsort(list->aRes, list->nRes, sizeof(list->aRes[0]), + (QsortCompareFunc)compareResource); + for( i=0; i<list->nRes-1 ; ++i){ + Resource *res = &list->aRes[i]; + + while( i<list->nRes-1 + && compareResource(res, &list->aRes[i+1])==0 ){ + fprintf(stderr, "Skipped a duplicate file [%s]\n", list->aRes[i+1].zName); + memcpy(&list->aRes[i+1], pDupRes, sizeof(list->aRes[0])); + ++dupcount; + + ++i; + } + } + if( dupcount == 0){ + return list->nRes; + } + qsort(list->aRes, list->nRes, sizeof(list->aRes[0]), + (QsortCompareFunc)compareResource); + list->nRes -= dupcount; + memset(&list->aRes[list->nRes], 0, sizeof(list->aRes[0])); + + return list->nRes; +} + +int main(int argc, char **argv){ + int i, sz; + int j, n; + ResourceList resList; + Resource *aRes; + int nRes; + unsigned char *pData; + int nErr = 0; + int nSkip; + int nPrefix = 0; +#ifndef FOSSIL_DEBUG + int nName; +#endif + + if( argc==1 ){ + fprintf(stderr, "usage\t:%s " + "[--prefix path] [--reslist file] [resource-file1 ...]\n", + argv[0] + ); + return 1; + } + if( argc>3 && strcmp(argv[1],"--prefix")==0 ){ + nPrefix = (int)strlen(argv[2]); + argc -= 2; + argv += 2; + } + + memset(&resList, 0, sizeof(resList)); + if( argc>2 && strcmp(argv[1],"--reslist")==0 ){ + if( read_reslist(argv[2], &resList)==0 ){ + fprintf(stderr, "Failed to load resource list from [%s]", argv[2]); + free_reslist(&resList); + return 1; + } + argc -= 2; + argv += 2; + } + + if( argc>1 ){ + aRes = realloc(resList.aRes, (resList.nRes+argc-1)*sizeof(resList.aRes[0])); + if( aRes==0 || aRes==resList.aRes ){ + fprintf(stderr, "realloc failed\n"); + free_reslist(&resList); + return 1; + } + resList.aRes = aRes; + + for(i=0; i<argc-1; i++){ + resList.aRes[resList.nRes].zName = argv[i+1]; + ++resList.nRes; + } + } + + if( resList.nRes==0 ){ + fprintf(stderr,"No resource files to process\n"); + free_reslist(&resList); + return 1; + } + remove_duplicates(&resList); + + nRes = resList.nRes; + aRes = resList.aRes; + qsort(aRes, nRes, sizeof(aRes[0]), (QsortCompareFunc)compareResource); + + printf("/* Automatically generated code: Do not edit.\n**\n" + "** Rerun the \"mkbuiltin.c\" program or rerun the Fossil\n" + "** makefile to update this source file.\n" + "*/\n"); + for(i=0; i<nRes; i++){ + pData = read_file(aRes[i].zName, &sz); + if( pData==0 ){ + fprintf(stderr, "Cannot open file [%s]\n", aRes[i].zName); + nErr++; + continue; + } + + /* Skip initial lines beginning with # */ + nSkip = 0; + while( pData[nSkip]=='#' ){ + while( pData[nSkip]!=0 && pData[nSkip]!='\n' ){ nSkip++; } + if( pData[nSkip]=='\n' ) nSkip++; + } + +#ifndef FOSSIL_DEBUG + /* Compress javascript source files */ + nName = (int)strlen(aRes[i].zName); + if( (nName>3 && strcmp(&aRes[i].zName[nName-3],".js")==0) + || (nName>7 && strcmp(&aRes[i].zName[nName-7], "/js.txt")==0) + ){ + int x = sz-nSkip; + compressJavascript(pData+nSkip, &x); + sz = x + nSkip; + } +#endif + + aRes[i].nByte = sz - nSkip; + aRes[i].idx = i; + printf("/* Content of file %s */\n", aRes[i].zName); + printf("static const unsigned char bidata%d[%d] = {\n ", + i, sz+1-nSkip); + for(j=nSkip, n=0; j<=sz; j++){ + printf("%3d", pData[j]); + if( j==sz ){ + printf(" };\n"); + }else if( n==14 ){ + printf(",\n "); + n = 0; + }else{ + printf(", "); + n++; + } + } + free(pData); + } + printf("typedef struct BuiltinFileTable BuiltinFileTable;\n"); + printf("struct BuiltinFileTable {\n"); + printf(" const char *zName;\n"); + printf(" const unsigned char *pData;\n"); + printf(" int nByte;\n"); + printf("};\n"); + printf("static const BuiltinFileTable aBuiltinFiles[] = {\n"); + for(i=0; i<nRes; i++){ + char *z = aRes[i].zName; + if( strlen(z)>=nPrefix ) z += nPrefix; + while( z[0]=='.' || z[0]=='/' || z[0]=='\\' ){ z++; } + aRes[i].zName = z; + while( z[0] ){ + if( z[0]=='\\' ) z[0] = '/'; + z++; + } + } + qsort(aRes, nRes, sizeof(aRes[0]), (QsortCompareFunc)compareResource); + for(i=0; i<nRes; i++){ + printf(" { \"%s\", bidata%d, %d },\n", + aRes[i].zName, aRes[i].idx, aRes[i].nByte); + } + printf("};\n"); + free_reslist(&resList); + return nErr; +} diff --git a/tools/mkindex.c b/tools/mkindex.c new file mode 100644 index 0000000..97a2456 --- /dev/null +++ b/tools/mkindex.c @@ -0,0 +1,607 @@ +/* +** Copyright (c) 2002 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 utility program scans Fossil source text looking for specially +** formatted comments and generates C source code for constant tables +** that define the behavior of commands, webpages, and settings. +** +** USAGE: +** +** mkindex *.c >page_index.h +** +** Run this command with arguments that are all input source files to +** scan. Generated C code appears on standard output. The generated +** C code includes structures that: +** +** * Map command names to the C-language functions that implement +** those command. +** +** * Map webpage names to the C-language functions that implement +** those web pages. +** +** * Map settings into attributes, such as they default value for +** each setting, and the kind of value (boolean, multi-line, etc). +** +** * Provide help text for commands, webpages, settings, and other +** miscellanous help topics. +** +** COMMENT TEXT THAT THIS PROGRAM LOOKS FOR: +** +** The source code is scanned for comment lines of the form: +** +** WEBPAGE: /abc/xyz +** COMMAND: cmdname +** SETTING: access-log +** TOPIC: help-topic +** +** The WEBPAGE and COMMAND comments should be followed by a function that +** implements the webpage or command. The form of this function is: +** +** void function_name(void){ +** +** Command names can divided into three classes: 1st-tier, 2nd-tier, +** and test. 1st-tier commands are the most frequently used and the +** ones that show up with "fossil help". 2nd-tier are seldom-used and/or +** legacy commands. Test commands are unsupported commands used for testing +** and analysis only. +** +** Commands are 1st-tier by default. If the command name begins with +** "test-" or if the command name has a "test" argument, then it becomes +** a test command. If the command name has a "2nd-tier" argument or ends +** with a "*" character, it is second tier. If the command name has an "alias" +** argument or ends with a "#" character, it is an alias: another name +** (a one-to-one replacement) for a command. Examples: +** +** COMMAND: abcde* +** COMMAND: fghij 2nd-tier +** COMMAND: mnopq# +** COMMAND: rstuv alias +** COMMAND: test-xyzzy +** COMMAND: xyzzy test +** +** A SETTING: may be followed by arguments that give additional attributes +** to that setting: +** +** SETTING: clean-blob versionable width=40 block-text +** SETTING: auto-shun boolean default=on +** +** New arguments may be added in future releases that set additional +** bits in the eCmdFlags field. +** +** Additional lines of comment after the COMMAND: or WEBPAGE: or SETTING: +** become the built-in help text for that command or webpage or setting. +** Backslashes must be escaped ("\\" in comment yields "\" in the help text.) +** +** Multiple COMMAND: entries can be attached to the same command, thus +** creating multiple aliases for that command. Similarly, multiple +** WEBPAGE: entries can be attached to the same webpage function, to give +** that page aliases. +** +** For SETTING: entries, the default value for the setting can be specified +** using a default=VALUE argument if the default contains no spaces. If the +** default value does contain spaces, use a separate line like this: +** +** SETTING: pgp-command +** DEFAULT: gpg --clearsign -o +** +** If no default is supplied, the default is assumed to be an empty string +** or "off" in the case of a boolean. +** +** A TOPIC: is followed by help text for the named topic. +** +** OUTPUTS: +** +** The output is C-language text to define and initialize a constant +** array of CmdOrPage objects named "aCommand[]". That array is a global +** variable. The dispatch.c source file defines the CmdOrPage object and +** deals with the aCommand[] global variable. +** +** The output also contains a constant array of Setting objects named +** aSetting[]. The Setting object is defined in db.c. +*/ +#include <stdio.h> +#include <stdlib.h> +#include <assert.h> +#include <string.h> + +/*************************************************************************** +** These macros must match similar macros in dispatch.c. +** +** Allowed values for CmdOrPage.eCmdFlags. */ +#define CMDFLAG_1ST_TIER 0x000001 /* Most important commands */ +#define CMDFLAG_2ND_TIER 0x000002 /* Obscure and seldom used commands */ +#define CMDFLAG_TEST 0x000004 /* Commands for testing only */ +#define CMDFLAG_WEBPAGE 0x000008 /* Web pages */ +#define CMDFLAG_COMMAND 0x000010 /* A command */ +#define CMDFLAG_SETTING 0x000020 /* A setting */ +#define CMDFLAG_VERSIONABLE 0x000040 /* A versionable setting */ +#define CMDFLAG_BLOCKTEXT 0x000080 /* Multi-line text setting */ +#define CMDFLAG_BOOLEAN 0x000100 /* A boolean setting */ +#define CMDFLAG_RAWCONTENT 0x000200 /* Do not interpret webpage content */ +#define CMDFLAG_SENSITIVE 0x000400 /* Security-sensitive setting */ +#define CMDFLAG_HIDDEN 0x000800 /* Elide from most listings */ +#define CMDFLAG_LDAVG_EXEMPT 0x001000 /* Exempt from load_control() */ +#define CMDFLAG_ALIAS 0x002000 /* Command aliases */ +#define CMDFLAG_KEEPEMPTY 0x004000 /* Do not unset empty settings */ +#define CMDFLAG_ABBREVSUBCMD 0x008000 /* Abbreviated subcmd in help text */ +#define CMDFLAG_TOPIC 0x010000 /* A help topic */ +/**************************************************************************/ + +/* +** Each entry looks like this: +*/ +typedef struct Entry { + int eType; /* CMDFLAG_* values */ + char *zIf; /* Enclose in #if */ + char *zFunc; /* Name of implementation */ + char *zPath; /* Webpage or command name */ + char *zHelp; /* Help text */ + char *zDflt; /* Default value for settings */ + char *zVar; /* config.name for settings, if different from zPath */ + int iHelp; /* Index of Help text */ + int iWidth; /* Display width for SETTING: values */ +} Entry; + +/* +** Maximum number of entries +*/ +#define N_ENTRY 5000 + +/* +** Maximum size of a help message +*/ +#define MX_HELP 250000 + +/* +** Table of entries +*/ +Entry aEntry[N_ENTRY]; + +/* +** Current help message accumulator +*/ +char zHelp[MX_HELP]; +int nHelp; + +/* +** Most recently encountered #if +*/ +char zIf[2000]; + +/* +** How many entries are used +*/ +int nUsed; +int nFixed; + +/* +** Current filename and line number +*/ +char *zFile; +int nLine; + +/* +** Number of errors +*/ +int nErr = 0; + +/* +** Duplicate N characters of a string. +*/ +char *string_dup(const char *zSrc, int n){ + char *z; + if( n<0 ) n = strlen(zSrc); + z = malloc( n+1 ); + if( z==0 ){ fprintf(stderr,"Out of memory!\n"); exit(1); } + strncpy(z, zSrc, n); + z[n] = 0; + return z; +} + +/* +** Safe isspace macro. Works with signed characters. +*/ +int fossil_isspace(char c){ + return c==' ' || (c<='\r' && c>='\t'); +} + +/* +** Safe isident macro. Works with signed characters. +*/ +int fossil_isident(char c){ + if( c>='a' && c<='z' ) return 1; + if( c>='A' && c<='Z' ) return 1; + if( c>='0' && c<='9' ) return 1; + if( c=='_' ) return 1; + return 0; +} + +/* +** Scan a line looking for comments containing zLabel. Make +** new entries if found. +*/ +void scan_for_label(const char *zLabel, char *zLine, int eType){ + int i, j; + int len = strlen(zLabel); + if( nUsed>=N_ENTRY ) return; + for(i=0; fossil_isspace(zLine[i]) || zLine[i]=='*'; i++){} + if( zLine[i]!=zLabel[0] ) return; + if( strncmp(&zLine[i],zLabel, len)==0 ){ + i += len; + }else{ + return; + } + while( fossil_isspace(zLine[i]) ){ i++; } + if( zLine[i]=='/' ) i++; + for(j=0; zLine[i+j] && !fossil_isspace(zLine[i+j]); j++){} + aEntry[nUsed].eType = eType; + if( eType & CMDFLAG_WEBPAGE ){ + aEntry[nUsed].zPath = string_dup(&zLine[i-1], j+1); + aEntry[nUsed].zPath[0] = '/'; + }else{ + aEntry[nUsed].zPath = string_dup(&zLine[i], j); + } + aEntry[nUsed].zFunc = 0; + if( (eType & CMDFLAG_COMMAND)!=0 ){ + if( strncmp(&zLine[i], "test-", 5)==0 ){ + /* Commands that start with "test-" are test-commands */ + aEntry[nUsed].eType |= CMDFLAG_TEST; + }else if( zLine[i+j-1]=='*' ){ + /* If the command name ends in '*', remove the '*' from the name + ** but move the command into the second tier */ + aEntry[nUsed].zPath[j-1] = 0; + aEntry[nUsed].eType |= CMDFLAG_2ND_TIER; + }else if( zLine[i+j-1]=='#' ){ + /* If the command name ends in '#', remove the '#' from the name + ** but move the command into aliases */ + aEntry[nUsed].zPath[j-1] = 0; + aEntry[nUsed].eType |= CMDFLAG_ALIAS; + }else{ + /* Otherwise, this is a first-tier command */ + aEntry[nUsed].eType |= CMDFLAG_1ST_TIER; + } + } + + /* Process additional flags that might follow the command name */ + while( zLine[i+j]!=0 ){ + i += j; + while( fossil_isspace(zLine[i]) ){ i++; } + if( zLine[i]==0 ) break; + for(j=0; zLine[i+j] && !fossil_isspace(zLine[i+j]); j++){} + if( j==8 && strncmp(&zLine[i], "1st-tier", j)==0 ){ + aEntry[nUsed].eType &= ~(CMDFLAG_2ND_TIER|CMDFLAG_TEST|CMDFLAG_ALIAS); + aEntry[nUsed].eType |= CMDFLAG_1ST_TIER; + }else if( j==8 && strncmp(&zLine[i], "2nd-tier", j)==0 ){ + aEntry[nUsed].eType &= ~(CMDFLAG_1ST_TIER|CMDFLAG_TEST|CMDFLAG_ALIAS); + aEntry[nUsed].eType |= CMDFLAG_2ND_TIER; + }else if( j==4 && strncmp(&zLine[i], "test", j)==0 ){ + aEntry[nUsed].eType &= ~(CMDFLAG_1ST_TIER|CMDFLAG_2ND_TIER|CMDFLAG_ALIAS); + aEntry[nUsed].eType |= CMDFLAG_TEST; + }else if( j==5 && strncmp(&zLine[i], "alias", j)==0 ){ + aEntry[nUsed].eType &= ~(CMDFLAG_1ST_TIER|CMDFLAG_2ND_TIER|CMDFLAG_TEST); + aEntry[nUsed].eType |= CMDFLAG_ALIAS; + }else if( j==11 && strncmp(&zLine[i], "raw-content", j)==0 ){ + aEntry[nUsed].eType |= CMDFLAG_RAWCONTENT; + }else if( j==7 && strncmp(&zLine[i], "boolean", j)==0 ){ + aEntry[nUsed].eType &= ~(CMDFLAG_BLOCKTEXT); + aEntry[nUsed].iWidth = 0; + aEntry[nUsed].eType |= CMDFLAG_BOOLEAN; + }else if( j==10 && strncmp(&zLine[i], "block-text", j)==0 ){ + aEntry[nUsed].eType &= ~(CMDFLAG_BOOLEAN); + aEntry[nUsed].eType |= CMDFLAG_BLOCKTEXT; + }else if( j==10 && strncmp(&zLine[i], "keep-empty", j)==0 ){ + aEntry[nUsed].eType |= CMDFLAG_KEEPEMPTY; + }else if( j==11 && strncmp(&zLine[i], "versionable", j)==0 ){ + aEntry[nUsed].eType |= CMDFLAG_VERSIONABLE; + }else if( j==9 && strncmp(&zLine[i], "sensitive", j)==0 ){ + aEntry[nUsed].eType |= CMDFLAG_SENSITIVE; + }else if( j>6 && strncmp(&zLine[i], "width=", 6)==0 ){ + aEntry[nUsed].iWidth = atoi(&zLine[i+6]); + }else if( j>8 && strncmp(&zLine[i], "default=", 8)==0 ){ + aEntry[nUsed].zDflt = string_dup(&zLine[i+8], j-8); + }else if( j>9 && strncmp(&zLine[i], "variable=", 9)==0 ){ + aEntry[nUsed].zVar = string_dup(&zLine[i+9], j-9); + }else if( j==6 && strncmp(&zLine[i], "hidden", 6)==0 ){ + aEntry[nUsed].eType |= CMDFLAG_HIDDEN; + }else if( j==14 && strncmp(&zLine[i], "loadavg-exempt", 14)==0 ){ + aEntry[nUsed].eType |= CMDFLAG_LDAVG_EXEMPT; + }else if( (j==23 && strncmp(&zLine[i], "abbreviated-subcommands", 23)==0) + || (j==12 && strncmp(&zLine[i], "abbrv-subcom", 12)==0) ){ + aEntry[nUsed].eType |= CMDFLAG_ABBREVSUBCMD; + }else{ + fprintf(stderr, "%s:%d: unknown option: '%.*s'\n", + zFile, nLine, j, &zLine[i]); + nErr++; + } + } + + nUsed++; + return; +} + +/* +** Check to see if the current line is an #if and if it is, add it to +** the zIf[] string. If the current line is an #endif or #else or #elif +** then cancel the current zIf[] string. +*/ +void scan_for_if(const char *zLine){ + int i; + int len; + if( zLine[0]!='#' ) return; + for(i=1; fossil_isspace(zLine[i]); i++){} + if( zLine[i]==0 ) return; + len = strlen(&zLine[i]); + if( strncmp(&zLine[i],"if",2)==0 ){ + zIf[0] = '#'; + memcpy(&zIf[1], &zLine[i], len+1); + }else if( zLine[i]=='e' ){ + zIf[0] = 0; + } +} + +/* +** Check to see if the current line is a "** DEFAULT: ..." line for a +** SETTING definition. If so, remember the default value. +*/ +void scan_for_default(const char *zLine){ + int len; + const char *z; + if( nUsed<1 ) return; + if( (aEntry[nUsed-1].eType & CMDFLAG_SETTING)==0 ) return; + if( strncmp(zLine, "** DEFAULT: ", 12)!=0 ) return; + z = zLine + 12; + while( fossil_isspace(z[0]) ) z++; + len = (int)strlen(z); + while( len>0 && fossil_isspace(z[len-1]) ){ len--; } + aEntry[nUsed-1].zDflt = string_dup(z,len); +} + +/* Local strcpy() clone to squelch an unwarranted warning from OpenBSD. */ +static void local_strcpy(char *dest, const char *src){ + while( (*(dest++) = *(src++))!=0 ){} +} + +/* +** Scan a line for a function that implements a web page or command. +*/ +void scan_for_func(char *zLine){ + int i,j,k; + char *z; + int hasFunc; + if( nUsed<=nFixed ) return; + if( strncmp(zLine, "**", 2)==0 + && fossil_isspace(zLine[2]) + && strlen(zLine)<sizeof(zHelp)-nHelp-1 + && nUsed>nFixed + && strncmp(zLine,"** COMMAND:",11)!=0 + && strncmp(zLine,"** WEBPAGE:",11)!=0 + && strncmp(zLine,"** SETTING:",11)!=0 + && strncmp(zLine,"** DEFAULT:",11)!=0 + && strncmp(zLine,"** TOPIC:",9)!=0 + ){ + if( zLine[2]=='\n' ){ + zHelp[nHelp++] = '\n'; + }else{ + if( strncmp(&zLine[3], "Usage: ", 6)==0 ) nHelp = 0; + local_strcpy(&zHelp[nHelp], &zLine[3]); + nHelp += strlen(&zHelp[nHelp]); + } + return; + } + for(i=0; fossil_isspace(zLine[i]); i++){} + if( zLine[i]==0 ) return; + hasFunc = (aEntry[nFixed].eType & (CMDFLAG_SETTING|CMDFLAG_TOPIC))==0; + if( hasFunc ){ + if( strncmp(&zLine[i],"void",4)!=0 ){ + if( zLine[i]!='*' ) goto page_skip; + return; + } + i += 4; + if( !fossil_isspace(zLine[i]) ) goto page_skip; + while( fossil_isspace(zLine[i]) ){ i++; } + for(j=0; fossil_isident(zLine[i+j]); j++){} + if( j==0 ) goto page_skip; + }else{ + j = 0; + } + for(k=nHelp-1; k>=0 && fossil_isspace(zHelp[k]); k--){} + nHelp = k+1; + zHelp[nHelp] = 0; + for(k=0; k<nHelp && fossil_isspace(zHelp[k]); k++){} + if( k<nHelp ){ + z = string_dup(&zHelp[k], nHelp-k); + }else{ + z = ""; + } + for(k=nFixed; k<nUsed; k++){ + aEntry[k].zIf = zIf[0] ? string_dup(zIf, -1) : 0; + aEntry[k].zFunc = hasFunc ? string_dup(&zLine[i], j) : "0"; + aEntry[k].zHelp = z; + z = 0; + aEntry[k].iHelp = nFixed; + } + if( hasFunc ){ + i+=j; + while( fossil_isspace(zLine[i]) ){ i++; } + if( zLine[i]!='(' ) goto page_skip; + } + nFixed = nUsed; + nHelp = 0; + return; + +page_skip: + for(i=nFixed; i<nUsed; i++){ + fprintf(stderr,"%s:%d: skipping page \"%s\"\n", + zFile, nLine, aEntry[i].zPath); + } + nUsed = nFixed; +} + +/* +** Compare two entries +*/ +int e_compare(const void *a, const void *b){ + const Entry *pA = (const Entry*)a; + const Entry *pB = (const Entry*)b; + return strcmp(pA->zPath, pB->zPath); +} + +/* +** Build the binary search table. +*/ +void build_table(void){ + int i; + int nWeb = 0; + int mxLen = 0; + + qsort(aEntry, nFixed, sizeof(aEntry[0]), e_compare); + + printf( + "/* Automatically generated code\n" + "** DO NOT EDIT!\n" + "**\n" + "** This file was generated by the mkindex.exe program based on\n" + "** comments in other Fossil source files.\n" + "*/\n" + ); + + /* Output declarations for all the action functions */ + for(i=0; i<nFixed; i++){ + if( aEntry[i].eType & (CMDFLAG_SETTING|CMDFLAG_TOPIC) ) continue; + if( aEntry[i].zIf ) printf("%s", aEntry[i].zIf); + printf("extern void %s(void);\n", aEntry[i].zFunc); + if( aEntry[i].zIf ) printf("#endif\n"); + } + + /* Output strings for all the help text */ + for(i=0; i<nFixed; i++){ + char *z = aEntry[i].zHelp; + if( z==0 ) continue; + if( aEntry[i].zIf ) printf("%s", aEntry[i].zIf); + printf("static const char zHelp%03d[] =\n \"", aEntry[i].iHelp); + while( *z ){ + if( *z=='\n' ){ + printf("\\n\"\n \""); + }else if( *z=='"' ){ + printf("\\\""); + }else{ + putchar(*z); + } + z++; + } + printf("\";\n"); + if( aEntry[i].zIf ) printf("#endif\n"); + } + + /* Generate the aCommand[] table */ + printf("static const CmdOrPage aCommand[] = {\n"); + for(i=0; i<nFixed; i++){ + const char *z = aEntry[i].zPath; + int n = strlen(z); + if( n>mxLen ) mxLen = n; + if( aEntry[i].zIf ){ + printf("%s", aEntry[i].zIf); + }else if( (aEntry[i].eType & CMDFLAG_WEBPAGE)!=0 ){ + nWeb++; + } + printf(" { \"%.*s\",%*s%s,%*szHelp%03d, %3d, 0x%05x },\n", + n, z, + 25-n, "", + aEntry[i].zFunc, + (int)(29-strlen(aEntry[i].zFunc)), "", + aEntry[i].iHelp, + aEntry[i].iHelp, + aEntry[i].eType + ); + if( aEntry[i].zIf ) printf("#endif\n"); + } + printf("};\n"); + printf("#define FOSSIL_FIRST_CMD %d\n", nWeb); + printf("#define FOSSIL_MX_CMDNAME %d /* max length of any command name */\n", + mxLen); + printf("#define FOSSIL_MX_CMDIDX %d /* max index for commands */\n", nFixed); + + /* Generate the aSetting[] table */ + printf("const Setting aSetting[] = {\n"); + for(i=0; i<nFixed; i++){ + const char *z; + const char *zVar; + const char *zDef; + if( (aEntry[i].eType & CMDFLAG_SETTING)==0 ) continue; + z = aEntry[i].zPath; + zVar = aEntry[i].zVar; + zDef = aEntry[i].zDflt; + if( zDef==0 ) zDef = ""; + if( aEntry[i].zIf ){ + printf("%s", aEntry[i].zIf); + } + printf(" { \"%s\",%*s", z, (int)(20-strlen(z)), ""); + if( zVar ){ + printf(" \"%s\",%*s", zVar, (int)(15-strlen(zVar)), ""); + }else{ + printf(" 0,%*s", 16, ""); + } + printf(" %3d, %d, %d, %d, \"%s\"%*s },\n", + aEntry[i].iWidth, + (aEntry[i].eType & CMDFLAG_VERSIONABLE)!=0, + (aEntry[i].eType & CMDFLAG_BLOCKTEXT)!=0, + (aEntry[i].eType & CMDFLAG_SENSITIVE)!=0, + zDef, (int)(10-strlen(zDef)), "" + ); + if( aEntry[i].zIf ){ + printf("#endif\n"); + } + } + printf("{0,0,0,0,0,0,0}};\n"); + +} + +/* +** Process a single file of input +*/ +void process_file(void){ + FILE *in = fopen(zFile, "r"); + char zLine[2000]; + if( in==0 ){ + fprintf(stderr,"%s: cannot open\n", zFile); + return; + } + nLine = 0; + while( fgets(zLine, sizeof(zLine), in) ){ + nLine++; + scan_for_if(zLine); + scan_for_label("WEBPAGE:",zLine,CMDFLAG_WEBPAGE); + scan_for_label("COMMAND:",zLine,CMDFLAG_COMMAND); + scan_for_func(zLine); + scan_for_label("SETTING:",zLine,CMDFLAG_SETTING); + scan_for_default(zLine); + scan_for_label("TOPIC:",zLine,CMDFLAG_TOPIC); + } + fclose(in); + nUsed = nFixed; +} + +int main(int argc, char **argv){ + int i; + memset(aEntry, 0, sizeof(Entry) * N_ENTRY); + for(i=1; i<argc; i++){ + zFile = argv[i]; + process_file(); + } + build_table(); + return nErr; +} diff --git a/tools/mkversion.c b/tools/mkversion.c new file mode 100644 index 0000000..005babd --- /dev/null +++ b/tools/mkversion.c @@ -0,0 +1,218 @@ +/* +** This C program generates the "VERSION.h" header file from information +** extracted out of the "manifest", "manifest.uuid", and "VERSION" files. +** Call this program with three arguments: +** +** ./a.out manifest.uuid manifest VERSION +** +** Note that the manifest.uuid and manifest files are generated by Fossil. +** +** The output becomes the "VERSION.h" file. The output is a C-language +** header that contains #defines for various properties of the build: +** +** MANIFEST_UUID These values are text strings that +** MANIFEST_VERSION identify the Fossil check-in to which +** the source tree belongs. They do not +** take into account any uncommitted edits. +** +** FOSSIL_BUILD_HASH A hexadecimal string that is a strong hash +** of the MANIFEST_UUID together with the +** current time of the build. We normally want +** this to be different on each build, as the +** value is used to expire ETag: fields in +** HTTP requests. But if you need to do +** repeatable byte-for-byte identical builds, +** add the -DFOSSIL_BUILD_EPOCH=n option. +** +** MANIFEST_DATE The date/time of the source-code check-in +** MANIFEST_YEAR in various formats. +** MANIFEST_NUMERIC_DATE +** MANIFEST_NUMERIC_TIME +** +** RELEASE_VERSION The version number (from the VERSION source +** RELEASE_VERSION_NUMBER file) in various format. +** RELEASE_RESOURCE_VERSION +** +** New #defines may be added in the future. +*/ +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <ctype.h> +#include <time.h> + +#if defined(_MSC_VER) && (_MSC_VER < 1800) /* MSVS 2013 */ +# define strtoll _strtoi64 +#endif + +static FILE *open_for_reading(const char *zFilename){ + FILE *f = fopen(zFilename, "r"); + if( f==0 ){ + fprintf(stderr, "cannot open \"%s\" for reading\n", zFilename); + exit(1); + } + return f; +} + +/* +** Given an arbitrary-length input string key zIn, generate +** an N-byte hexadecimal hash of that string into zOut. +*/ +static void hash(const char *zIn, int N, char *zOut){ + unsigned char i, j, t; + int m, n; + unsigned char s[256]; + for(m=0; m<256; m++){ s[m] = m; } + for(j=0, m=n=0; m<256; m++, n++){ + j += s[m] + zIn[n]; + if( zIn[n]==0 ){ n = -1; } + t = s[j]; + s[j] = s[m]; + s[m] = t; + } + i = j = 0; + for(n=0; n<N-2; n+=2){ + i++; + t = s[i]; + j += t; + s[i] = s[j]; + s[j] = t; + t += s[i]; + zOut[n] = "0123456789abcdef"[(t>>4)&0xf]; + zOut[n+1] = "0123456789abcdef"[t&0xf]; + } + zOut[n] = 0; +} + +/* Local strcpy() clone to squelch an unwarranted warning from OpenBSD. */ +static void local_strcpy(char *dest, const char *src){ + while( (*(dest++) = *(src++))!=0 ){} +} + +int main(int argc, char *argv[]){ + FILE *m,*u,*v; + char *z; +#if defined(__DMC__) /* e.g. 0x857 */ + int i = 0; +#endif + int j = 0, x = 0, d = 0; + size_t n; + int vn[3]; + char b[1000]; + char vx[1000]; + if( argc!=4 ){ + fprintf(stderr, "Usage: %s manifest.uuid manifest VERSION\n", argv[0]); + exit(1); + } + memset(b,0,sizeof(b)); + memset(vx,0,sizeof(vx)); + u = open_for_reading(argv[1]); + if( fgets(b, sizeof(b)-1,u)==0 ){ + fprintf(stderr, "malformed manifest.uuid file: %s\n", argv[1]); + exit(1); + } + fclose(u); + for(z=b; z[0] && z[0]!='\r' && z[0]!='\n'; z++){} + *z = 0; + printf("#define MANIFEST_UUID \"%s\"\n",b); + printf("#define MANIFEST_VERSION \"[%10.10s]\"\n",b); + n = strlen(b); + if( n + 50 < sizeof(b) ){ +#ifdef FOSSIL_BUILD_EPOCH +#define str(s) #s + snprintf(b+n, sizeof(b)-n, + "%d", (int)strtoll(str(FOSSIL_BUILD_EPOCH), 0, 10)); +#else + const char *zEpoch = getenv("SOURCE_DATE_EPOCH"); + if( zEpoch && isdigit(zEpoch[0]) ){ + snprintf(b+n, sizeof(b)-n, "%d", (int)strtoll(zEpoch, 0, 10)); + }else{ + snprintf(b+n, sizeof(b)-n, "%d", (int)time(0)); + } +#endif + hash(b,33,vx); + printf("#define FOSSIL_BUILD_HASH \"%s\"\n", vx); + } + m = open_for_reading(argv[2]); + while(b == fgets(b, sizeof(b)-1,m)){ + if(0 == strncmp("D ",b,2)){ + int k, n; + char zDateNum[30]; + printf("#define MANIFEST_DATE \"%.10s %.8s\"\n",b+2,b+13); + printf("#define MANIFEST_YEAR \"%.4s\"\n",b+2); + n = 0; + for(k=0; k<10; k++){ + if( isdigit(b[k+2]) ) zDateNum[n++] = b[k+2]; + } + zDateNum[n] = 0; + printf("#define MANIFEST_NUMERIC_DATE %s\n", zDateNum); + n = 0; + for(k=0; k<8; k++){ + if( isdigit(b[k+13]) ) zDateNum[n++] = b[k+13]; + } + zDateNum[n] = 0; + for(k=0; zDateNum[k]=='0'; k++){} + printf("#define MANIFEST_NUMERIC_TIME %s\n", zDateNum+k); + } + } + fclose(m); + v = open_for_reading(argv[3]); + if( fgets(b, sizeof(b)-1,v)==0 ){ + fprintf(stderr, "malformed VERSION file: %s\n", argv[3]); + exit(1); + } + fclose(v); + for(z=b; z[0] && z[0]!='\r' && z[0]!='\n'; z++){} + *z = 0; + printf("#define RELEASE_VERSION \"%s\"\n", b); + z=b; + vn[0] = vn[1] = vn[2] = 0; + while(1){ + if( z[0]>='0' && z[0]<='9' ){ + x = x*10 + z[0] - '0'; + }else{ + if( j<3 ) vn[j++] = x; + x = 0; + if( z[0]==0 ) break; + } + z++; + } + for(z=vx; z[0]=='0'; z++){} + printf("#define RELEASE_VERSION_NUMBER %d%02d%02d\n", vn[0], vn[1], vn[2]); + memset(vx,0,sizeof(vx)); + local_strcpy(vx,b); + for(z=vx; z[0]; z++){ + if( z[0]=='-' ){ + z[0] = 0; + break; + } + if( z[0]!='.' ) continue; + if ( d<3 ){ + z[0] = ','; + d++; + }else{ + z[0] = '\0'; + break; + } + } + printf("#define RELEASE_RESOURCE_VERSION %s", vx); + while( d<3 ){ printf(",0"); d++; } + printf("\n"); +#if defined(__DMC__) /* e.g. 0x857 */ + d = (__DMC__ & 0xF00) >> 8; /* major */ + x = (__DMC__ & 0x0F0) >> 4; /* minor */ + i = (__DMC__ & 0x00F); /* revision */ + printf("#define COMPILER_VERSION \"%d.%d.%d\"\n", d, x, i); +#elif defined(__POCC__) /* e.g. 700 */ + d = (__POCC__ / 100); /* major */ + x = (__POCC__ % 100); /* minor */ + printf("#define COMPILER_VERSION \"%d.%02d\"\n", d, x); +#elif defined(_MSC_VER) /* e.g. 1800 */ + /* _MSC_FULL_VER also defined, e.g. 193030709 */ + d = (_MSC_VER / 100); /* major */ + x = (_MSC_VER % 100); /* minor */ + printf("#define COMPILER_VERSION \"%d.%02d.", d, x); + printf("%05d\"\n",(int)(_MSC_FULL_VER % 100000)); /* build (patch) */ +#endif + return 0; +} diff --git a/tools/randomize-js-names.tcl b/tools/randomize-js-names.tcl new file mode 100644 index 0000000..b5c5500 --- /dev/null +++ b/tools/randomize-js-names.tcl @@ -0,0 +1,36 @@ +#!/usr/bin/tclsh +# +# This script is run as part of "make wasm". After emcc has +# run to generate extsrc/pikchr.wasm and extsrc/pikchr.js from +# extsrc/pikchr.c, we need to make changes to these filenames to +# work around caching problems. +# +# (1) in extsrc/pikchr.js -> change "pikchr.wasm" into +# "pikchr-vNNNNNNNN.wasm" where Ns are random digits. +# +# (2) in extsrc/pikchr-worker.js -> change "pikchr-vNNNNNNNN.js" +# by altering the random digits N. +# +set DIR extsrc +if {[llength $argv]>0} { + set DIR [lindex $argv 0] +} + +set R [expr {int(rand()*10000000000)+1000000000}] +set in [open $DIR/pikchr.js rb] +set f1 [read $in] +close $in +set f1mod [regsub {\ypikchr(-v\d+)?\.wasm\y} $f1 "pikchr-v$R.wasm"] +set out [open $DIR/pikchr.js wb] +puts -nonewline $out $f1mod +close $out +puts "modified $DIR/pikchr.js to reference \"pikchr-v$R.wasm\"" + +set in [open $DIR/pikchr-worker.js rb] +set f1 [read $in] +close $in +set f1mod [regsub {\ypikchr(-v\d+)?\.js\y} $f1 "pikchr-v$R.js"] +set out [open $DIR/pikchr-worker.js wb] +puts -nonewline $out $f1mod +close $out +puts "modified $DIR/pikchr-worker.js to reference \"pikchr-v$R.js\"" diff --git a/tools/skintxt2config.c b/tools/skintxt2config.c new file mode 100644 index 0000000..99cf333 --- /dev/null +++ b/tools/skintxt2config.c @@ -0,0 +1,219 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* +** Copyright (c) 2021 Stephan Beal (https://wanderinghorse.net/home/stephan/) +** +** 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. +** +******************************************************************************* +** +** This application reads in Fossil SCM skin configuration files and emits +** them in a form suitable for importing directly into a fossil database +** using the (fossil config import) command. +** +** As input it requires one or more skin configuration files (css.txt, +** header.txt, footer.txt, details.txt, js.txt) and all output goes to +** stdout unless redirected using the -o FILENAME flag. +** +** Run it with no arguments or one of (help, --help, -?) for help text. +*/ +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <errno.h> +#include <time.h> +#include <stdarg.h> + +static struct App_ { + const char * argv0; + time_t now; + FILE * ostr; +} App = { +0, 0, 0 +}; + +static void err(const char *zFmt, ...){ + va_list vargs; + va_start(vargs, zFmt); + fputs("ERROR: ",stderr); + vfprintf(stderr, zFmt, vargs); + fputc('\n', stderr); + va_end(vargs); +} + +static void app_usage(int isErr){ + FILE * const ios = isErr ? stderr : stdout; + fprintf(ios, "Usage: %s ?OPTIONS? input-filename...\n\n", + App.argv0); + fprintf(ios, "Each filename must be one file which is conventionally " + "part of a Fossil SCM skin set:\n" + " css.txt, header.txt, footer.txt, details.txt, js.txt\n"); + fprintf(ios, "\nOptions:\n"); + fprintf(ios, "\n\t-o FILENAME = send output to the given file. " + "'-' means stdout (the default).\n"); + fputc('\n', ios); +} + +/* +** Reads file zFilename, stores its contents in *zContent, and sets the +** length of its contents to *nContent. +** +** Returns 0 on success. On error, *zContent and *nContent are not +** modified and it may emit a message describing the problem. +*/ +int read_file(char const *zFilename, unsigned char ** zContent, + int * nContent){ + long fpos; + int rc = 0; + unsigned char * zMem = 0; + FILE * f = fopen(zFilename, "rb"); + if(!f){ + err("Cannot open file %s. Errno=%d", zFilename, errno); + return errno; + } + fseek(f, 0L, SEEK_END); + rc = errno; + if(rc){ + err("Cannot seek() file %s. Errno=%d", zFilename, rc); + goto end; + } + fpos = ftell(f); + fseek(f, 0L, SEEK_SET); + zMem = (unsigned char *)malloc((size_t)fpos + 1); + if(!zMem){ + err("Malloc failed."); + rc = ENOMEM; + goto end; + } + zMem[fpos] = 0; + if(fpos && (size_t)1 != fread(zMem, (size_t)fpos, 1, f)){ + rc = EIO; + err("Error #%d reading file %s", rc, zFilename); + goto end; + } + end: + fclose(f); + if(rc){ + free(zMem); + }else{ + *zContent = zMem; + *nContent = fpos; + } + return rc; +} + +/* +** Expects zFilename to be one of the conventional skin filename +** parts. This routine converts it to config format and emits it to +** App.ostr. +*/ +int dispatch_file(char const *zFilename){ + const char * zKey = 0; + int nContent = 0, nContent2 = 0, nOut = 0, nTime = 0, rc = 0; + time_t theTime = App.now; + unsigned char * zContent = 0; + unsigned char * z = 0; + if(strstr(zFilename, "css.txt")){ + zKey = "css"; + }else if(strstr(zFilename, "header.txt")){ + zKey = "header"; + }else if(strstr(zFilename, "footer.txt")){ + zKey = "footer"; + }else if(strstr(zFilename, "details.txt")){ + zKey = "details"; + }else if(strstr(zFilename, "js.txt")){ + zKey = "js"; + }else { + err("Cannot determine skin part from filename: %s", zFilename); + return 1; + } + rc = read_file(zFilename, &zContent, &nContent); + if(rc) return rc; + for( z = zContent; z < zContent + nContent; ++z ){ + /* Count file content length with ' characters doubled */ + nContent2 += ('\'' == *z) ? 2 : 1; + } + while(theTime > 0){/* # of digits in time */ + ++nTime; + theTime /= 10; + } + fprintf(App.ostr, "config /config %d\n", + (int)(nTime + 12/*"value"+spaces+quotes*/ + + (int)strlen(zKey) + nContent2)); + fprintf(App.ostr, "%d '%s' value '", (int)App.now, zKey); + for( z = zContent; z < zContent + nContent; ++z ){ + /* Emit file content with ' characters doubled */ + if('\'' == (char)*z){ + fputc('\'', App.ostr); + } + fputc((char)*z, App.ostr); + } + free(zContent); + fprintf(App.ostr, "'\n"); + return 0; +} + +int main(int argc, char const * const * argv){ + int rc = 0, i ; + App.argv0 = argv[0]; + App.ostr = stdout; + if(argc<2){ + app_usage(1); + rc = 1; + goto end; + } + App.now = time(0); + for( i = 1; i < argc; ++i ){ + const char * zArg = argv[i]; + if(0==strcmp(zArg,"help") || + 0==strcmp(zArg,"--help") || + 0==strcmp(zArg,"-?")){ + app_usage(0); + rc = 0; + break; + }else if(0==strcmp(zArg,"-o")){ + /* -o OUTFILE (- == stdout) */ + ++i; + if(i==argc){ + err("Missing filename for -o flag"); + rc = 1; + break; + }else{ + const char *zOut = argv[i]; + if(App.ostr != stdout){ + err("Cannot specify -o more than once."); + rc = 1; + break; + } + if(0!=strcmp("-",zOut)){ + FILE * o = fopen(zOut, "wb"); + if(!o){ + err("Could not open file %s for writing. Errno=%d", + zOut, errno); + rc = errno; + break; + } + App.ostr = o; + } + } + }else if('-' == zArg[0]){ + err("Unhandled argument: %s", zArg); + rc = 1; + break; + }else{ + rc = dispatch_file(zArg); + if(rc) break; + } + } + end: + if(App.ostr != stdout){ + fclose(App.ostr); + } + return rc ? EXIT_FAILURE : EXIT_SUCCESS; +} diff --git a/tools/sqlcompattest.c b/tools/sqlcompattest.c new file mode 100644 index 0000000..57378af --- /dev/null +++ b/tools/sqlcompattest.c @@ -0,0 +1,89 @@ +/* +** Copyright (c) 2019 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 is NOT part of the Fossil executable. It is called from +** auto.def in the autosetup system. +** +** This file contains a test program used by ../configure with the +** the --disable-internal-sqlite option to determine whether or +** not the system SQLite library is sufficient to support Fossil. +** +** This must be compiled with -D MINIMUM_SQLITE_VERSION set in auto.def. +** +** It is preferred to statically link Fossil with the sqlite3.c source +** file that is part of the source tree and not use any SQLite shared +** library that is included with the system. But some packagers do not +** like to do this. Hence, we provide the option to link Fossil against +** the system SQLite shared library. But Fossil is very particular about +** the version and build options for SQLite. Unless a recent version of +** SQLite is available, and unless that SQLite is built using some +** non-default features, the system library won't meet the needs of +** Fossil. This program attempts to determine if the system library +** SQLite is sufficient for Fossil. +** +** Compile this program, linking it against the system SQLite library, +** and run it. If it returns with a zero exit code, then all is well. +** But if it returns a non-zero exit code, then the system SQLite library +** lacks some capability that Fossil uses. A message on stdout describes +** the missing feature. +*/ +#include "sqlite3.h" +#include <stdio.h> +#include <string.h> + +int main(int argc, char **argv){ + +#if !defined(MINIMUM_SQLITE_VERSION) +#error "Must set -DMINIMUM_SQLITE_VERSION=nn.nn.nn in auto.def" +#endif + +#define QUOTE(VAL) #VAL +#define STR(MACRO_VAL) QUOTE(MACRO_VAL) + + char zMinimumVersionNumber[8]="nn.nn.nn"; + strncpy((char *)&zMinimumVersionNumber,STR(MINIMUM_SQLITE_VERSION), + sizeof(zMinimumVersionNumber)); + + long major, minor, release, version; + sscanf(zMinimumVersionNumber, "%li.%li.%li", &major, &minor, &release); + version=(major*1000000)+(minor*1000)+release; + + int i; + static const char *zRequiredOpts[] = { + "ENABLE_FTS4", /* Required for repository search */ + "ENABLE_DBSTAT_VTAB", /* Required by /repo-tabsize page */ + }; + + /* Check minimum SQLite version number */ + if( sqlite3_libversion_number()<version ){ + printf("found system SQLite version %s but need %s or later, " + "consider removing --disable-internal-sqlite\n", + sqlite3_libversion(),STR(MINIMUM_SQLITE_VERSION)); + return 1; + } + + for(i=0; i<sizeof(zRequiredOpts)/sizeof(zRequiredOpts[0]); i++){ + if( !sqlite3_compileoption_used(zRequiredOpts[i]) ){ + printf("system SQLite library omits required build option -DSQLITE_%s\n", + zRequiredOpts[i]); + return 1; + } + } + + /* Success! */ + return 0; +} diff --git a/tools/translate.c b/tools/translate.c new file mode 100644 index 0000000..6863d2e --- /dev/null +++ b/tools/translate.c @@ -0,0 +1,270 @@ +/* +** Copyright (c) 2002 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/ +** +******************************************************************************* +** +** SYNOPSIS: +** +** Input lines that begin with the "@" character are translated into +** either cgi_printf() statements or string literals and the +** translated code is written on standard output. +** +** The problem this program is attempt to solve is as follows: When +** writing CGI programs in C, we typically want to output a lot of HTML +** text to standard output. In pure C code, this involves doing a +** printf() with a big string containing all that text. But we have +** to insert special codes (ex: \n and \") for many common characters, +** which interferes with the readability of the HTML. +** +** This tool allows us to put raw HTML, without the special codes, in +** the middle of a C program. This program then translates the text +** into standard C by inserting all necessary backslashes and other +** punctuation. +** +** Enhancement #1: +** +** If the last non-whitespace character prior to the first "@" of a +** @-block is "=" or "," then the @-block is a string literal initializer +** rather than text that is to be output via cgi_printf(). Render it +** as such. +** +** Enhancement #2: +** +** Comments of the form: "|* @-comment: CC" (where "|" is really "/") +** cause CC to become a comment character for the @-substitution. +** Typical values for CC are "--" (for SQL text) or "#" (for Tcl script) +** or "//" (for C++ code). Lines of subsequent @-blocks that begin with +** CC are omitted from the output. +** +** Enhancement #3: +** +** If a non-enhancement #1 line ends in backslash, the backslash and the +** newline (\n) are not included in the argument to cgi_printf(). This +** is used to split one long output line across multiple source lines. +*/ +#include <stdio.h> +#include <ctype.h> +#include <stdlib.h> +#include <string.h> + +/* +** Space to hold arguments at the end of the cgi_printf() +*/ +#define MX_ARG_SP 10000 +static char zArg[MX_ARG_SP]; +static int nArg = 0; + +/* +** True if we are currently in a cgi_printf() +*/ +static int inPrint = 0; + +/* +** True if we are currently doing a free string +*/ +static int inStr = 0; + +/* +** Name of files being processed +*/ +static const char *zInFile = "(stdin)"; + +/* +** The `fossil_isspace()' function copied from the Fossil source code. +** Some MSVC runtime library versions of `isspace()' break with an `assert()' if +** the input is smaller than -1 or greater than 255 in debug builds, due to sign +** extension when promoting `signed char' to `int' for non-ASCII characters. Use +** an `isspace()' replacement instead of explicit type casts to `unsigned char'. +*/ +int fossil_isspace(char c){ + return c==' ' || (c<='\r' && c>='\t'); +} + +/* +** Terminate an active cgi_printf() or free string +*/ +static void end_block(FILE *out){ + if( inPrint ){ + zArg[nArg] = 0; + fprintf(out, "%s);\n", zArg); + nArg = 0; + inPrint = 0; + } +} + +/* +** Translate the input stream into the output stream +*/ +static void trans(FILE *in, FILE *out){ + int i, j, k; /* Loop counters */ + char c1, c2; /* Characters used to start a comment */ + int lastWasEq = 0; /* True if last non-whitespace character was "=" */ + int lastWasComma = 0; /* True if last non-whitespace character was "," */ + int lineNo = 0; /* Line number */ + char zLine[2000]; /* A single line of input */ + char zOut[4000]; /* The input line translated into appropriate output */ + + c1 = c2 = '-'; + while( fgets(zLine, sizeof(zLine), in) ){ + lineNo++; + for(i=0; zLine[i] && fossil_isspace(zLine[i]); i++){} + if( zLine[i]!='@' ){ + if( inPrint || inStr ) end_block(out); + fprintf(out,"%s",zLine); + /* 0123456789 12345 */ + if( strncmp(zLine, "/* @-comment: ", 14)==0 ){ + c1 = zLine[14]; + c2 = zLine[15]; + } + i += strlen(&zLine[i]); + while( i>0 && fossil_isspace(zLine[i-1]) ){ i--; } + lastWasEq = i>0 && zLine[i-1]=='='; + lastWasComma = i>0 && zLine[i-1]==','; + }else if( lastWasEq || lastWasComma){ + /* If the last non-whitespace character before the first @ was + ** an "="(var init/set) or a ","(const definition in list) then + ** generate a string literal. But skip comments + ** consisting of all text between c1 and c2 (default "--") + ** and end of line. + */ + int indent, omitline; + char *zNewline = "\\n"; + i++; + if( fossil_isspace(zLine[i]) ){ i++; } + indent = i - 2; + if( indent<0 ) indent = 0; + omitline = 0; + for(j=0; zLine[i] && zLine[i]!='\r' && zLine[i]!='\n'; i++){ + if( zLine[i]==c1 && (c2==' ' || zLine[i+1]==c2) ){ + omitline = 1; break; + } + if( zLine[i]=='\\' && (zLine[i+1]==0 || zLine[i+1]=='\r' + || zLine[i+1]=='\n') ){ + zLine[i] = 0; + zNewline = ""; + /* fprintf(stderr, "%s:%d: omit newline\n", zInFile, lineNo); */ + break; + } + if( zLine[i]=='\\' || zLine[i]=='"' ){ zOut[j++] = '\\'; } + zOut[j++] = zLine[i]; + } + if( zNewline[0] ) while( j>0 && fossil_isspace(zOut[j-1]) ){ j--; } + zOut[j] = 0; + if( j<=0 && omitline ){ + fprintf(out,"\n"); + }else{ + fprintf(out,"%*s\"%s%s\"\n",indent, "", zOut, zNewline); + } + }else{ + /* Otherwise (if the last non-whitespace was not '=') then generate a + ** cgi_printf() statement whose format is the text following the '@'. + ** Substrings of the form "%C(...)" (where C is any sequence of characters + ** other than \000 and '(') will put "%C" in the format and add the + ** "(...)" as an argument to the cgi_printf call. Each '*' character + ** present in C (max two) causes one more "(...)" sequence to be consumed. + ** For example, "%*.*d(4)(2)(1)" converts to "%*.*d" with arguments "4", + ** "2", and "1", which will be used as the field width, precision, and + ** value, respectively, producing a final formatted result of " 01". + */ + const char *zNewline = "\\n"; + int indent; + int nC; + int nParam; + char c; + i++; + if( fossil_isspace(zLine[i]) ){ i++; } + indent = i; + for(j=0; zLine[i] && zLine[i]!='\r' && zLine[i]!='\n'; i++){ + if( zLine[i]=='\\' && (!zLine[i+1] || zLine[i+1]=='\r' + || zLine[i+1]=='\n') ){ + zNewline = ""; + break; + } + if( zLine[i]=='"' || zLine[i]=='\\' ){ zOut[j++] = '\\'; } + zOut[j++] = zLine[i]; + if( zLine[i]!='%' || zLine[i+1]=='%' || zLine[i+1]==0 ) continue; + nParam=1; + for(nC=1; zLine[i+nC] && zLine[i+nC]!='('; nC++){ + if( zLine[i+nC]=='*' && nParam < 3 ) nParam++; + } + if( zLine[i+nC]!='(' || !isalpha(zLine[i+nC-1]) ) continue; + while( --nC ) zOut[j++] = zLine[++i]; + do{ + zArg[nArg++] = ','; + k = 0; i++; + if( zLine[i]!='(' ) break; + while( (c = zLine[i])!=0 ){ + zArg[nArg++] = c; + if( c==')' ){ + k--; + if( k==0 ) break; + }else if( c=='(' ){ + k++; + } + i++; + } + }while( --nParam ); + } + zOut[j] = 0; + if( !inPrint ){ + fprintf(out,"%*scgi_printf(\"%s%s\"",indent-2,"", zOut, zNewline); + inPrint = 1; + }else{ + fprintf(out,"\n%*s\"%s%s\"",indent+5, "", zOut, zNewline); + } + } + } +} + +static void print_source_ref(const char *zSrcFile, FILE *out){ +/* Set source line reference to the original source file. + * This makes compiler show the original file name in the compile error + * messages, instead of referring to the translated file. + * NOTE: This somewhat complicates stepping in debugger, as the resuling + * code would not match the referenced sources. + */ +#ifndef FOSSIL_DEBUG + const char *arg; + if( !*zSrcFile ){ + return; + } + fprintf(out,"#line 1 \""); + for(arg=zSrcFile; *arg; arg++){ + if( *arg!='\\' ){ + fprintf(out,"%c", *arg); + }else{ + fprintf(out,"\\\\"); + } + } + fprintf(out,"\"\n"); +#endif +} + +int main(int argc, char **argv){ + if( argc==2 ){ + FILE *in = fopen(argv[1], "r"); + if( in==0 ){ + fprintf(stderr,"can not open %s\n", argv[1]); + exit(1); + } + zInFile = argv[1]; + print_source_ref(zInFile, stdout); + trans(in, stdout); + fclose(in); + }else{ + trans(stdin, stdout); + } + return 0; +} |
