/*
** 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 checkins 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 a bundle.
**
** 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(
@ peid INTEGER REFERENCES purgeevent ON DELETE CASCADE, -- Purge event
@ uuid TEXT NOT NULL, -- SHA1 hash of the purged artifact
@ sz INT NOT NULL, -- Uncompressed size of the purged artifact
@ data BLOB -- Compressed artifact content
@ );
;
/*
** This routine purges multiple artifacts from the repository, transfering
** 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.
*/
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 */
){
int peid = 0; /* New purgeevent ID */
Stmt q; /* General-use prepared statement */
assert( g.repositoryOpen ); /* Main database must already be open */
add_content_sql_commands(g.db);
db_begin_transaction();
if( purge_baseline_out_from_under_delta(zTab) ){
fossil_fatal("attempt to purge a baseline manifest without also purging "
"all of its deltas");
}
db_multi_exec(zPurgeInit /*works-like:"%w%w"*/,
db_name("repository"), db_name("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 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);
db_multi_exec(
"INSERT INTO purgeitem(peid,uuid,sz,data)"
" SELECT %d, uuid, size, compress(content(uuid))"
" FROM blob WHERE rid IN \"%w\"",
peid, zTab
);
db_multi_exec("DELETE FROM blob WHERE rid 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);
db_end_transaction(0);
return peid;
}
/*
** The TEMP table named zTab contains RIDs for a set of checkins.
**
** Check to see if any checkin 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 checkins in zTab
** are about to be deleted or otherwise made inaccessible. This routine
** is checking to ensure that purging the checkins in zTab will not delete
** a baseline manifest out from under a delta.
*/
int purge_baseline_out_from_under_delta(const char *zTab){
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 checkin
** artifacts. Expand this set (by adding new entries to zTab) to include
** all other artifacts that are used exclusively by the set of checkins in
** the original list.
*/
void find_checkin_associates(const char *zTab){
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
);
/* 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
);
/* 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_event_content(int peid){
Stmt q;
sqlite3_int64 sz1 = 0;
sqlite3_int64 sz2 = 0;
db_prepare(&q, "SELECT uuid, sz, length(data) FROM purgeitem WHERE peid=%d",
peid);
while( db_step(&q)==SQLITE_ROW ){
fossil_print(" %s %10d %10d\n",
db_column_text(&q,0),
db_column_int(&q,1),
db_column_int(&q,2));
sz1 += db_column_int(&q,1);
sz2 += db_column_int(&q,2);
}
db_finalize(&q);
fossil_print(" %40s %10lld %10lld\n", "Total:", sz1, sz2);
}
/*
** COMMAND: purge
**
** The purge command is used to remove content from a repository into a
** "graveyard" and also to show manage the graveyard and optionally restored
** content into the repository from the graveyard.
**
** fossil purge list|ls [-l]
**
** Show the graveyard of prior purges.
**
** fossil purge undo ID
**
** Restore the content previously removed by purge ID.
**
** fossil purge [checkin] TAGS... [--explain]
**
** Move the checkins identified by TAGS and all of their descendants
** out of the repository and into the graveyard. If the --explain option
** is included, then the repository and graveyard are unchanged and
** an explaination of what would have happened is shown instead.
**
** SUMMARY:
** fossil purge [checkin] TAGS... [--explain]
** fossil purge list
** fossil purge undo ID
*/
void purge_cmd(void){
const char *zSubcmd;
int n;
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( strncmp(zSubcmd, "list", n)==0 || strcmp(zSubcmd,"ls")==0 ){
int showDetail = find_option("l","l",0)!=0;
if( db_int(-1,"PRAGMA table_info('purgeevent')")<0 ) return;
db_prepare(&q, "SELECT peid, datetime(ctime,'unixepoch','localtime')"
" 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_event_content(db_column_int(&q,0));
}
}
db_finalize(&q);
}else if( strncmp(zSubcmd, "undo", n)==0 ){
fossil_print("Not yet implemented...\n");
}else{
int explainOnly = find_option("explain",0,0)!=0;
int dryRun = find_option("dry-run",0,0)!=0;
const char *zTag;
int i;
int vid;
int nCkin;
int nArtifact;
verify_all_options();
db_begin_transaction();
i = strncmp(zSubcmd,"checkin",n)==0 ? 3 : 2;
if( i>=g.argc ) usage("[checkin] TAGS... [--explain]");
db_multi_exec("CREATE TEMP TABLE ok(rid INTEGER PRIMARY KEY)");
for(; i<g.argc; i++){
int r = symbolic_name_to_rid(g.argv[i], "ci");
if( r>0 ){
compute_descendants(r, 1000000000);
}else if( r==0 ){
fossil_fatal("not found: %s", g.argv[i]);
}else{
fossil_fatal("ambiguous: %s\n", g.argv[i]);
}
}
vid = db_lget_int("checkout",0);
if( db_exists("SELECT 1 FROM ok WHERE rid=%d",vid) ){
fossil_fatal("cannot purge the current checkout");
}
nCkin = db_int(0, "SELECT count(*) FROM ok");
find_checkin_associates("ok");
nArtifact = db_int(0, "SELECT count(*) FROM ok");
if( explainOnly ){
i = 0;
db_prepare(&q, "SELECT rid FROM ok");
while( db_step(&q)==SQLITE_ROW ){
if( i++ > 0 ) fossil_print("%.78c\n",'-');
whatis_rid(db_column_int(&q,0), 0);
}
db_finalize(&q);
}else{
int peid = purge_artifact_list("ok","");
fossil_print("%d checkins and %d artifacts purged.\n", nCkin, nArtifact);
fossil_print("undoable using \"%s purge undo %d\".\n",
g.nameOfExe, peid);
}
db_end_transaction(explainOnly||dryRun);
}
}