summaryrefslogtreecommitdiff
path: root/src/purge.c
diff options
context:
space:
mode:
authorcashmere <cashmere@autistici.org>2026-09-04 09:05:24 +0200
committercashmere <cashmere@autistici.org>2026-09-04 09:05:24 +0200
commit4f49b16079b27d7ce633619e4cddfdf2bffbc952 (patch)
treec296c1e7f3c3e2f6859c6d3a1b1b208779b89679 /src/purge.c
downloadrelic-master.tar.gz
initial commitHEADmaster
Diffstat (limited to 'src/purge.c')
-rw-r--r--src/purge.c662
1 files changed, 662 insertions, 0 deletions
diff --git a/src/purge.c b/src/purge.c
new file mode 100644
index 0000000..07c8c1d
--- /dev/null
+++ b/src/purge.c
@@ -0,0 +1,662 @@
+/*
+** 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 file contains code used to implement the "purge" command and
+** related functionality for removing check-ins from a repository. It also
+** manages the graveyard of purged content.
+*/
+#include "config.h"
+#include "purge.h"
+#include <assert.h>
+
+/*
+** SQL code used to initialize the schema of the graveyard.
+**
+** The purgeevent table contains one entry for each purge event. For each
+** purge event, multiple artifacts might have been removed. Each removed
+** artifact is stored as an entry in the purgeitem table.
+**
+** The purgeevent and purgeitem tables are not synced, even by the
+** "fossil config" command. They exist only as a backup in case of a
+** mistaken purge or for content recovery in case there is a bug in the
+** purge command.
+*/
+static const char zPurgeInit[] =
+@ CREATE TABLE IF NOT EXISTS "%w".purgeevent(
+@ peid INTEGER PRIMARY KEY, -- Unique ID for the purge event
+@ ctime DATETIME, -- When purge occurred. Seconds since 1970.
+@ pnotes TEXT -- Human-readable notes about the purge event
+@ );
+@ CREATE TABLE IF NOT EXISTS "%w".purgeitem(
+@ piid INTEGER PRIMARY KEY, -- ID for the purge item
+@ peid INTEGER REFERENCES purgeevent ON DELETE CASCADE, -- Purge event
+@ orid INTEGER, -- Original RID before purged
+@ uuid TEXT NOT NULL, -- hash of the purged artifact
+@ srcid INTEGER, -- Basis purgeitem for delta compression
+@ isPrivate BOOLEAN, -- True if artifact was originally private
+@ sz INT NOT NULL, -- Uncompressed size of the purged artifact
+@ desc TEXT, -- Brief description of this artifact
+@ data BLOB -- Compressed artifact content
+@ );
+;
+
+/*
+** Flags for the purge_artifact_list() function.
+*/
+#if INTERFACE
+#define PURGE_MOVETO_GRAVEYARD 0x0001 /* Move artifacts in graveyard */
+#define PURGE_EXPLAIN_ONLY 0x0002 /* Show what would have happened */
+#define PURGE_PRINT_SUMMARY 0x0004 /* Print a summary report at end */
+#endif
+
+/*
+** This routine purges multiple artifacts from the repository, transferring
+** those artifacts into the PURGEITEM table.
+**
+** Prior to invoking this routine, the caller must create a (TEMP) table
+** named zTab that contains the RID of every artifact to be purged.
+**
+** This routine does the following:
+**
+** (1) Create the purgeevent and purgeitem tables, if required
+** (2) Create a new purgeevent
+** (3) Make sure no DELTA table entries depend on purged artifacts
+** (4) Create new purgeitem entries for each purged artifact
+** (5) Remove purged artifacts from the BLOB table
+** (6) Remove references to purged artifacts in the following tables:
+** (a) EVENT
+** (b) PRIVATE
+** (c) MLINK
+** (d) PLINK
+** (e) LEAF
+** (f) UNCLUSTERED
+** (g) UNSENT
+** (h) BACKLINK
+** (i) ATTACHMENT
+** (j) TICKETCHNG
+** (7) If any ticket artifacts were removed (6j) then rebuild the
+** corresponding ticket entries. Possibly remove entries from
+** the ticket table.
+**
+** Steps 1-4 (saving the purged artifacts into the graveyard) are only
+** undertaken if the moveToGraveyard flag is true.
+*/
+int purge_artifact_list(
+ const char *zTab, /* TEMP table containing list of RIDS to be purged */
+ const char *zNote, /* Text of the purgeevent.pnotes field */
+ unsigned purgeFlags /* zero or more PURGE_* flags */
+){
+ int peid = 0; /* New purgeevent ID */
+ Stmt q; /* General-use prepared statement */
+ char *z;
+
+ assert( g.repositoryOpen ); /* Main database must already be open */
+ db_begin_transaction();
+ z = sqlite3_mprintf("IN \"%w\"", zTab);
+ describe_artifacts(z);
+ sqlite3_free(z);
+ describe_artifacts_to_stdout(0, 0);
+
+ /* The explain-only flags causes this routine to list the artifacts
+ ** that would have been purged but to not actually make any changes
+ ** to the repository.
+ */
+ if( purgeFlags & PURGE_EXPLAIN_ONLY ){
+ db_end_transaction(0);
+ return 0;
+ }
+
+ /* Make sure we are not removing a manifest that is the baseline of some
+ ** manifest that is being left behind. This step is not strictly necessary.
+ ** It is just a safety check. */
+ if( purge_baseline_out_from_under_delta(zTab) ){
+ fossil_panic("attempt to purge a baseline manifest without also purging "
+ "all of its deltas");
+ }
+
+ /* Make sure that no delta that is left behind requires a purged artifact
+ ** as its basis. If such artifacts exist, go ahead and undelta them now.
+ */
+ db_prepare(&q, "SELECT rid FROM delta WHERE srcid IN \"%w\""
+ " AND rid NOT IN \"%w\"", zTab, zTab);
+ while( db_step(&q)==SQLITE_ROW ){
+ int rid = db_column_int(&q, 0);
+ content_undelta(rid);
+ verify_before_commit(rid);
+ }
+ db_finalize(&q);
+
+ /* Construct the graveyard and copy the artifacts to be purged into the
+ ** graveyard */
+ if( purgeFlags & PURGE_MOVETO_GRAVEYARD ){
+ db_multi_exec(zPurgeInit /*works-like:"%w%w"*/,
+ "repository", "repository");
+ db_multi_exec(
+ "INSERT INTO purgeevent(ctime,pnotes) VALUES(now(),%Q)", zNote
+ );
+ peid = db_last_insert_rowid();
+ db_prepare(&q, "SELECT rid FROM delta WHERE rid IN \"%w\""
+ " AND srcid NOT IN \"%w\"", zTab, zTab);
+ while( db_step(&q)==SQLITE_ROW ){
+ int rid = db_column_int(&q, 0);
+ content_undelta(rid);
+ }
+ db_finalize(&q);
+ db_multi_exec(
+ "INSERT INTO purgeitem(peid,orid,uuid,sz,isPrivate,desc,data)"
+ " SELECT %d, rid, uuid, size,"
+ " EXISTS(SELECT 1 FROM private WHERE private.rid=blob.rid),"
+ " (SELECT summary FROM description WHERE rid=blob.rid),"
+ " content"
+ " FROM blob WHERE rid IN \"%w\"",
+ peid, zTab
+ );
+ db_multi_exec(
+ "UPDATE purgeitem"
+ " SET srcid=(SELECT piid FROM purgeitem px, delta"
+ " WHERE px.orid=delta.srcid"
+ " AND delta.rid=purgeitem.orid)"
+ " WHERE peid=%d",
+ peid
+ );
+ }
+
+ /* Remove the artifacts being purged. Also remove all references to those
+ ** artifacts from the secondary tables. */
+ db_multi_exec("DELETE FROM blob WHERE rid IN \"%w\"", zTab);
+ db_multi_exec("DELETE FROM delta WHERE rid IN \"%w\"", zTab);
+ db_multi_exec("DELETE FROM delta WHERE srcid IN \"%w\"", zTab);
+ db_multi_exec("DELETE FROM event WHERE objid IN \"%w\"", zTab);
+ db_multi_exec("DELETE FROM private WHERE rid IN \"%w\"", zTab);
+ db_multi_exec("DELETE FROM mlink WHERE mid IN \"%w\"", zTab);
+ db_multi_exec("DELETE FROM plink WHERE pid IN \"%w\"", zTab);
+ db_multi_exec("DELETE FROM plink WHERE cid IN \"%w\"", zTab);
+ db_multi_exec("DELETE FROM leaf WHERE rid IN \"%w\"", zTab);
+ db_multi_exec("DELETE FROM phantom WHERE rid IN \"%w\"", zTab);
+ db_multi_exec("DELETE FROM unclustered WHERE rid IN \"%w\"", zTab);
+ db_multi_exec("DELETE FROM unsent WHERE rid IN \"%w\"", zTab);
+ db_multi_exec("DELETE FROM tagxref"
+ " WHERE rid IN \"%w\""
+ " OR srcid IN \"%w\""
+ " OR origid IN \"%w\"", zTab, zTab, zTab);
+ db_multi_exec("DELETE FROM backlink WHERE srctype=0 AND srcid IN \"%w\"",
+ zTab);
+ db_multi_exec(
+ "CREATE TEMP TABLE \"%w_tickets\" AS"
+ " SELECT DISTINCT tkt_uuid FROM ticket WHERE tkt_id IN"
+ " (SELECT tkt_id FROM ticketchng WHERE tkt_rid IN \"%w\")",
+ zTab, zTab);
+ db_multi_exec("DELETE FROM ticketchng WHERE tkt_rid IN \"%w\"", zTab);
+ db_prepare(&q, "SELECT tkt_uuid FROM \"%w_tickets\"", zTab);
+ while( db_step(&q)==SQLITE_ROW ){
+ ticket_rebuild_entry(db_column_text(&q, 0));
+ }
+ db_finalize(&q);
+ /* db_multi_exec("DROP TABLE \"%w_tickets\"", zTab); */
+
+ /* Mission accomplished */
+ db_end_transaction(0);
+
+ if( purgeFlags & PURGE_PRINT_SUMMARY ){
+ fossil_print("%d artifacts purged\n",
+ db_int(0, "SELECT count(*) FROM \"%w\";", zTab));
+ fossil_print("undoable using \"%s purge undo %d\".\n",
+ g.nameOfExe, peid);
+ }
+ return peid;
+}
+
+/*
+** The TEMP table named zTab contains RIDs for a set of check-ins.
+**
+** Check to see if any check-in in zTab is a baseline manifest for some
+** delta manifest that is not in zTab. Return true if zTab contains a
+** baseline for a delta that is not in zTab.
+**
+** This is a database integrity preservation check. The check-ins in zTab
+** are about to be deleted or otherwise made inaccessible. This routine
+** is checking to ensure that purging the check-ins in zTab will not delete
+** a baseline manifest out from under a delta.
+*/
+int purge_baseline_out_from_under_delta(const char *zTab){
+ if( !db_table_has_column("repository","plink","baseid") ){
+ /* Skip this check if the current database is an older schema that
+ ** does not contain the PLINK.BASEID field. */
+ return 0;
+ }else{
+ return db_int(0,
+ "SELECT 1 FROM plink WHERE baseid IN \"%w\" AND cid NOT IN \"%w\"",
+ zTab, zTab);
+ }
+}
+
+
+/*
+** The TEMP table named zTab contains the RIDs for a set of check-in
+** artifacts. Expand this set (by adding new entries to zTab) to include
+** all other artifacts that are used by the check-ins in
+** the original list.
+**
+** If the bExclusive flag is true, then the set is only expanded by
+** artifacts that are used exclusively by the check-ins in the set.
+** When bExclusive is false, then all artifacts used by the check-ins
+** are added even if those artifacts are also used by other check-ins
+** not in the set.
+**
+** The "fossil publish" command with the (undocumented) --test and
+** --exclusive options can be used for interactive testing of this
+** function.
+*/
+void find_checkin_associates(const char *zTab, int bExclusive){
+ db_begin_transaction();
+
+ /* Compute the set of files that need to be added to zTab */
+ db_multi_exec("CREATE TEMP TABLE \"%w_files\"(fid INTEGER PRIMARY KEY)",zTab);
+ db_multi_exec(
+ "INSERT OR IGNORE INTO \"%w_files\"(fid)"
+ " SELECT fid FROM mlink WHERE fid!=0 AND mid IN \"%w\"",
+ zTab, zTab
+ );
+ if( bExclusive ){
+ /* But take out all files that are referenced by check-ins not in zTab */
+ db_multi_exec(
+ "DELETE FROM \"%w_files\""
+ " WHERE fid IN (SELECT fid FROM mlink"
+ " WHERE fid IN \"%w_files\""
+ " AND mid NOT IN \"%w\")",
+ zTab, zTab, zTab
+ );
+ }
+
+ /* Compute the set of tags that need to be added to zTag */
+ db_multi_exec("CREATE TEMP TABLE \"%w_tags\"(tid INTEGER PRIMARY KEY)",zTab);
+ db_multi_exec(
+ "INSERT OR IGNORE INTO \"%w_tags\"(tid)"
+ " SELECT DISTINCT srcid FROM tagxref WHERE rid in \"%w\" AND srcid!=0",
+ zTab, zTab
+ );
+ if( bExclusive ){
+ /* But take out tags that references some check-ins in zTab and other
+ ** check-ins not in zTab. The current Fossil implementation never creates
+ ** such tags, so the following should usually be a no-op. But the file
+ ** format specification allows such tags, so we should check for them.
+ */
+ db_multi_exec(
+ "DELETE FROM \"%w_tags\""
+ " WHERE tid IN (SELECT srcid FROM tagxref"
+ " WHERE srcid IN \"%w_tags\""
+ " AND rid NOT IN \"%w\")",
+ zTab, zTab, zTab
+ );
+ }
+
+ /* Transfer the extra artifacts into zTab */
+ db_multi_exec(
+ "INSERT OR IGNORE INTO \"%w\" SELECT fid FROM \"%w_files\";"
+ "INSERT OR IGNORE INTO \"%w\" SELECT tid FROM \"%w_tags\";"
+ "DROP TABLE \"%w_files\";"
+ "DROP TABLE \"%w_tags\";",
+ zTab, zTab, zTab, zTab, zTab, zTab
+ );
+
+ db_end_transaction(0);
+}
+
+/*
+** Display the content of a single purge event.
+*/
+static void purge_list_event_content(int peid){
+ Stmt q;
+ sqlite3_int64 sz = 0;
+ db_prepare(&q, "SELECT piid, substr(uuid,1,16), srcid, isPrivate,"
+ " length(data), desc"
+ " FROM purgeitem WHERE peid=%d", peid);
+ while( db_step(&q)==SQLITE_ROW ){
+ fossil_print(" %5d %s %4s %c %10d %s\n",
+ db_column_int(&q,0),
+ db_column_text(&q,1),
+ db_column_text(&q,2),
+ db_column_int(&q,3) ? 'P' : ' ',
+ db_column_int(&q,4),
+ db_column_text(&q,5));
+ sz += db_column_int(&q,4);
+ }
+ db_finalize(&q);
+ fossil_print("%.11c%16s%.8c%10lld\n", ' ', "Total:", ' ', sz);
+}
+
+/*
+** Extract the content for purgeitem number piid into a Blob. Return
+** the number of errors.
+*/
+static int purge_extract_item(
+ int piid, /* ID of the item to extract */
+ Blob *pOut /* Write the content into this blob */
+){
+ Stmt q;
+ int srcid;
+ Blob h1, x;
+ static Bag busy;
+
+ db_prepare(&q, "SELECT uuid, srcid, data FROM purgeitem"
+ " WHERE piid=%d", piid);
+ if( db_step(&q)!=SQLITE_ROW ){
+ db_finalize(&q);
+ fossil_fatal("missing purge-item %d", piid);
+ }
+ if( bag_find(&busy, piid) ) return 1;
+ srcid = db_column_int(&q, 1);
+ blob_zero(pOut);
+ blob_zero(&x);
+ db_column_blob(&q, 2, &x);
+ blob_uncompress(&x, pOut);
+ blob_reset(&x);
+ if( srcid>0 ){
+ Blob baseline, out;
+ bag_insert(&busy, piid);
+ purge_extract_item(srcid, &baseline);
+ blob_zero(&out);
+ blob_delta_apply(&baseline, pOut, &out);
+ blob_reset(pOut);
+ *pOut = out;
+ blob_reset(&baseline);
+ }
+ bag_remove(&busy, piid);
+ blob_zero(&h1);
+ db_column_blob(&q, 0, &h1);
+ if( hname_verify_hash(pOut, blob_buffer(&h1), blob_size(&h1))==0 ){
+ fossil_fatal("incorrect artifact hash on %b", &h1);
+ }
+ blob_reset(&h1);
+ db_finalize(&q);
+ return 0;
+}
+
+/*
+** There is a TEMP table ix(piid,srcid) containing a set of purgeitems
+** that need to be transferred to the BLOB table. This routine does
+** all items that have srcid=iSrc. The pBasis blob holds the content
+** of the source document if iSrc>0.
+*/
+static void purge_item_resurrect(int iSrc, Blob *pBasis){
+ Stmt q;
+ static Bag busy;
+ assert( pBasis!=0 || iSrc==0 );
+ if( iSrc>0 ){
+ if( bag_find(&busy, iSrc) ){
+ fossil_fatal("delta loop while uncompressing purged artifacts");
+ }
+ bag_insert(&busy, iSrc);
+ }
+ db_prepare(&q,
+ "SELECT uuid, data, isPrivate, ix.piid"
+ " FROM ix, purgeitem"
+ " WHERE ix.srcid=%d"
+ " AND ix.piid=purgeitem.piid;",
+ iSrc
+ );
+ while( db_step(&q)==SQLITE_ROW ){
+ Blob h1, c1, c2;
+ int isPriv, rid;
+ blob_zero(&h1);
+ db_column_blob(&q, 0, &h1);
+ blob_zero(&c1);
+ db_column_blob(&q, 1, &c1);
+ blob_uncompress(&c1, &c1);
+ blob_zero(&c2);
+ if( pBasis ){
+ blob_delta_apply(pBasis, &c1, &c2);
+ blob_reset(&c1);
+ }else{
+ c2 = c1;
+ }
+ if( hname_verify_hash(&c2, blob_buffer(&h1), blob_size(&h1))==0 ){
+ fossil_fatal("incorrect hash on %b", &h1);
+ }
+ isPriv = db_column_int(&q, 2);
+ rid = content_put_ex(&c2, blob_str(&h1), 0, 0, isPriv);
+ if( rid==0 ){
+ fossil_fatal("%s", g.zErrMsg);
+ }else{
+ if( !isPriv ) content_make_public(rid);
+ content_get(rid, &c1);
+ manifest_crosslink(rid, &c1, MC_NO_ERRORS);
+ }
+ purge_item_resurrect(db_column_int(&q,3), &c2);
+ blob_reset(&c2);
+ }
+ db_finalize(&q);
+ if( iSrc>0 ) bag_remove(&busy, iSrc);
+}
+
+/*
+** COMMAND: purge*
+**
+** The purge command removes content from a repository and stores that content
+** in a "graveyard". The graveyard exists so that content can be recovered
+** using the "fossil purge undo" command. The "fossil purge obliterate"
+** command empties the graveyard, making the content unrecoverable.
+**
+** WARNING: This command can potentially destroy historical data and
+** leave your repository in a goofy state. Know what you are doing!
+** Make a backup of your repository before using this command!
+**
+** FURTHER WARNING: This command is a work-in-progress and may yet
+** contain bugs.
+**
+** > fossil purge artifacts HASH... ?OPTIONS?
+**
+** Move arbitrary artifacts identified by the HASH list into the
+** graveyard.
+**
+** > fossil purge cat HASH...
+**
+** Write the content of one or more artifacts in the graveyard onto
+** standard output.
+**
+** > fossil purge checkins TAGS... ?OPTIONS?
+**
+** Move the check-ins or branches identified by TAGS and all of
+** their descendants out of the repository and into the graveyard.
+** If TAGS includes a branch name then it means all the check-ins
+** on the most recent occurrence of that branch.
+**
+** > fossil purge files NAME ... ?OPTIONS?
+**
+** Move all instances of files called NAME into the graveyard.
+** NAME should be the name of the file relative to the root of the
+** repository. If NAME is a directory, then all files within that
+** directory are moved.
+**
+** > fossil purge list|ls ?-l?
+**
+** Show the graveyard of prior purges. The -l option gives more
+** detail in the output.
+**
+** > fossil purge obliterate ID... ?--force?
+**
+** Remove one or more purge events from the graveyard. Once a purge
+** event is obliterated, it can no longer be undone. The --force
+** option suppresses the confirmation prompt.
+**
+** > fossil purge tickets NAME ... ?OPTIONS?
+**
+** TBD...
+**
+** > fossil purge undo ID
+**
+** Restore the content previously removed by purge ID.
+**
+** > fossil purge wiki NAME ... ?OPTIONS?
+**
+** TBD...
+**
+** COMMON OPTIONS:
+**
+** --explain Make no changes, but show what would happen
+** --dry-run An alias for --explain
+*/
+void purge_cmd(void){
+ int purgeFlags = PURGE_MOVETO_GRAVEYARD | PURGE_PRINT_SUMMARY;
+ const char *zSubcmd;
+ int n;
+ int i;
+ Stmt q;
+
+ if( g.argc<3 ) usage("SUBCOMMAND ?ARGS?");
+ zSubcmd = g.argv[2];
+ db_find_and_open_repository(0,0);
+ n = (int)strlen(zSubcmd);
+ if( find_option("explain",0,0)!=0 || find_option("dry-run",0,0)!=0 ){
+ purgeFlags |= PURGE_EXPLAIN_ONLY;
+ }
+ if( strncmp(zSubcmd, "artifacts", n)==0 ){
+ verify_all_options();
+ db_begin_transaction();
+ db_multi_exec("CREATE TEMP TABLE ok(rid INTEGER PRIMARY KEY)");
+ for(i=3; i<g.argc; i++){
+ int r = name_to_typed_rid(g.argv[i], "");
+ db_multi_exec("INSERT OR IGNORE INTO ok(rid) VALUES(%d);", r);
+ }
+ describe_artifacts_to_stdout("IN ok", 0);
+ purge_artifact_list("ok", "", purgeFlags);
+ db_end_transaction(0);
+ }else if( strncmp(zSubcmd, "cat", n)==0 ){
+ int i, piid;
+ Blob content;
+ if( g.argc<4 ) usage("cat HASH...");
+ for(i=3; i<g.argc; i++){
+ piid = db_int(0, "SELECT piid FROM purgeitem WHERE uuid LIKE '%q%%'",
+ g.argv[i]);
+ if( piid==0 ) fossil_fatal("no such item: %s", g.argv[3]);
+ purge_extract_item(piid, &content);
+ blob_write_to_file(&content, "-");
+ blob_reset(&content);
+ }
+ }else if( strncmp(zSubcmd, "checkins", n)==0 ){
+ int vid;
+ if( find_option("explain",0,0)!=0 || find_option("dry-run",0,0)!=0 ){
+ purgeFlags |= PURGE_EXPLAIN_ONLY;
+ }
+ verify_all_options();
+ db_begin_transaction();
+ if( g.argc<=3 ) usage("checkins TAGS... [OPTIONS]");
+ db_multi_exec("CREATE TEMP TABLE ok(rid INTEGER PRIMARY KEY)");
+ for(i=3; i<g.argc; i++){
+ int r = name_to_typed_rid(g.argv[i], "br");
+ compute_descendants(r, 1000000000);
+ }
+ vid = db_lget_int("checkout",0);
+ if( db_exists("SELECT 1 FROM ok WHERE rid=%d",vid) ){
+ fossil_fatal("cannot purge the current check-out");
+ }
+ find_checkin_associates("ok", 1);
+ purge_artifact_list("ok", "", purgeFlags);
+ db_end_transaction(0);
+ }else if( strncmp(zSubcmd, "files", n)==0 ){
+ verify_all_options();
+ db_begin_transaction();
+ db_multi_exec("CREATE TEMP TABLE ok(rid INTEGER PRIMARY KEY)");
+ for(i=3; i<g.argc; i++){
+ db_multi_exec(
+ "INSERT OR IGNORE INTO ok(rid) "
+ " SELECT fid FROM mlink, filename"
+ " WHERE mlink.fnid=filename.fnid"
+ " AND (filename.name=%Q OR filename.name GLOB '%q/*')",
+ g.argv[i], g.argv[i]
+ );
+ }
+ purge_artifact_list("ok", "", purgeFlags);
+ db_end_transaction(0);
+ }else if( strncmp(zSubcmd, "list", n)==0 || strcmp(zSubcmd,"ls")==0 ){
+ int showDetail = find_option("l","l",0)!=0;
+ if( !db_table_exists("repository","purgeevent") ) return;
+ db_prepare(&q, "SELECT peid, datetime(ctime,'unixepoch',toLocal())"
+ " FROM purgeevent");
+ while( db_step(&q)==SQLITE_ROW ){
+ fossil_print("%4d on %s\n", db_column_int(&q,0), db_column_text(&q,1));
+ if( showDetail ){
+ purge_list_event_content(db_column_int(&q,0));
+ }
+ }
+ db_finalize(&q);
+ }else if( strncmp(zSubcmd, "obliterate", n)==0 ){
+ int i;
+ int bForce = find_option("force","f",0)!=0;
+ if( g.argc<4 ) usage("obliterate ID...");
+ if( !bForce ){
+ Blob ans;
+ char cReply;
+ prompt_user(
+ "Obliterating the graveyard will permanently delete information.\n"
+ "Changes cannot be undone. Continue (y/N)? ", &ans);
+ cReply = blob_str(&ans)[0];
+ if( cReply!='y' && cReply!='Y' ){
+ fossil_exit(1);
+ }
+ }
+ db_begin_transaction();
+ for(i=3; i<g.argc; i++){
+ int peid = atoi(g.argv[i]);
+ if( !db_exists("SELECT 1 FROM purgeevent WHERE peid=%d",peid) ){
+ fossil_fatal("no such purge event: %s", g.argv[i]);
+ }
+ db_multi_exec(
+ "DELETE FROM purgeevent WHERE peid=%d;"
+ "DELETE FROM purgeitem WHERE peid=%d;",
+ peid, peid
+ );
+ }
+ db_end_transaction(0);
+ }else if( strncmp(zSubcmd, "tickets", n)==0 ){
+ fossil_fatal("not yet implemented....");
+ }else if( strncmp(zSubcmd, "undo", n)==0 ){
+ int peid;
+ if( g.argc!=4 ) usage("undo ID");
+ peid = atoi(g.argv[3]);
+ if( (purgeFlags & PURGE_EXPLAIN_ONLY)==0 ){
+ db_begin_transaction();
+ db_multi_exec(
+ "CREATE TEMP TABLE ix("
+ " piid INTEGER PRIMARY KEY,"
+ " srcid INTEGER"
+ ");"
+ "CREATE INDEX ixsrcid ON ix(srcid);"
+ "INSERT INTO ix(piid,srcid) "
+ " SELECT piid, coalesce(srcid,0) FROM purgeitem WHERE peid=%d;",
+ peid
+ );
+ db_multi_exec(
+ "DELETE FROM shun"
+ " WHERE uuid IN (SELECT uuid FROM purgeitem WHERE peid=%d);",
+ peid
+ );
+ manifest_crosslink_begin();
+ purge_item_resurrect(0, 0);
+ manifest_crosslink_end(0);
+ db_multi_exec("DELETE FROM purgeevent WHERE peid=%d", peid);
+ db_multi_exec("DELETE FROM purgeitem WHERE peid=%d", peid);
+ db_end_transaction(0);
+ }
+ }else if( strncmp(zSubcmd, "wiki", n)==0 ){
+ fossil_fatal("not yet implemented....");
+ }else{
+ fossil_fatal("unknown subcommand \"%s\".\n"
+ "should be one of: cat, checkins, files, list, obliterate,"
+ " tickets, undo, wiki", zSubcmd);
+ }
+}