Index: src/checkin.c ================================================================== --- src/checkin.c +++ src/checkin.c @@ -276,15 +276,15 @@ int dotfilesFlag; const char *zIgnoreFlag; Blob path, repo; Stmt q; int n; + Glob *pIgnore; + allFlag = find_option("force","f",0)!=0; dotfilesFlag = find_option("dotfiles",0,0)!=0; zIgnoreFlag = find_option("ignore",0,1); - Glob *pIgnore; - db_must_be_within_tree(); if( zIgnoreFlag==0 ){ zIgnoreFlag = db_get("ignore-glob", 0); } db_multi_exec("CREATE TEMP TABLE sfile(x TEXT PRIMARY KEY)"); Index: src/clone.c ================================================================== --- src/clone.c +++ src/clone.c @@ -64,14 +64,14 @@ file_copy(g.urlName, g.argv[3]); db_close(1); db_open_repository(g.argv[3]); db_record_repository_filename(g.argv[3]); db_multi_exec( - "REPLACE INTO config(name,value)" - " VALUES('server-code', lower(hex(randomblob(20))));" - "REPLACE INTO config(name,value)" - " VALUES('last-sync-url', '%q');", + "REPLACE INTO config(name,value,mtime)" + " VALUES('server-code', lower(hex(randomblob(20))),now());" + "REPLACE INTO config(name,value,mtime)" + " VALUES('last-sync-url', '%q',now());", g.urlCanonical ); db_multi_exec( "DELETE FROM blob WHERE rid IN private;" "DELETE FROM delta wHERE rid IN private;" @@ -92,12 +92,12 @@ user_select(); db_set("content-schema", CONTENT_SCHEMA, 0); db_set("aux-schema", AUX_SCHEMA, 0); db_set("last-sync-url", g.argv[2], 0); db_multi_exec( - "REPLACE INTO config(name,value)" - " VALUES('server-code', lower(hex(randomblob(20))));" + "REPLACE INTO config(name,value,mtime)" + " VALUES('server-code', lower(hex(randomblob(20))), now());" ); url_enable_proxy(0); url_get_password_if_needed(); g.xlinkClusterOnly = 1; nErr = client_sync(0,0,1,bPrivate,CONFIGSET_ALL,0); Index: src/configure.c ================================================================== --- src/configure.c +++ src/configure.c @@ -2,11 +2,11 @@ ** Copyright (c) 2008 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: @@ -27,18 +27,21 @@ #if INTERFACE /* ** Configuration transfers occur in groups. These are the allowed ** groupings: */ -#define CONFIGSET_SKIN 0x000001 /* WWW interface appearance */ -#define CONFIGSET_TKT 0x000002 /* Ticket configuration */ -#define CONFIGSET_PROJ 0x000004 /* Project name */ -#define CONFIGSET_SHUN 0x000008 /* Shun settings */ -#define CONFIGSET_USER 0x000010 /* The USER table */ -#define CONFIGSET_ADDR 0x000020 /* The CONCEALED table */ - -#define CONFIGSET_ALL 0xffffff /* Everything */ +#define CONFIGSET_SKIN 0x000001 /* WWW interface appearance */ +#define CONFIGSET_TKT 0x000002 /* Ticket configuration */ +#define CONFIGSET_PROJ 0x000004 /* Project name */ +#define CONFIGSET_SHUN 0x000008 /* Shun settings */ +#define CONFIGSET_USER 0x000010 /* The USER table */ +#define CONFIGSET_ADDR 0x000020 /* The CONCEALED table */ + +#define CONFIGSET_ALL 0x0000ff /* Everything */ + +#define CONFIGSET_OVERWRITE 0x100000 /* Causes overwrite instead of merge */ +#define CONFIGSET_OLDFORMAT 0x200000 /* Use the legacy format */ #endif /* INTERFACE */ /* ** Names of the configuration sets @@ -46,17 +49,17 @@ static struct { const char *zName; /* Name of the configuration set */ int groupMask; /* Mask for that configuration set */ const char *zHelp; /* What it does */ } aGroupName[] = { - { "email", CONFIGSET_ADDR, "Concealed email addresses in tickets" }, - { "project", CONFIGSET_PROJ, "Project name and description" }, - { "skin", CONFIGSET_SKIN, "Web interface apparance settings" }, - { "shun", CONFIGSET_SHUN, "List of shunned artifacts" }, - { "ticket", CONFIGSET_TKT, "Ticket setup", }, - { "user", CONFIGSET_USER, "Users and privilege settings" }, - { "all", CONFIGSET_ALL, "All of the above" }, + { "/email", CONFIGSET_ADDR, "Concealed email addresses in tickets" }, + { "/project", CONFIGSET_PROJ, "Project name and description" }, + { "/skin", CONFIGSET_SKIN, "Web interface apparance settings" }, + { "/shun", CONFIGSET_SHUN, "List of shunned artifacts" }, + { "/ticket", CONFIGSET_TKT, "Ticket setup", }, + { "/user", CONFIGSET_USER, "Users and privilege settings" }, + { "/all", CONFIGSET_ALL, "All of the above" }, }; /* ** The following is a list of settings that we are willing to @@ -106,15 +109,29 @@ const char *configure_first_name(int iMask){ iConfig = 0; return configure_next_name(iMask); } const char *configure_next_name(int iMask){ - while( iConfig2 && zName[0]=='\'' && zName[n-1]=='\'' ){ + zName++; + n -= 2; + } for(i=0; i=count(aType) ) return; + while( blob_token(pContent, &name) && blob_sqltoken(pContent, &value) ){ + char *z = blob_terminate(&name); + if( !safeSql(z) ) return; + if( nToken>0 ){ + for(jj=0; jj=aType[ii].nField ) continue; + }else{ + if( !safeInt(z) ) return; + } + azToken[nToken++] = z; + azToken[nToken++] = z = blob_terminate(&value); + if( !safeSql(z) ) return; + if( nToken>=count(azToken) ) break; + } + if( nToken<2 ) return; + if( aType[ii].zName[0]=='/' ){ + thisMask = configure_is_exportable(azToken[1]); + }else{ + thisMask = configure_is_exportable(aType[ii].zName); + } + if( (thisMask & groupMask)==0 ) return; + + blob_zero(&sql); + if( groupMask & CONFIGSET_OVERWRITE ){ + if( (thisMask & configHasBeenReset)==0 && aType[ii].zName[0]!='/' ){ + db_multi_exec("DELETE FROM %s", &aType[ii].zName[1]); + configHasBeenReset |= thisMask; + } + blob_append(&sql, "REPLACE INTO ", -1); + }else{ + blob_append(&sql, "INSERT OR IGNORE INTO ", -1); + } + blob_appendf(&sql, "%s(%s, mtime", &zName[1], aType[ii].zPrimKey); + for(jj=2; jj=%lld", iStart); + while( db_step(&q)==SQLITE_ROW ){ + blob_appendf(&rec,"%s %s scom %s", + db_column_text(&q, 0), + db_column_text(&q, 1), + db_column_text(&q, 2) + ); + blob_appendf(pOut, "config /shun %d\n%s\n", + blob_size(&rec), blob_str(&rec)); + nCard++; + blob_reset(&rec); + } + db_finalize(&q); + } + if( groupMask & CONFIGSET_USER ){ + db_prepare(&q, "SELECT mtime, quote(login), quote(pw), quote(cap)," + " quote(info), quote(photo) FROM user" + " WHERE mtime>=%lld", iStart); + while( db_step(&q)==SQLITE_ROW ){ + blob_appendf(&rec,"%s %s pw %s cap %s info %s photo %s", + db_column_text(&q, 0), + db_column_text(&q, 1), + db_column_text(&q, 2), + db_column_text(&q, 3), + db_column_text(&q, 4), + db_column_text(&q, 5) + ); + blob_appendf(pOut, "config /user %d\n%s\n", + blob_size(&rec), blob_str(&rec)); + nCard++; + blob_reset(&rec); + } + db_finalize(&q); + } + if( groupMask & CONFIGSET_TKT ){ + db_prepare(&q, "SELECT mtime, quote(title), quote(owner), quote(cols)," + " quote(sqlcode) FROM reportfmt" + " WHERE mtime>=%lld", iStart); + while( db_step(&q)==SQLITE_ROW ){ + blob_appendf(&rec,"%s %s owner %s cols %s sqlcode %s", + db_column_text(&q, 0), + db_column_text(&q, 1), + db_column_text(&q, 2), + db_column_text(&q, 3), + db_column_text(&q, 4) + ); + blob_appendf(pOut, "config /reportfmt %d\n%s\n", + blob_size(&rec), blob_str(&rec)); + nCard++; + blob_reset(&rec); + } + db_finalize(&q); + } + if( groupMask & CONFIGSET_ADDR ){ + db_prepare(&q, "SELECT mtime, quote(hash), quote(content) FROM concealed" + " WHERE mtime>=%lld", iStart); + while( db_step(&q)==SQLITE_ROW ){ + blob_appendf(&rec,"%s %s content %s", + db_column_text(&q, 0), + db_column_text(&q, 1), + db_column_text(&q, 2) + ); + blob_appendf(pOut, "config /concealed %d\n%s\n", + blob_size(&rec), blob_str(&rec)); + nCard++; + blob_reset(&rec); + } + db_finalize(&q); + } + db_prepare(&q, "SELECT mtime, quote(name), quote(value) FROM config" + " WHERE name=:name AND mtime>=%lld", iStart); + for(ii=0; ii #include #include #include +#include #include "db.h" #if INTERFACE /* ** An single SQL statement is represented as an instance of the following @@ -606,10 +607,23 @@ } va_end(ap); sqlite3_exec(db, "COMMIT", 0, 0, 0); sqlite3_close(db); } + +/* +** Function to return the number of seconds since 1970. This is +** the same as strftime('%s','now') but is more compact. +*/ +static void db_now_function( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + sqlite3_result_int64(context, time(0)); +} + /* ** Open a database file. Return a pointer to the new database ** connection. An error results in process abort. */ @@ -630,10 +644,11 @@ if( rc!=SQLITE_OK ){ db_err(sqlite3_errmsg(db)); } sqlite3_busy_timeout(db, 5000); sqlite3_wal_autocheckpoint(db, 1); /* Set to checkpoint frequently */ + sqlite3_create_function(db, "now", 0, SQLITE_ANY, 0, db_now_function, 0, 0); return db; } /* @@ -1105,14 +1120,14 @@ db_set("content-schema", CONTENT_SCHEMA, 0); db_set("aux-schema", AUX_SCHEMA, 0); if( makeServerCodes ){ db_multi_exec( - "INSERT INTO config(name,value)" - " VALUES('server-code', lower(hex(randomblob(20))));" - "INSERT INTO config(name,value)" - " VALUES('project-code', lower(hex(randomblob(20))));" + "INSERT INTO config(name,value,mtime)" + " VALUES('server-code', lower(hex(randomblob(20))),now());" + "INSERT INTO config(name,value,mtime)" + " VALUES('project-code', lower(hex(randomblob(20))),now());" ); } if( !db_is_global("autosync") ) db_set_int("autosync", 1, 0); if( !db_is_global("localauth") ) db_set_int("localauth", 0, 0); db_create_default_users(0, zDefaultUser); @@ -1295,11 +1310,12 @@ sha1sum_step_text(zContent, n); sha1sum_finish(&out); sqlite3_snprintf(sizeof(zHash), zHash, "%s", blob_str(&out)); blob_reset(&out); db_multi_exec( - "INSERT OR IGNORE INTO concealed VALUES(%Q,%#Q)", + "INSERT OR IGNORE INTO concealed(hash,content,mtime)" + " VALUES(%Q,%#Q,now())", zHash, n, zContent ); } return zHash; } @@ -1406,11 +1422,11 @@ db_swap_connections(); db_multi_exec("REPLACE INTO global_config(name,value) VALUES(%Q,%Q)", zName, zValue); db_swap_connections(); }else{ - db_multi_exec("REPLACE INTO config(name,value) VALUES(%Q,%Q)", + db_multi_exec("REPLACE INTO config(name,value,mtime) VALUES(%Q,%Q,now())", zName, zValue); } if( globalFlag && g.repositoryOpen ){ db_multi_exec("DELETE FROM config WHERE name=%Q", zName); } @@ -1465,11 +1481,11 @@ db_swap_connections(); db_multi_exec("REPLACE INTO global_config(name,value) VALUES(%Q,%d)", zName, value); db_swap_connections(); }else{ - db_multi_exec("REPLACE INTO config(name,value) VALUES(%Q,%d)", + db_multi_exec("REPLACE INTO config(name,value,mtime) VALUES(%Q,%d,now())", zName, value); } if( globalFlag && g.repositoryOpen ){ db_multi_exec("DELETE FROM config WHERE name=%Q", zName); } Index: src/login.c ================================================================== --- src/login.c +++ src/login.c @@ -1257,12 +1257,12 @@ db_multi_exec("DETACH other"); /* Propagate the changes to all other members of the login-group */ zSql = mprintf( "BEGIN;" - "REPLACE INTO config(name, value) VALUES('peer-name-%q', %Q);" - "REPLACE INTO config(name, value) VALUES('peer-repo-%q', %Q);" + "REPLACE INTO config(name,value,mtime) VALUES('peer-name-%q',%Q,now());" + "REPLACE INTO config(name,value,mtime) VALUES('peer-repo-%q',%Q,now());" "COMMIT;", zSelfProjCode, zSelfLabel, zSelfProjCode, zSelfRepo ); login_group_sql(zSql, "
  • ", "
  • ", pzErrMsg); fossil_free(zSql); Index: src/rebuild.c ================================================================== --- src/rebuild.c +++ src/rebuild.c @@ -22,13 +22,15 @@ #include #include #include /* -** Schema changes +** Make changes to the stable part of the schema (the part that is not +** simply deleted and reconstructed on a rebuild) to bring the schema +** up to the latest. */ -static const char zSchemaUpdates[] = +static const char zSchemaUpdates1[] = @ -- Index on the delta table @ -- @ CREATE INDEX IF NOT EXISTS delta_i1 ON delta(srcid); @ @ -- Artifacts that should not be processed are identified in the @@ -39,41 +41,124 @@ @ -- @ -- Shunned artifacts do not exist in the blob table. Hence they @ -- have not artifact ID (rid) and we thus must store their full @ -- UUID. @ -- -@ CREATE TABLE IF NOT EXISTS shun(uuid UNIQUE); +@ CREATE TABLE IF NOT EXISTS shun( +@ uuid UNIQUE, -- UUID of artifact to be shunned. Canonical form +@ mtime INTEGER, -- When added. Seconds since 1970 +@ scom TEXT -- Optional text explaining why the shun occurred +@ ); @ @ -- Artifacts that should not be pushed are stored in the "private" @ -- table. @ -- @ CREATE TABLE IF NOT EXISTS private(rid INTEGER PRIMARY KEY); @ -@ -- An entry in this table describes a database query that generates a -@ -- table of tickets. -@ -- -@ CREATE TABLE IF NOT EXISTS reportfmt( -@ rn integer primary key, -- Report number -@ owner text, -- Owner of this report format (not used) -@ title text, -- Title of this report -@ cols text, -- A color-key specification -@ sqlcode text -- An SQL SELECT statement for this report -@ ); -@ @ -- Some ticket content (such as the originators email address or contact @ -- information) needs to be obscured to protect privacy. This is achieved @ -- by storing an SHA1 hash of the content. For display, the hash is @ -- mapped back into the original text using this table. @ -- @ -- This table contains sensitive information and should not be shared @ -- with unauthorized users. @ -- @ CREATE TABLE IF NOT EXISTS concealed( -@ hash TEXT PRIMARY KEY, -@ content TEXT +@ hash TEXT PRIMARY KEY, -- The SHA1 hash of content +@ mtime INTEGER, -- Time created. Seconds since 1970 +@ content TEXT -- Content intended to be concealed +@ ); +; +static const char zSchemaUpdates2[] = +@ -- An entry in this table describes a database query that generates a +@ -- table of tickets. +@ -- +@ CREATE TABLE IF NOT EXISTS reportfmt( +@ rn INTEGER PRIMARY KEY, -- Report number +@ owner TEXT, -- Owner of this report format (not used) +@ title TEXT UNIQUE, -- Title of this report +@ mtime INTEGER, -- Time last modified. Seconds since 1970 +@ cols TEXT, -- A color-key specification +@ sqlcode TEXT -- An SQL SELECT statement for this report @ ); ; + +static void rebuild_update_schema(void){ + int rc; + db_multi_exec(zSchemaUpdates1); + db_multi_exec(zSchemaUpdates2); + + rc = db_exists("SELECT 1 FROM sqlite_master" + " WHERE name='user' AND sql GLOB '* mtime *'"); + if( rc==0 ){ + db_multi_exec( + "CREATE TEMP TABLE temp_user AS SELECT * FROM user;" + "DROP TABLE user;" + "CREATE TABLE user(\n" + " uid INTEGER PRIMARY KEY,\n" + " login TEXT UNIQUE,\n" + " pw TEXT,\n" + " cap TEXT,\n" + " cookie TEXT,\n" + " ipaddr TEXT,\n" + " cexpire DATETIME,\n" + " info TEXT,\n" + " mtime DATE,\n" + " photo BLOB\n" + ");" + "INSERT OR IGNORE INTO user" + " SELECT uid, login, pw, cap, cookie," + " ipaddr, cexpire, info, now(), photo FROM temp_user;" + "DROP TABLE temp_user;" + ); + } + + rc = db_exists("SELECT 1 FROM sqlite_master" + " WHERE name='config' AND sql GLOB '* mtime *'"); + if( rc==0 ){ + db_multi_exec( + "ALTER TABLE config ADD COLUMN mtime INTEGER;" + "UPDATE config SET mtime=now();" + ); + } + + rc = db_exists("SELECT 1 FROM sqlite_master" + " WHERE name='shun' AND sql GLOB '* mtime *'"); + if( rc==0 ){ + db_multi_exec( + "ALTER TABLE shun ADD COLUMN mtime INTEGER;" + "ALTER TABLE shun ADD COLUMN scom TEXT;" + "UPDATE shun SET mtime=now();" + ); + } + + rc = db_exists("SELECT 1 FROM sqlite_master" + " WHERE name='reportfmt' AND sql GLOB '* mtime *'"); + if( rc==0 ){ + db_multi_exec( + "CREATE TEMP TABLE old_fmt AS SELECT * FROM reportfmt;" + "DROP TABLE reportfmt;" + ); + db_multi_exec(zSchemaUpdates2); + db_multi_exec( + "INSERT OR IGNORE INTO reportfmt(rn,owner,title,cols,sqlcode,mtime)" + " SELECT rn, owner, title, cols, sqlcode, now() FROM old_fmt;" + "INSERT OR IGNORE INTO reportfmt(rn,owner,title,cols,sqlcode,mtime)" + " SELECT rn, owner, title || ' (' || rn || ')', cols, sqlcode, now()" + " FROM old_fmt;" + ); + } + + rc = db_exists("SELECT 1 FROM sqlite_master" + " WHERE name='concealed' AND sql GLOB '* mtime *'"); + if( rc==0 ){ + db_multi_exec( + "ALTER TABLE concealed ADD COLUMN mtime INTEGER;" + "UPDATE concealed SET mtime=now();" + ); + } +} /* ** Variables used to store state information about an on-going "rebuild" ** or "deconstruct". */ @@ -256,11 +341,11 @@ ttyOutput = doOut; processCnt = 0; if (!g.fQuiet) { percent_complete(0); } - db_multi_exec(zSchemaUpdates); + rebuild_update_schema(); for(;;){ zTable = db_text(0, "SELECT name FROM sqlite_master /*scan*/" " WHERE type='table'" " AND name NOT IN ('blob','delta','rcvfrom','user'," @@ -459,12 +544,12 @@ } db_begin_transaction(); ttyOutput = 1; errCnt = rebuild_db(randomizeFlag, 1, doClustering); db_multi_exec( - "REPLACE INTO config(name,value) VALUES('content-schema','%s');" - "REPLACE INTO config(name,value) VALUES('aux-schema','%s');", + "REPLACE INTO config(name,value,mtime) VALUES('content-schema','%s',now());" + "REPLACE INTO config(name,value,mtime) VALUES('aux-schema','%s',now());", CONTENT_SCHEMA, AUX_SCHEMA ); if( errCnt && !forceFlag ){ printf("%d errors. Rolling back changes. Use --force to force a commit.\n", errCnt); Index: src/report.c ================================================================== --- src/report.c +++ src/report.c @@ -360,19 +360,25 @@ }else if( (zTitle = trim_string(zTitle))[0]==0 ){ zErr = "Please supply a title"; }else{ zErr = verify_sql_statement(zSQL); } + if( zErr==0 + && db_exists("SELECT 1 FROM reportfmt WHERE title=%Q and rn<>%d", + zTitle, rn) + ){ + zErr = mprintf("There is already another report named \"%h\"", zTitle); + } if( zErr==0 ){ login_verify_csrf_secret(); if( rn>0 ){ db_multi_exec("UPDATE reportfmt SET title=%Q, sqlcode=%Q," - " owner=%Q, cols=%Q WHERE rn=%d", + " owner=%Q, cols=%Q, mtime=now() WHERE rn=%d", zTitle, zSQL, zOwner, zClrKey, rn); }else{ - db_multi_exec("INSERT INTO reportfmt(title,sqlcode,owner,cols) " - "VALUES(%Q,%Q,%Q,%Q)", + db_multi_exec("INSERT INTO reportfmt(title,sqlcode,owner,cols,mtime) " + "VALUES(%Q,%Q,%Q,%Q,now())", zTitle, zSQL, zOwner, zClrKey); rn = db_last_insert_rowid(); } cgi_redirect(mprintf("rptview?rn=%d", rn)); return; Index: src/schema.c ================================================================== --- src/schema.c +++ src/schema.c @@ -39,12 +39,12 @@ ** changes. The aux tables have an arbitrary version number (typically ** a date) which can change frequently. When the content schema changes, ** we have to execute special procedures to update the schema. When ** the aux schema changes, all we need to do is rebuild the database. */ -#define CONTENT_SCHEMA "1" -#define AUX_SCHEMA "2011-02-25 14:52" +#define CONTENT_SCHEMA "2" +#define AUX_SCHEMA "2011-04-25 19:50" #endif /* INTERFACE */ /* @@ -51,21 +51,25 @@ ** The schema for a repository database. ** ** Schema1[] contains parts of the schema that are fixed and unchanging ** across versions. Schema2[] contains parts of the schema that can ** change from one version to the next. The information in Schema2[] -** can be reconstructed from the information in Schema1[]. +** is reconstructed from the information in Schema1[] by the "rebuild" +** operation. */ const char zRepositorySchema1[] = @ -- The BLOB and DELTA tables contain all records held in the repository. @ -- -@ -- The BLOB.CONTENT column is always compressed using libz. This +@ -- The BLOB.CONTENT column is always compressed using zlib. This @ -- column might hold the full text of the record or it might hold @ -- a delta that is able to reconstruct the record from some other @ -- record. If BLOB.CONTENT holds a delta, then a DELTA table entry @ -- will exist for the record and that entry will point to another @ -- entry that holds the source of the delta. Deltas can be chained. +@ -- +@ -- The blob and delta tables collectively hold the "global state" of +@ -- a Fossil repository. @ -- @ CREATE TABLE blob( @ rid INTEGER PRIMARY KEY, -- Record ID @ rcvid INTEGER, -- Origin of this record @ size INTEGER, -- Size of content. -1 for a phantom. @@ -77,17 +81,24 @@ @ rid INTEGER PRIMARY KEY, -- Record ID @ srcid INTEGER NOT NULL REFERENCES blob -- Record holding source document @ ); @ CREATE INDEX delta_i1 ON delta(srcid); @ +@ ------------------------------------------------------------------------- +@ -- The BLOB and DELTA tables above hold the "global state" of a Fossil +@ -- project; the stuff that is normally exchanged during "sync". The +@ -- "local state" of a repository is contained in the remaining tables of +@ -- the zRepositorySchema1 string. +@ ------------------------------------------------------------------------- +@ @ -- Whenever new blobs are received into the repository, an entry @ -- in this table records the source of the blob. @ -- @ CREATE TABLE rcvfrom( @ rcvid INTEGER PRIMARY KEY, -- Received-From ID @ uid INTEGER REFERENCES user, -- User login -@ mtime DATETIME, -- Time or receipt +@ mtime DATETIME, -- Time of receipt. Julian day. @ nonce TEXT UNIQUE, -- Nonce used for login @ ipaddr TEXT -- Remote IP address. NULL for direct. @ ); @ @ -- Information about users @@ -99,26 +110,28 @@ @ -- hash based on the project-code, the user login, and the cleartext @ -- password. @ -- @ CREATE TABLE user( @ uid INTEGER PRIMARY KEY, -- User ID -@ login TEXT, -- login name of the user +@ login TEXT UNIQUE, -- login name of the user @ pw TEXT, -- password @ cap TEXT, -- Capabilities of this user @ cookie TEXT, -- WWW login cookie @ ipaddr TEXT, -- IP address for which cookie is valid @ cexpire DATETIME, -- Time when cookie expires @ info TEXT, -- contact information +@ mtime DATE, -- last change. seconds since 1970 @ photo BLOB -- JPEG image of this user @ ); @ @ -- The VAR table holds miscellanous information about the repository. @ -- in the form of name-value pairs. @ -- @ CREATE TABLE config( @ name TEXT PRIMARY KEY NOT NULL, -- Primary name of the entry @ value CLOB, -- Content of the named parameter +@ mtime DATE, -- last modified. seconds since 1970 @ CHECK( typeof(name)='text' AND length(name)>=1 ) @ ); @ @ -- Artifacts that should not be processed are identified in the @ -- "shun" table. Artifacts that are control-file forgeries or @@ -128,11 +141,15 @@ @ -- @ -- Shunned artifacts do not exist in the blob table. Hence they @ -- have not artifact ID (rid) and we thus must store their full @ -- UUID. @ -- -@ CREATE TABLE shun(uuid UNIQUE); +@ CREATE TABLE shun( +@ uuid UNIQUE, -- UUID of artifact to be shunned. Canonical form +@ mtime DATE, -- When added. seconds since 1970 +@ scom TEXT -- Optional text explaining why the shun occurred +@ ); @ @ -- Artifacts that should not be pushed are stored in the "private" @ -- table. Private artifacts are omitted from the "unclustered" and @ -- "unsent" tables. @ -- @@ -140,17 +157,19 @@ @ @ -- An entry in this table describes a database query that generates a @ -- table of tickets. @ -- @ CREATE TABLE reportfmt( -@ rn integer primary key, -- Report number -@ owner text, -- Owner of this report format (not used) -@ title text, -- Title of this report -@ cols text, -- A color-key specification -@ sqlcode text -- An SQL SELECT statement for this report +@ rn INTEGER PRIMARY KEY, -- Report number +@ owner TEXT, -- Owner of this report format (not used) +@ title TEXT UNIQUE, -- Title of this report +@ mtime DATE, -- Last modified. seconds since 1970 +@ cols TEXT, -- A color-key specification +@ sqlcode TEXT -- An SQL SELECT statement for this report @ ); -@ INSERT INTO reportfmt(title,cols,sqlcode) VALUES('All Tickets','#ffffff Key: +@ INSERT INTO reportfmt(title,mtime,cols,sqlcode) +@ VALUES('All Tickets',julianday('1970-01-01'),'#ffffff Key: @ #f2dcdc Active @ #e8e8e8 Review @ #cfe8bd Fixed @ #bde5d6 Tested @ #cacae5 Deferred @@ -176,12 +195,13 @@ @ -- @ -- This table contains sensitive information and should not be shared @ -- with unauthorized users. @ -- @ CREATE TABLE concealed( -@ hash TEXT PRIMARY KEY, -@ content TEXT +@ hash TEXT PRIMARY KEY, -- The SHA1 hash of content +@ mtime DATE, -- Time created. Seconds since 1970 +@ content TEXT -- Content intended to be concealed @ ); ; const char zRepositorySchema2[] = @ -- Filenames @@ -214,11 +234,11 @@ @ -- @ CREATE TABLE plink( @ pid INTEGER REFERENCES blob, -- Parent manifest @ cid INTEGER REFERENCES blob, -- Child manifest @ isprim BOOLEAN, -- pid is the primary parent of cid -@ mtime DATETIME, -- the date/time stamp on cid +@ mtime DATETIME, -- the date/time stamp on cid. Julian day. @ UNIQUE(pid, cid) @ ); @ CREATE INDEX plink_i2 ON plink(cid,pid); @ @ -- A "leaf" checkin is a checkin that has no children in the same @@ -232,11 +252,11 @@ @ @ -- Events used to generate a timeline @ -- @ CREATE TABLE event( @ type TEXT, -- Type of event: 'ci', 'w', 'e', 't' -@ mtime DATETIME, -- Date and time when the event occurs +@ mtime DATETIME, -- Time of occurrence. Julian day. @ objid INTEGER PRIMARY KEY, -- Associated record ID @ tagid INTEGER, -- Associated ticket or wiki name tag @ uid INTEGER REFERENCES user, -- User who caused the event @ bgcolor TEXT, -- Color set by 'bgcolor' property @ euser TEXT, -- User set by 'user' property @@ -319,11 +339,11 @@ @ tagid INTEGER REFERENCES tag, -- The tag that added or removed @ tagtype INTEGER, -- 0:-,cancel 1:+,single 2:*,propagate @ srcid INTEGER REFERENCES blob, -- Artifact of tag. 0 for propagated tags @ origid INTEGER REFERENCES blob, -- check-in holding propagated tag @ value TEXT, -- Value of the tag. Might be NULL. -@ mtime TIMESTAMP, -- Time of addition or removal +@ mtime TIMESTAMP, -- Time of addition or removal. Julian day @ rid INTEGER REFERENCE blob, -- Artifact tag is applied to @ UNIQUE(rid, tagid) @ ); @ CREATE INDEX tagxref_i1 ON tagxref(tagid, mtime); @ @@ -334,11 +354,11 @@ @ -- @ CREATE TABLE backlink( @ target TEXT, -- Where the hyperlink points to @ srctype INT, -- 0: check-in 1: ticket 2: wiki @ srcid INT, -- rid for checkin or wiki. tkt_id for ticket. -@ mtime TIMESTAMP, -- time that the hyperlink was added +@ mtime TIMESTAMP, -- time that the hyperlink was added. Julian day. @ UNIQUE(target, srctype, srcid) @ ); @ CREATE INDEX backlink_src ON backlink(srcid, srctype); @ @ -- Each attachment is an entry in the following table. Only @@ -345,11 +365,11 @@ @ -- the most recent attachment (identified by the D card) is saved. @ -- @ CREATE TABLE attachment( @ attachid INTEGER PRIMARY KEY, -- Local id for this attachment @ isLatest BOOLEAN DEFAULT 0, -- True if this is the one to use -@ mtime TIMESTAMP, -- Time when attachment last changed +@ mtime TIMESTAMP, -- Last changed. Julian day. @ src TEXT, -- UUID of the attachment. NULL to delete @ target TEXT, -- Object attached to. Wikiname or Tkt UUID @ filename TEXT, -- Filename for the attachment @ comment TEXT, -- Comment associated with this attachment @ user TEXT -- Name of user adding attachment @@ -443,11 +463,11 @@ @ chnged INT DEFAULT 0, -- 0:unchnged 1:edited 2:m-chng 3:m-add @ deleted BOOLEAN DEFAULT 0, -- True if deleted @ isexe BOOLEAN, -- True if file should be executable @ rid INTEGER, -- Originally from this repository record @ mrid INTEGER, -- Based on this record due to a merge -@ mtime INTEGER, -- Modification time of file on disk +@ mtime INTEGER, -- Mtime of file on disk. sec since 1970 @ pathname TEXT, -- Full pathname relative to root @ origname TEXT, -- Original pathname. NULL if unchanged @ UNIQUE(pathname,vid) @ ); @ Index: src/setup.c ================================================================== --- src/setup.c +++ src/setup.c @@ -354,12 +354,12 @@ style_footer(); return; } login_verify_csrf_secret(); db_multi_exec( - "REPLACE INTO user(uid,login,info,pw,cap) " - "VALUES(nullif(%d,0),%Q,%Q,%Q,'%s')", + "REPLACE INTO user(uid,login,info,pw,cap,mtime) " + "VALUES(nullif(%d,0),%Q,%Q,%Q,'%s',now())", uid, P("login"), P("info"), zPw, zCap ); if( atoi(PD("all","0"))>0 ){ Blob sql; char *zErr = 0; @@ -375,11 +375,12 @@ blob_appendf(&sql, "UPDATE user SET login=%Q," " pw=coalesce(shared_secret(%Q,%Q," "(SELECT value FROM config WHERE name='project-code')),pw)," " info=%Q," - " cap=%Q" + " cap=%Q," + " mtime=now()" " WHERE login=%Q;", zLogin, P("pw"), zLogin, P("info"), zCap, zOldLogin ); login_group_sql(blob_str(&sql), "
  • ", "
  • \n", &zErr); @@ -1286,18 +1287,18 @@ if( P("set")!=0 && zMime && zMime[0] && szImg>0 ){ Blob img; Stmt ins; blob_init(&img, aImg, szImg); db_prepare(&ins, - "REPLACE INTO config(name, value)" - " VALUES('logo-image',:bytes)" + "REPLACE INTO config(name,value,mtime)" + " VALUES('logo-image',:bytes,now())" ); db_bind_blob(&ins, ":bytes", &img); db_step(&ins); db_finalize(&ins); db_multi_exec( - "REPLACE INTO config(name, value) VALUES('logo-mimetype',%Q)", + "REPLACE INTO config(name,value,mtime) VALUES('logo-mimetype',%Q,now())", zMime ); db_end_transaction(0); cgi_redirect("setup_logo"); }else if( P("clr")!=0 ){ Index: src/shun.c ================================================================== --- src/shun.c +++ src/shun.c @@ -81,17 +81,31 @@ @ fossil rebuild command-line before the artifact content @ can pulled in from other respositories.

    } } if( zUuid && P("add") ){ + int rid, tagid; login_verify_csrf_secret(); - db_multi_exec("INSERT OR IGNORE INTO shun VALUES('%s')", zUuid); + db_multi_exec( + "INSERT OR IGNORE INTO shun(uuid,mtime)" + " VALUES('%s', now())", zUuid); @

    Artifact @ %s(zUuid) has been @ shunned. It will no longer be pushed. @ It will be removed from the repository the next time the respository @ is rebuilt using the fossil rebuild command-line

    + db_multi_exec("DELETE FROM attachment WHERE src=%Q", zUuid); + rid = db_int(0, "SELECT rid FROM blob WHERE uuid=%Q", zUuid); + if( rid ){ + db_multi_exec("DELETE FROM event WHERE objid=%d", rid); + } + tagid = db_int(0, "SELECT tagid FROM tag WHERE tagname='tkt-%q'", zUuid); + if( tagid ){ + db_multi_exec("DELETE FROM ticket WHERE tkt_uuid=%Q", zUuid); + db_multi_exec("DELETE FROM tag WHERE tagid=%d", tagid); + db_multi_exec("DELETE FROM tagxref WHERE tagid=%d", tagid); + } } @

    A shunned artifact will not be pushed nor accepted in a pull and the @ artifact content will be purged from the repository the next time the @ repository is rebuilt. A list of shunned artifacts can be seen at the @ bottom of this page.

    Index: src/skins.c ================================================================== --- src/skins.c +++ src/skins.c @@ -25,11 +25,12 @@ /* ** A black-and-white theme with the project title in a bar across the top ** and no logo image. */ static const char zBuiltinSkin1[] = -@ REPLACE INTO config VALUES('css','/* General settings for the entire page */ +@ REPLACE INTO config(name,mtime,value) +@ VALUES('css',now(),'/* General settings for the entire page */ @ body { @ margin: 0ex 1ex; @ padding: 0px; @ background-color: white; @ font-family: sans-serif; @@ -152,11 +153,11 @@ @ table.label-value th { @ vertical-align: top; @ text-align: right; @ padding: 0.2ex 2ex; @ }'); -@ REPLACE INTO config VALUES('header',' +@ REPLACE INTO config(name,mtime,value) VALUES('header',now(),' @ @ $<project_name>: $<title> @ @ Login " @ } @ @ '); -@ REPLACE INTO config VALUES('footer',' @ '); -@ REPLACE INTO config VALUES('footer',' @
    @ '); -@ REPLACE INTO config VALUES('footer','
    +@ REPLACE INTO config(name,mtime,value) VALUES('footer',now(),' @ @ @ '); @@ -653,11 +658,12 @@ /* ** Gradients and rounded corners. */ static const char zBuiltinSkin4[] = -@ REPLACE INTO config VALUES('css','/* General settings for the entire page */ +@ REPLACE INTO config(name,mtime,value) +@ VALUES('css',now(),'/* General settings for the entire page */ @ html { @ min-height: 100%; @ } @ body { @ margin: 0ex 1ex; @@ -880,11 +886,11 @@ @ } @ @ textarea { @ font-size: 1em; @ }'); -@ REPLACE INTO config VALUES('header',' +@ REPLACE INTO config(name,mtime,value) VALUES('header',now(),' @ @ $<project_name>: $<title> @ @ Login" @ } @ @
    @ '); -@ REPLACE INTO config VALUES('footer','
    +@ REPLACE INTO config(name,mtime,value) VALUES('footer',now(),' @ @ @ '); @@ -984,17 +990,20 @@ ** Memory to hold the returned string is obtained from malloc. */ static char *getSkin(int useDefault){ Blob val; blob_zero(&val); - blob_appendf(&val, "REPLACE INTO config VALUES('css',%Q);\n", + blob_appendf(&val, + "REPLACE INTO config(name,value,mtime) VALUES('css',%Q,now());\n", useDefault ? zDefaultCSS : db_get("css", (char*)zDefaultCSS) ); - blob_appendf(&val, "REPLACE INTO config VALUES('header',%Q);\n", + blob_appendf(&val, + "REPLACE INTO config(name,value,mtime) VALUES('header',%Q,now());\n", useDefault ? zDefaultHeader : db_get("header", (char*)zDefaultHeader) ); - blob_appendf(&val, "REPLACE INTO config VALUES('footer',%Q);\n", + blob_appendf(&val, + "REPLACE INTO config(name,value,mtime) VALUES('footer',%Q,now());\n", useDefault ? zDefaultFooter : db_get("footer", (char*)zDefaultFooter) ); return blob_str(&val); } @@ -1048,11 +1057,11 @@ if( db_exists("SELECT 1 FROM config WHERE name=%Q", zName) || strcmp(zName, "Default")==0 ){ zErr = mprintf("Skin name \"%h\" already exists. " "Choose a different name.", P("sn")); }else{ - db_multi_exec("INSERT INTO config VALUES(%Q,%Q)", + db_multi_exec("INSERT INTO config(name,value,mtime) VALUES(%Q,%Q,now())", zName, zCurrent ); } } @@ -1069,13 +1078,13 @@ seen = db_exists("SELECT 1 FROM config WHERE name GLOB 'skin:*'" " AND value=%Q", zCurrent); } if( !seen ){ db_multi_exec( - "INSERT INTO config VALUES(" + "INSERT INTO config(name,value,mtime) VALUES(" " strftime('skin:Backup On %%Y-%%m-%%d %%H:%%M:%%S')," - " %Q)", zCurrent + " %Q,now())", zCurrent ); } seen = 0; for(i=0; i=2 && strncmp(g.argv[2],"default",n)==0 ){ user_select(); @@ -249,11 +249,12 @@ } if( blob_size(&pw)==0 ){ printf("password unchanged\n"); }else{ char *zSecret = sha1_shared_secret(blob_str(&pw), g.argv[3], 0); - db_multi_exec("UPDATE user SET pw=%Q WHERE uid=%d", zSecret, uid); + db_multi_exec("UPDATE user SET pw=%Q, mtime=now() WHERE uid=%d", + zSecret, uid); free(zSecret); } }else if( n>=2 && strncmp(g.argv[2],"capabilities",2)==0 ){ int uid; if( g.argc!=4 && g.argc!=5 ){ @@ -263,12 +264,12 @@ if( uid==0 ){ fossil_fatal("no such user: %s", g.argv[3]); } if( g.argc==5 ){ db_multi_exec( - "UPDATE user SET cap=%Q WHERE uid=%d", g.argv[4], - uid + "UPDATE user SET cap=%Q, mtime=now() WHERE uid=%d", + g.argv[4], uid ); } printf("%s\n", db_text(0, "SELECT cap FROM user WHERE uid=%d", uid)); }else{ fossil_panic("user subcommand should be one of: " @@ -340,12 +341,12 @@ db_finalize(&s); } if( g.userUid==0 ){ db_multi_exec( - "INSERT INTO user(login, pw, cap, info)" - "VALUES('anonymous', '', 'cfghjkmnoqw', '')" + "INSERT INTO user(login, pw, cap, info, mtime)" + "VALUES('anonymous', '', 'cfghjkmnoqw', '', now())" ); g.userUid = db_last_insert_rowid(); g.zLogin = "anonymous"; } } @@ -364,11 +365,11 @@ if( g.argc!=3 ) usage("REPOSITORY"); db_open_repository(g.argv[2]); sqlite3_create_function(g.db, "shared_secret", 2, SQLITE_UTF8, 0, sha1_shared_secret_sql_function, 0, 0); db_multi_exec( - "UPDATE user SET pw=shared_secret(pw,login)" + "UPDATE user SET pw=shared_secret(pw,login), mtime=now()" " WHERE length(pw)>0 AND length(pw)!=40" ); } /* Index: src/xfer.c ================================================================== --- src/xfer.c +++ src/xfer.c @@ -454,11 +454,11 @@ " (SELECT uuid FROM delta, blob" " WHERE delta.rid=:rid AND delta.srcid=blob.rid)" " FROM blob" " WHERE rid=:rid" " AND size>=0" - " AND uuid NOT IN shun" + " AND NOT EXISTS(SELECT 1 FROM shun WHERE shun.uuid=blob.uuid)" ); db_bind_int(&q1, ":rid", rid); rc = db_step(&q1); if( rc==SQLITE_ROW ){ zUuid = db_column_text(&q1, 0); @@ -738,13 +738,16 @@ } db_finalize(&q); } /* -** Send a single config card for configuration item zName +** Send a single old-style config card for configuration item zName. +** +** This routine and the functionality it implements is scheduled for +** removal on 2012-05-01. */ -static void send_config_card(Xfer *pXfer, const char *zName){ +static void send_legacy_config_card(Xfer *pXfer, const char *zName){ if( zName[0]!='@' ){ Blob val; blob_zero(&val); db_blob(&val, "SELECT value FROM config WHERE name=%Q", zName); if( blob_size(&val)>0 ){ @@ -760,11 +763,10 @@ blob_appendf(pXfer->pOut, "config %s %d\n%s\n", zName, blob_size(&content), blob_str(&content)); blob_reset(&content); } } - /* ** Called when there is an attempt to transfer private content to and ** from a server without authorization. */ @@ -1007,11 +1009,11 @@ */ if( blob_eq(&xfer.aToken[0], "login") && xfer.nToken==4 ){ if( disableLogin ){ - g.okRead = g.okWrite = g.okPrivate = 1; + g.okRead = g.okWrite = g.okPrivate = g.okAdmin = 1; }else{ if( check_tail_hash(&xfer.aToken[2], xfer.pIn) || check_login(&xfer.aToken[1], &xfer.aToken[2], &xfer.aToken[3]) ){ cgi_reset_content(); @@ -1029,12 +1031,19 @@ if( blob_eq(&xfer.aToken[0], "reqconfig") && xfer.nToken==2 ){ if( g.okRead ){ char *zName = blob_str(&xfer.aToken[1]); - if( configure_is_exportable(zName) ){ - send_config_card(&xfer, zName); + if( zName[0]=='/' ){ + /* New style configuration transfer */ + int groupMask = configure_name_to_mask(&zName[1], 0); + if( !g.okAdmin ) groupMask &= ~CONFIGSET_USER; + if( !g.okRdAddr ) groupMask &= ~CONFIGSET_ADDR; + configure_send_group(xfer.pOut, groupMask, 0); + }else if( configure_is_exportable(zName) ){ + /* Old style configuration transfer */ + send_legacy_config_card(&xfer, zName); } } }else /* config NAME SIZE \n CONTENT @@ -1052,11 +1061,11 @@ cgi_reset_content(); @ error not\sauthorized\sto\spush\sconfiguration nErr++; break; } - if( !recvConfig ){ + if( !recvConfig && zName[0]=='@' ){ configure_prepare_to_receive(0); recvConfig = 1; } configure_receive(zName, &content, CONFIGSET_ALL); blob_reset(&content); @@ -1185,14 +1194,14 @@ ** gdb fossil ** r test-xfer out.txt */ void cmd_test_xfer(void){ int notUsed; + db_find_and_open_repository(0,0); if( g.argc!=2 && g.argc!=3 ){ usage("?MESSAGEFILE?"); } - db_must_be_within_tree(); blob_zero(&g.cgiIn); blob_read_from_file(&g.cgiIn, g.argc==2 ? "-" : g.argv[2]); disableLogin = 1; page_xfer(); printf("%s\n", cgi_extract_content(¬Used)); @@ -1331,25 +1340,32 @@ while( zName ){ blob_appendf(&send, "reqconfig %s\n", zName); zName = configure_next_name(configRcvMask); nCardSent++; } - if( configRcvMask & (CONFIGSET_USER|CONFIGSET_TKT) ){ - configure_prepare_to_receive(0); + if( (configRcvMask & (CONFIGSET_USER|CONFIGSET_TKT))!=0 + && (configRcvMask & CONFIGSET_OLDFORMAT)!=0 + ){ + int overwrite = (configRcvMask & CONFIGSET_OVERWRITE)!=0; + configure_prepare_to_receive(overwrite); } origConfigRcvMask = configRcvMask; configRcvMask = 0; } /* Send configuration parameters being pushed */ if( configSendMask ){ - const char *zName; - zName = configure_first_name(configSendMask); - while( zName ){ - send_config_card(&xfer, zName); - zName = configure_next_name(configSendMask); - nCardSent++; + if( configSendMask & CONFIGSET_OLDFORMAT ){ + const char *zName; + zName = configure_first_name(configSendMask); + while( zName ){ + send_legacy_config_card(&xfer, zName); + zName = configure_next_name(configSendMask); + nCardSent++; + } + }else{ + nCardSent += configure_send_group(xfer.pOut, configSendMask, 0); } configSendMask = 0; } /* Append randomness to the end of the message. This makes all @@ -1647,11 +1663,13 @@ break; } blobarray_reset(xfer.aToken, xfer.nToken); blob_reset(&xfer.line); } - if( origConfigRcvMask & (CONFIGSET_TKT|CONFIGSET_USER) ){ + if( (configRcvMask & (CONFIGSET_USER|CONFIGSET_TKT))!=0 + && (configRcvMask & CONFIGSET_OLDFORMAT)!=0 + ){ configure_finalize_receive(); } origConfigRcvMask = 0; if( nCardRcvd>0 ){ fossil_print(zValueFormat, "Received:",