summaryrefslogtreecommitdiff
path: root/tools
diff options
context:
space:
mode:
authorcashmere <cashmere@autistici.org>2026-09-04 09:05:24 +0200
committercashmere <cashmere@autistici.org>2026-09-04 09:05:24 +0200
commit4f49b16079b27d7ce633619e4cddfdf2bffbc952 (patch)
treec296c1e7f3c3e2f6859c6d3a1b1b208779b89679 /tools
downloadrelic-master.tar.gz
initial commitHEADmaster
Diffstat (limited to 'tools')
-rwxr-xr-xtools/co-rsync.tcl72
-rw-r--r--tools/codecheck1.c700
-rw-r--r--tools/copland/dist/copland-skin.config827
-rw-r--r--tools/copland/package-lock.json549
-rw-r--r--tools/copland/package.json15
-rw-r--r--tools/copland/playwright.config.mjs20
-rw-r--r--tools/copland/scripts/build-config.mjs30
-rwxr-xr-xtools/copland/scripts/create-demo.sh113
-rwxr-xr-xtools/copland/scripts/fossil-env.sh42
-rwxr-xr-xtools/copland/scripts/install-repolist.sh34
-rwxr-xr-xtools/copland/scripts/install.sh24
-rwxr-xr-xtools/copland/scripts/preview.sh31
-rw-r--r--tools/copland/src/copland.scss443
-rw-r--r--tools/copland/tests/fixtures/org/all.html50
-rw-r--r--tools/copland/tests/fixtures/org/all.org52
-rw-r--r--tools/copland/tests/fixtures/org/all.txt66
-rw-r--r--tools/copland/tests/fixtures/org/security.html9
-rw-r--r--tools/copland/tests/fixtures/org/security.org13
-rwxr-xr-xtools/copland/tests/org-renderer.sh30
-rwxr-xr-xtools/copland/tests/serve-fixture.sh18
-rwxr-xr-xtools/copland/tests/smoke.sh188
-rw-r--r--tools/copland/tests/visual.spec.mjs94
-rw-r--r--tools/copland/tests/visual.spec.mjs-snapshots/home-dark-linux.pngbin0 -> 21720 bytes
-rw-r--r--tools/copland/tests/visual.spec.mjs-snapshots/home-light-linux.pngbin0 -> 21117 bytes
-rw-r--r--tools/copland/tests/visual.spec.mjs-snapshots/login-dark-linux.pngbin0 -> 33606 bytes
-rw-r--r--tools/copland/tests/visual.spec.mjs-snapshots/login-light-linux.pngbin0 -> 32850 bytes
-rw-r--r--tools/copland/tests/visual.spec.mjs-snapshots/org-document-dark-linux.pngbin0 -> 68961 bytes
-rw-r--r--tools/copland/tests/visual.spec.mjs-snapshots/org-document-light-linux.pngbin0 -> 67587 bytes
-rw-r--r--tools/copland/tests/visual.spec.mjs-snapshots/org-document-mobile-linux.pngbin0 -> 53550 bytes
-rw-r--r--tools/copland/tests/visual.spec.mjs-snapshots/org-wiki-dark-linux.pngbin0 -> 42595 bytes
-rw-r--r--tools/copland/tests/visual.spec.mjs-snapshots/org-wiki-light-linux.pngbin0 -> 41174 bytes
-rw-r--r--tools/copland/tests/visual.spec.mjs-snapshots/org-wiki-mobile-linux.pngbin0 -> 31636 bytes
-rw-r--r--tools/copland/tests/visual.spec.mjs-snapshots/repository-list-dark-linux.pngbin0 -> 16492 bytes
-rw-r--r--tools/copland/tests/visual.spec.mjs-snapshots/repository-list-light-linux.pngbin0 -> 15656 bytes
-rw-r--r--tools/copland/tests/visual.spec.mjs-snapshots/timeline-dark-linux.pngbin0 -> 46898 bytes
-rw-r--r--tools/copland/tests/visual.spec.mjs-snapshots/timeline-light-linux.pngbin0 -> 45662 bytes
-rw-r--r--tools/copland/tests/visual.spec.mjs-snapshots/timeline-mobile-linux.pngbin0 -> 35066 bytes
-rw-r--r--tools/copland/tests/visual.spec.mjs-snapshots/tree-dark-linux.pngbin0 -> 50142 bytes
-rw-r--r--tools/copland/tests/visual.spec.mjs-snapshots/tree-light-linux.pngbin0 -> 48683 bytes
-rw-r--r--tools/cvs2fossil/changeset119
-rw-r--r--tools/cvs2fossil/cvs2fossil32
-rw-r--r--tools/cvs2fossil/doc/LICENSE19
-rw-r--r--tools/cvs2fossil/doc/README7
-rw-r--r--tools/cvs2fossil/filerevtree178
-rw-r--r--tools/cvs2fossil/getmemoryseries.tcl69
-rw-r--r--tools/cvs2fossil/lib/c2f_blobstore.tcl155
-rw-r--r--tools/cvs2fossil/lib/c2f_cyclebreaker.tcl579
-rw-r--r--tools/cvs2fossil/lib/c2f_file.tcl1452
-rw-r--r--tools/cvs2fossil/lib/c2f_fossil.tcl273
-rw-r--r--tools/cvs2fossil/lib/c2f_frev.tcl552
-rw-r--r--tools/cvs2fossil/lib/c2f_fsym.tcl325
-rw-r--r--tools/cvs2fossil/lib/c2f_ftrunk.tcl57
-rw-r--r--tools/cvs2fossil/lib/c2f_gtcore.tcl239
-rw-r--r--tools/cvs2fossil/lib/c2f_integrity.tcl964
-rw-r--r--tools/cvs2fossil/lib/c2f_option.tcl284
-rw-r--r--tools/cvs2fossil/lib/c2f_pass.tcl243
-rw-r--r--tools/cvs2fossil/lib/c2f_patopsort.tcl210
-rw-r--r--tools/cvs2fossil/lib/c2f_pbreakacycle.tcl433
-rw-r--r--tools/cvs2fossil/lib/c2f_pbreakrcycle.tcl126
-rw-r--r--tools/cvs2fossil/lib/c2f_pbreakscycle.tcl126
-rw-r--r--tools/cvs2fossil/lib/c2f_pcollar.tcl337
-rw-r--r--tools/cvs2fossil/lib/c2f_pcollrev.tcl680
-rw-r--r--tools/cvs2fossil/lib/c2f_pcollsym.tcl318
-rw-r--r--tools/cvs2fossil/lib/c2f_pcsetdeps.tcl135
-rw-r--r--tools/cvs2fossil/lib/c2f_pfiltersym.tcl598
-rw-r--r--tools/cvs2fossil/lib/c2f_pimportcsets.tcl133
-rw-r--r--tools/cvs2fossil/lib/c2f_pimportfiles.tcl151
-rw-r--r--tools/cvs2fossil/lib/c2f_pimportfinal.tcl122
-rw-r--r--tools/cvs2fossil/lib/c2f_pinitcsets.tcl379
-rw-r--r--tools/cvs2fossil/lib/c2f_prev.tcl1846
-rw-r--r--tools/cvs2fossil/lib/c2f_prevlink.tcl240
-rw-r--r--tools/cvs2fossil/lib/c2f_project.tcl233
-rw-r--r--tools/cvs2fossil/lib/c2f_prtopsort.tcl159
-rw-r--r--tools/cvs2fossil/lib/c2f_psym.tcl422
-rw-r--r--tools/cvs2fossil/lib/c2f_ptrunk.tcl89
-rw-r--r--tools/cvs2fossil/lib/c2f_repository.tcl502
-rw-r--r--tools/cvs2fossil/lib/c2f_ristate.tcl113
-rw-r--r--tools/cvs2fossil/lib/c2f_state.tcl231
-rw-r--r--tools/cvs2fossil/lib/c2f_wsstate.tcl136
-rw-r--r--tools/cvs2fossil/lib/cvs2fossil.tcl89
-rw-r--r--tools/cvs2fossil/lib/dot.tcl108
-rw-r--r--tools/cvs2fossil/lib/id.tcl64
-rw-r--r--tools/cvs2fossil/lib/log.tcl184
-rw-r--r--tools/cvs2fossil/lib/mem.tcl139
-rw-r--r--tools/cvs2fossil/lib/misc.tcl144
-rw-r--r--tools/cvs2fossil/lib/pkgIndex.tcl47
-rw-r--r--tools/cvs2fossil/lib/rcsparser.tcl485
-rw-r--r--tools/cvs2fossil/lib/trouble.tcl124
-rw-r--r--tools/cvs2fossil/lodtree88
-rw-r--r--tools/cvs2fossil/showg6
-rw-r--r--tools/cvs2fossil/symboltree88
-rw-r--r--tools/decode-email.c163
-rwxr-xr-xtools/email-monitor.tcl51
-rwxr-xr-xtools/email-sender.tcl65
-rw-r--r--tools/emcc.sh.in66
-rwxr-xr-xtools/encode_math.sh12
-rw-r--r--tools/fake-smtpd.tcl84
-rwxr-xr-xtools/find-fossil-cgis.tcl141
-rwxr-xr-xtools/fossil-autocomplete.bash15
-rwxr-xr-xtools/fossil-autocomplete.zsh3529
-rwxr-xr-xtools/fossil-diff-log72
-rwxr-xr-xtools/fossil-stress.tcl144
-rwxr-xr-xtools/fossilwiki132
-rwxr-xr-xtools/fslsrv59
-rw-r--r--tools/makeheaders.c3441
-rw-r--r--tools/makeheaders.html1150
-rwxr-xr-xtools/makemake.tcl2159
-rwxr-xr-xtools/man_page_command_list.tcl68
-rw-r--r--tools/mkbuiltin.c415
-rw-r--r--tools/mkindex.c607
-rw-r--r--tools/mkversion.c218
-rw-r--r--tools/randomize-js-names.tcl36
-rw-r--r--tools/skintxt2config.c219
-rw-r--r--tools/sqlcompattest.c89
-rw-r--r--tools/translate.c270
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">&middot;</span>
+ <span>Powered by Fossil $release_version</span>
+ <span aria-hidden="true">&middot;</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 &amp; &lt;Fossil&gt;</h1>
+<dl class="org-metadata">
+<dt>Author</dt><dd>Ada &lt;admin@example.test&gt;</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 &amp; 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 &amp; safety</h1>
+<p>A paragraph with <strong>bold</strong>, <em>italic</em>, <u>underline</u>, <del>strike</del>, <code>&lt;code&gt;</code>, and <code>verbatim &amp; 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>&lt;tag&gt;</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, ... }: &lt;unsafe&gt; &amp; value</code></pre>
+<pre class="org-block org-example"><code>example &lt;script&gt;alert(1)&lt;/script&gt;</code></pre>
+<p><a href="https://example.test/a?x=1&amp;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">&lt;img src=x onerror=alert(1)&gt;</h1>
+<p>Raw &lt;script&gt;alert(1)&lt;/script&gt; and &quot;quotes&quot;.</p>
+<p><a href="https://example.test/&quot; onmouseover=&quot;alert(1)">attribute</a> file scheme confusion</p>
+<pre class="org-block org-unknown-block"><code>&lt;img src=x onerror=alert(1)&gt;</code></pre>
+<pre class="org-block org-src has-language"><code class="language-sh">echo &quot;unterminated and escaped &lt;tag&gt;&quot;
+</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 '&lt;script&gt;|&lt;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
new file mode 100644
index 0000000..52f9478
--- /dev/null
+++ b/tools/copland/tests/visual.spec.mjs-snapshots/home-dark-linux.png
Binary files differ
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
new file mode 100644
index 0000000..4660bf9
--- /dev/null
+++ b/tools/copland/tests/visual.spec.mjs-snapshots/home-light-linux.png
Binary files differ
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
new file mode 100644
index 0000000..738588b
--- /dev/null
+++ b/tools/copland/tests/visual.spec.mjs-snapshots/login-dark-linux.png
Binary files differ
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
new file mode 100644
index 0000000..03ba2db
--- /dev/null
+++ b/tools/copland/tests/visual.spec.mjs-snapshots/login-light-linux.png
Binary files differ
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
new file mode 100644
index 0000000..15d37f9
--- /dev/null
+++ b/tools/copland/tests/visual.spec.mjs-snapshots/org-document-dark-linux.png
Binary files differ
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
new file mode 100644
index 0000000..3c0785f
--- /dev/null
+++ b/tools/copland/tests/visual.spec.mjs-snapshots/org-document-light-linux.png
Binary files differ
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
new file mode 100644
index 0000000..d1f5cba
--- /dev/null
+++ b/tools/copland/tests/visual.spec.mjs-snapshots/org-document-mobile-linux.png
Binary files differ
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
new file mode 100644
index 0000000..464197b
--- /dev/null
+++ b/tools/copland/tests/visual.spec.mjs-snapshots/org-wiki-dark-linux.png
Binary files differ
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
new file mode 100644
index 0000000..ff3bdc1
--- /dev/null
+++ b/tools/copland/tests/visual.spec.mjs-snapshots/org-wiki-light-linux.png
Binary files differ
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
new file mode 100644
index 0000000..50381e0
--- /dev/null
+++ b/tools/copland/tests/visual.spec.mjs-snapshots/org-wiki-mobile-linux.png
Binary files differ
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
new file mode 100644
index 0000000..0cf1b4f
--- /dev/null
+++ b/tools/copland/tests/visual.spec.mjs-snapshots/repository-list-dark-linux.png
Binary files differ
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
new file mode 100644
index 0000000..af49135
--- /dev/null
+++ b/tools/copland/tests/visual.spec.mjs-snapshots/repository-list-light-linux.png
Binary files differ
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
new file mode 100644
index 0000000..469d7b0
--- /dev/null
+++ b/tools/copland/tests/visual.spec.mjs-snapshots/timeline-dark-linux.png
Binary files differ
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
new file mode 100644
index 0000000..e1dbe69
--- /dev/null
+++ b/tools/copland/tests/visual.spec.mjs-snapshots/timeline-light-linux.png
Binary files differ
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
new file mode 100644
index 0000000..02ed528
--- /dev/null
+++ b/tools/copland/tests/visual.spec.mjs-snapshots/timeline-mobile-linux.png
Binary files differ
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
new file mode 100644
index 0000000..164d3eb
--- /dev/null
+++ b/tools/copland/tests/visual.spec.mjs-snapshots/tree-dark-linux.png
Binary files differ
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
new file mode 100644
index 0000000..ed51114
--- /dev/null
+++ b/tools/copland/tests/visual.spec.mjs-snapshots/tree-light-linux.png
Binary files differ
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 {
+ ![regexp {^\d*[^,.:;@$]([^,.:;@$]*\d*)*$} $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 &#8220;<code>.h</code>&#8221;
+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> &#8220;<code>extern</code>&#8221; 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 &#8220;<code>.h</code>&#8221; suffix and definitions are
+placed in &#8220;<code>.c</code>&#8221; files.
+The .c files contain &#8220;<code>#include</code>&#8221; 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>&lt;stdio.h&gt</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 &#8220;exportable&#8221;.
+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
+&#8220;<code>alpha.c</code>&#8221;,
+&#8220;<code>beta.c</code>&#8221; and
+&#8220;<code>gamma.c</code>&#8221;
+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 &#8220;<code>-f</code>&#8221; 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 &#8220;<code>mkhdr.dat</code>&#8221;
+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 &#8220;<code>-local</code>&#8221; option causes makeheaders to
+generate of prototypes for &#8220;<code>static</code>&#8221; functions and
+procedures.
+Such prototypes are normally omitted.
+</p>
+
+<p>
+Finally, makeheaders also includes a &#8220;<code>-doc</code>&#8221; 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 &#8220;<code>--help</code>&#8221; or
+&#8220;<code>-?</code>&#8221;)
+and it will print a summary of the available options on standard
+error.
+If you need to process a file whose name begins with
+&#8220;<code>-</code>&#8221;,
+you can prepend a &#8220;<code>./</code>&#8221; to its name in order to get it
+accepted by the command line parser.
+Or, you can insert the special option &#8220;<code>--</code>&#8221; on the
+command line to cause all subsequent command line arguments to be treated as
+filenames even if their names begin with &#8220;<code>-</code>&#8221;.
+</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)
+&#8220;<code>alpha.c</code>&#8221;
+makeheaders will attempt to generate a corresponding header file
+named &#8220;<code>alpha.h</code>&#8221;.
+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 &#8220;<code>#include</code>&#8221; to the
+top of the file that sources the header file that makeheaders will generate.
+Hence, the beginning of a source file named &#8220;<code>alpha.c</code>&#8221;
+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
+&#8220;<code>#if</code>&#8221; 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 &#8220;<code>beta.h</code>&#8221;,
+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 &#8220;<code>#if</code>&#8221; 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,
+&#8220;<code>*.[ch]</code>&#8221;,
+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 &#8220;<code>static</code>&#8221; keyword of C appears at the
+beginning of the function definition, the prototype is suppressed.
+If you use the &#8220;<code>LOCAL</code>&#8221; keyword where you would normally
+say &#8220;<code>static</code>&#8221;, 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 &#8220;<code>LOCAL</code>&#8221; 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 &#8220;<code>-local</code>&#8221;
+command-line option, then it treats the &#8220;<code>static</code>&#8221;
+keyword like &#8220;<code>LOCAL</code>&#8221; 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
+&#8220;<code>extern</code>&#8221;
+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 &#8220;<code>struct</code>&#8221;, &#8220;<code>union</code>&#8221; or
+&#8220;<code>enum</code>&#8221; 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 &#8220;<code>X</code>&#8221; requires a
+prior declaration of another structure &#8220;<code>Y</code>&#8221;, 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
+&#8220;<code>#if INTERFACE</code>&#8221; but before the corresponding
+&#8220;<code>#endif</code>&#8221; are eligible to be copied into the
+automatically generated
+.h files of other .c files.
+</p>
+
+<p>
+If you use the &#8220;<code>#if INTERFACE</code>&#8221; 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
+&#8220;<code>#if INTERFACE</code>&#8221; 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
+&#8220;<code>#if INTERFACE</code>&#8221; 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 &#8220;<code>export</code>&#8221;.
+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 &#8220;<code>static</code>&#8221; 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: &#8220;<code>EXPORT</code>&#8221;
+</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 &#8220;<code>#if EXPORT_INTERFACE</code>&#8221; mechanism can be used in
+either .c or .h files.
+(The &#8220;<code>#if INTERFACE</code>&#8221; 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 &#8220;<code>private</code>&#8221;.
+</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 &#8220;<code>LOCAL_INTERFACE</code>&#8221; block works very much like the
+&#8220;<code>INTERFACE</code>&#8221; and
+&#8220;<code>EXPORT_INTERFACE</code>&#8221;
+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 &#8220;<code>class</code>&#8221;
+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 &#8220;<code>alpha.cpp</code>&#8221; will induce makeheaders to
+generate a header file named &#8220;<code>alpha.hpp</code>&#8221;.
+</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&amp;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&amp;R function.
+Therefore you should take care to avoid putting K&amp;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. &#8220;<code>enum {X, Y, Z};</code>&#8221;.
+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 &#8220;<code>-doc</code>&#8221; 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 &#8220;<code>-doc</code>&#8221; 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: &#8220;<code>#define</code>&#8221;)
+</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
+ (&#8220;<code>#if</code>&#8221;) 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;
+}