I don't expect anyone to review the whole of this, but any comments
would be welcome. It's about time I posted what I'm up to. It's not a
"finished" patch, by any stretch of the imagination, so please don't
commit it.

[[[
For Obliterate, teach the BDB FS implementation to duplicate an existing
revision into a new transaction, with a new txn-id but otherwise
identical. The purpose is to be able to modify the new txn (delete
one or more nodes out of it) and then replace the old one with it.

* subversion/libsvn_fs_base/revs-txns.c
  (rep_dup, copy_dup, node_rev_dup): New functions to make a deep copy
    of a rep, a copy or a node-rev, while also changing any references
    to the old txn-id.
  (begin_txn_args): Rename the 'rev' member to 'based_on_rev' for
    clarity.
  (txn_body_begin_txn, svn_fs_base__begin_txn): Adjust accordingly.
  (txn_body_begin_obliteration_txn): Add code to make a recursive
    duplicate of all the contents of the txn. Add documentation.
  (svn_fs_base__begin_obliteration_txn): Return success, because it is
    now implemented. Rename 'rev' to 'replacing_rev'. Add documentation.

* subversion/libsvn_fs_base/revs-txns.h
  (svn_fs_base__get_txn_ids): Fix a typo in the doc string.
  (svn_fs_base__begin_obliteration_txn): Rename the 'rev' argument to
    'replacing_rev' for clarity.

* subversion/libsvn_fs_base/tree.c
  (svn_fs_base__commit_obliteration_txn):
    ### Just return success, for testing, even though not implemented yet.

* subversion/tests/cmdline/obliterate_tests.py
  New file, with a skeleton of a test for obliterate. So far, this just
  exercises the obliterate operation and doesn't actually check the
  result.

* subversion/tests/cmdline/svntest/objects.py
  New file, with classes for easily performing common testing steps on a
  repository and on a WC.
  ### The repos class "dump" function calls an external script in my home dir.

=====

Concerns/unfinished business:

  - Should I be using the DAG layer to do these manipulations? Are there
    DAG functions that already do such duplication, or part of it? I
    found some that were useful, but don't understand what the
    significant difference is between the DAG layer and other levels.
    (I did read the description in <> section "".)

  - All the "dup" functions could take the old-txn-id as a parameter, and
    only change txn-ids that match it. If there are any txn-id fields that
    refer to the current txn in some cases but not in other cases, then
    we need this, and need to compare with the old-txn-id to decide whether
    to change it. Are there any such cases?

  - I don't know whether I should change the copy-ids. I don't think I
    should, because future revs refer to them, but I can't dup the copies
    table rows in the same way unless I give them new copy-ids. If
    necessary, I could possibly dup them to new ids and then translate
    the new ids back to the old ones at "commit-obliteration" time.

  - In a normal txn, are the "changes" rows used and updated during txn
    building, or only filled in at commit time? If the former, I need to
    dup them at begin-obliteration-txn time; if the latter, at
    commit-obliteration-txn time.

  - In "rep_dup()", when I dup a child rep in a "rep window" (or "delta
    chunk"), I need to set an "offset" field. I expect there's a
    function I should be using to ensure that gets set correctly.
]]]

- Julian


* subversion/libsvn_fs_base/revs-txns.c
  (rep_dup, copy_dup, node_rev_dup): New functions.
  (begin_txn_args): Rename the 'rev' member to 'based_on_rev' for
    clarity.
  (txn_body_begin_txn, svn_fs_base__begin_txn): Adjust accordingly.
  (txn_body_begin_obliteration_txn): Add code to make a recursive
    duplicate of all the contents of the txn. Add documentation.
  (svn_fs_base__begin_obliteration_txn): Return success, because it is
    now implemented. Rename 'rev' to 'replacing_rev'. Add documentation.

* subversion/libsvn_fs_base/revs-txns.h
  (svn_fs_base__get_txn_ids): Fix a typo in the doc string.
  (svn_fs_base__begin_obliteration_txn): Rename the 'rev' argument to
    'replacing_rev' for clarity.

* subversion/libsvn_fs_base/tree.c
  (svn_fs_base__commit_obliteration_txn): 
    ### Just return success, for testing, even though not implemented yet.

* subversion/tests/cmdline/obliterate_tests.py
  New file, with a skeleton of a test for obliterate. So far, this just
  exercises the obliterate operation and doesn't actually check the
  result.

* subversion/tests/cmdline/svntest/objects.py
  New file, with classes for easily performing common testing steps on a
  repository and on a WC.
--This line, and those below, will be ignored--

Index: subversion/libsvn_fs_base/revs-txns.c
===================================================================
--- subversion/libsvn_fs_base/revs-txns.c	(revision 887172)
+++ subversion/libsvn_fs_base/revs-txns.c	(working copy)
@@ -42,8 +42,11 @@
 #include "id.h"
 #include "bdb/rev-table.h"
 #include "bdb/txn-table.h"
+#include "bdb/nodes-table.h"
+#include "bdb/reps-table.h"
 #include "bdb/copies-table.h"
 #include "bdb/changes-table.h"
+#include "bdb/strings-table.h"
 #include "../libsvn_fs/fs-loader.h"
 
 #include "svn_private_config.h"
@@ -626,6 +629,171 @@ static txn_vtable_t txn_vtable = {
 };
 
 
+/* Create a new representation that is a duplicate of the one keyed by KEY,
+ * but make the duplicate refer to TXN_ID2.
+ * Set *NEW_KEY to the key of the new representation.
+ * Work within TRAIL within FS. */
+static svn_error_t *
+rep_dup(const char **new_key,
+        const char *txn_id2,
+        const char *key,
+        trail_t *trail,
+        apr_pool_t *pool)
+{
+  representation_t *rep;
+
+  SVN_ERR(svn_fs_bdb__read_rep(&rep, trail->fs, key, trail, pool));
+
+  rep->txn_id = txn_id2;
+
+  /* Dup the strings and any recursively used representations */
+  if (rep->kind == rep_kind_fulltext)
+    {
+      SVN_ERR(svn_fs_bdb__string_copy(trail->fs, &rep->contents.fulltext.string_key,
+                                      rep->contents.fulltext.string_key,
+                                      trail, pool));
+    }
+  else
+    {
+      apr_array_header_t *chunks = rep->contents.delta.chunks;
+      int i;
+
+      for (i = 0; i < chunks->nelts; i++)
+      {
+        rep_delta_chunk_t *w = APR_ARRAY_IDX(chunks, i, rep_delta_chunk_t *);
+
+        SVN_ERR(svn_fs_bdb__string_copy(trail->fs, &w->string_key, w->string_key,
+                                        trail, pool));
+        SVN_ERR(rep_dup(&w->rep_key, txn_id2, w->rep_key, trail, pool));
+        /* ### w->offset = calc_offset(w->rep_key); */
+      }
+    }
+
+  SVN_ERR(svn_fs_bdb__write_new_rep(new_key, trail->fs, rep, trail, pool));
+  return SVN_NO_ERROR;
+}
+
+/*
+def change_dup_body(change1, txn_id2):
+  change2 = dup(change1)
+  change2.node_rev_id.txn = txn_id2
+  return change2
+
+def changes_dup(txn_id1, txn_id2):
+  for change1 in changes<txn_id1>:
+    changes.add_row<txn_id2>(change_dup_body(change1, txn_id2))
+*/
+
+/* Make a deep copy of the row keyed by OLD_COPY_ID in the "copies" table,
+ * and change any references (### to a specific old-txn-id) to TXN_ID2.
+ * (Specifically: change the txn_id component of the row's dst_noderev_id
+ * to TXN_ID2.)
+ * Set *NEW_COPY_ID to the key of the new row.
+ * Allocate *NEW_COPY_ID in RESULT_POOL. */
+static svn_error_t *
+copy_dup(const char **new_copy_id,
+         const char *old_copy_id,
+         const char *txn_id2,
+         trail_t *trail,
+         apr_pool_t *result_pool,
+         apr_pool_t *scratch_pool)
+{
+  copy_t *copy;
+  const char *node_id, *copy_id;
+
+  /* Get the old copy */
+  SVN_ERR(svn_fs_bdb__get_copy(&copy, trail->fs, old_copy_id, trail,
+                               scratch_pool));
+
+  /* Modify it: change dst_noderev_id's txn_id to TXN_ID2 */
+  node_id = svn_fs_base__id_node_id(copy->dst_noderev_id);
+  copy_id = svn_fs_base__id_copy_id(copy->dst_noderev_id);
+  copy->dst_noderev_id = svn_fs_base__id_create(node_id, copy_id, txn_id2,
+                                                scratch_pool);
+
+  /* Save the new copy */
+  SVN_ERR(svn_fs_bdb__reserve_copy_id(new_copy_id, trail->fs, trail,
+                                      result_pool));
+  SVN_ERR(svn_fs_bdb__create_copy(trail->fs, *new_copy_id, copy->src_path,
+                                  copy->src_txn_id, copy->dst_noderev_id,
+                                  copy->kind, trail, scratch_pool));
+  return SVN_NO_ERROR;
+}
+
+/* Create a new node-rev that is a deep copy of that keyed by OLD_ID,
+ * except ###
+ * The new node-rev-id is OLD_ID except with the txn-id field changed to TXN_ID2.
+ * Set *NEW_ID to the new node-rev-id, allocated in RESULT_POOL.
+ * Work within TRAIL within FS.
+ *
+ * ### Use svn_fs_base__dag_copy() instead?
+ */
+static svn_error_t *
+node_rev_dup(const svn_fs_id_t **new_id,
+             const char *txn_id2,
+             const svn_fs_id_t *old_id,
+             trail_t *trail,
+             apr_pool_t *result_pool,
+             apr_pool_t *scratch_pool)
+{
+  node_revision_t *noderev;
+  const char *node_id, *copy_id, *txn_id;
+  svn_fs_id_t *id2;
+
+  SVN_ERR(svn_fs_bdb__get_node_revision(&noderev, trail->fs, old_id, trail,
+                                        scratch_pool));
+
+  /* Set ID2 to ID except with txn_id TXN_ID2 */
+  node_id = svn_fs_base__id_node_id(old_id);
+  copy_id = svn_fs_base__id_copy_id(old_id);
+  txn_id = svn_fs_base__id_txn_id(old_id);
+  id2 = svn_fs_base__id_create(node_id, copy_id, txn_id2, result_pool);
+
+  /* Dup the representation of its text or entries, and recurse to dup the
+   * node-revs of any children. */
+  if (noderev->kind == svn_node_dir)
+    {
+      dag_node_t *dag_node;
+      apr_hash_t *entries;
+      apr_hash_index_t *hi;
+
+      /* Get the children */
+      SVN_ERR(svn_fs_base__dag_get_node(&dag_node, trail->fs, old_id, trail,
+                                        trail->pool));
+      SVN_ERR(svn_fs_base__dag_dir_entries(&entries, dag_node, trail,
+                                           scratch_pool));
+
+      /* Dup the children (recursing) */
+      for (hi = apr_hash_first(scratch_pool, entries); hi;
+           hi = apr_hash_next(hi))
+        {
+          const char *child_name = svn_apr_hash_index_key(hi);
+          svn_fs_dirent_t *child_entry = svn_apr_hash_index_val(hi);
+          const svn_fs_id_t *new_noderev_id;
+
+          SVN_ERR(node_rev_dup(&new_noderev_id, txn_id2, child_entry->id,
+                               trail, scratch_pool, scratch_pool));
+          SVN_ERR(svn_fs_base__dag_set_entry(dag_node, child_name,
+                                             new_noderev_id, txn_id, trail,
+                                             scratch_pool));
+          /* ### Use instead: svn_fs_base__dag_clone_child() ? */
+        }
+    }
+  else
+    {
+      SVN_ERR(rep_dup(&noderev->data_key, txn_id2,
+                      noderev->data_key, trail, result_pool));
+      noderev->data_key_uniquifier = NULL;  /* ### ? */
+    }
+
+  SVN_ERR(svn_fs_bdb__put_node_revision(trail->fs, id2, noderev, trail,
+                                        scratch_pool));
+
+  *new_id = id2;
+  return SVN_NO_ERROR;
+}
+
+
 /* Allocate and return a new transaction object in POOL for FS whose
    transaction ID is ID.  ID is not copied.  */
 static svn_fs_txn_t *
@@ -649,7 +817,7 @@ make_txn(svn_fs_t *fs,
 struct begin_txn_args
 {
   svn_fs_txn_t **txn_p;
-  svn_revnum_t rev;
+  svn_revnum_t based_on_rev;
   apr_uint32_t flags;
 };
 
@@ -661,7 +829,7 @@ txn_body_begin_txn(void *baton, trail_t 
   const svn_fs_id_t *root_id;
   const char *txn_id;
 
-  SVN_ERR(svn_fs_base__rev_get_root(&root_id, trail->fs, args->rev,
+  SVN_ERR(svn_fs_base__rev_get_root(&root_id, trail->fs, args->based_on_rev,
                                     trail, trail->pool));
   SVN_ERR(svn_fs_bdb__create_txn(&txn_id, trail->fs, root_id,
                                  trail, trail->pool));
@@ -688,29 +856,118 @@ txn_body_begin_txn(void *baton, trail_t 
       SVN_ERR(txn_body_change_txn_prop(&cpargs, trail));
     }
 
-  *args->txn_p = make_txn(trail->fs, txn_id, args->rev, trail->pool);
+  *args->txn_p = make_txn(trail->fs, txn_id, args->based_on_rev, trail->pool);
   return SVN_NO_ERROR;
 }
 
+/* Create a new empty transaction that is able to become a replacement for
+ * an existing revision.
+ *
+ * This is like txn_body_begin_txn() except it does not support the
+ * CHECK_OOD or CHECK_LOCKS flags.
+ *
+ * ### The first part of this function does nothing that txn_body_begin_txn()
+ * can't do.
+ *
+ * Set BATON->args->txn_p to point to the new transaction.
+ * BATON->args->based_on_rev is the revision on which the existing revision
+ * is based, i.e. one less than the number of the revision to be replaced.
+ * BATON->args->flags must be 0: specifically, the CHECK_OOD and CHECK_LOCKS
+ * flags are not supported.
+ * BATON is of type (struct begin_txn_args *).
+ */
 static svn_error_t *
 txn_body_begin_obliteration_txn(void *baton, trail_t *trail)
 {
   struct begin_txn_args *args = baton;
-  const svn_fs_id_t *root_id;
-  const char *txn_id;
-
-  SVN_ERR(svn_fs_base__rev_get_root(&root_id, trail->fs, args->rev,
-                                    trail, trail->pool));
-  SVN_ERR(svn_fs_bdb__create_txn(&txn_id, trail->fs, root_id,
+  int replacing_rev = args->based_on_rev + 1;
+  const svn_fs_id_t *base_root_id;
+  const char *old_txn_id, *new_txn_id;
+  transaction_t *old_txn, *new_txn;
+
+  SVN_ERR_ASSERT(! (args->flags & SVN_FS_TXN_CHECK_OOD));
+  SVN_ERR_ASSERT(! (args->flags & SVN_FS_TXN_CHECK_LOCKS));
+
+  /* Create a new txn, like txn_body_begin_txn() does. */
+  SVN_ERR(svn_fs_base__rev_get_root(&base_root_id, trail->fs,
+                                    args->based_on_rev, trail, trail->pool));
+  SVN_ERR(svn_fs_bdb__create_txn(&new_txn_id, trail->fs, base_root_id,
                                  trail, trail->pool));
+  /* ### Use svn_fs_base__dag_clone_root() instead? */
 
-  /* ### No need for "CHECK_OOD" and "CHECK_LOCKS" like the non-oblit case? */
+  /* Get the old and new txns */
+  SVN_ERR(svn_fs_base__rev_get_txn_id(&old_txn_id, trail->fs, replacing_rev,
+                                      trail, trail->pool));
+  SVN_ERR(svn_fs_bdb__get_txn(&old_txn, trail->fs, old_txn_id, trail,
+                              trail->pool));
+  SVN_ERR(svn_fs_bdb__get_txn(&new_txn, trail->fs, new_txn_id, trail,
+                              trail->pool));
+
+  /* Populate the transaction NEW_TXN with a clone of the changes described
+   * by revision REPLACING_REV, in TRAIL in FS.
+   *
+   * This is like a combination of "dup the txn" and "make the txn mutable".
+   * "Dup the txn" means making a deep copy, but with a new txn id.
+   * "Make mutable" is like the opposite of finalizing a txn.
+   *
+   * Dup r50:
+   *   * dup TRANSACTIONS<t50> to TRANSACTIONS<t50'>
+   *       except (kind -> transaction)
+   *   * dup all NODES<*.*.t50> to NODES<*.*.t50'>
+   *   * dup all referenced REPRESENTATIONS<*> to REPRESENTATIONS<*'>
+   *   * dup all CHANGES<t50> to CHANGES<t50'>
+   *   * create new STRINGS<*> where necessary (###?)
+   *   * don't dup NODE-ORIGINS<node_id>
+   *   * don't dup CHECKSUM-REPS<csum>
+   *   * don't dup COPIES<cpy_id>
+   *     (We need to keep the copy IDs the same, but will need to modify the
+   *     copy src_txn fields.)
+   *
+   * Make mutable:
+   *   * change txn kind from "committed" to "transaction"
+   *   * do something with COPIES?
+   */
+
+  SVN_ERR_ASSERT(new_txn->kind == transaction_kind_normal);
+
+  /* Dup the old txn's root node-rev (recursively).
+   * ### What root node-rev did the new txn have when created? Should we be
+   * modifying that one instead of replacing it? */
+  SVN_ERR(node_rev_dup(&new_txn->root_id, new_txn_id, old_txn->root_id,
+                       trail, trail->pool, trail->pool));
 
-  *args->txn_p = make_txn(trail->fs, txn_id, args->rev, trail->pool);
-  return SVN_NO_ERROR;
-}
+  /* Dup txn->proplist */
+  new_txn->proplist = old_txn->proplist;
 
+  /* Dup txn->copies */
+  if (old_txn->copies)
+    {
+      int i;
 
+      new_txn->copies = apr_array_make(trail->pool, old_txn->copies->nelts,
+                                       sizeof(const char *));
+      for (i = 0; i < old_txn->copies->nelts; i++)
+        {
+          const char *old_copy_id = APR_ARRAY_IDX(new_txn->copies, i, const char *);
+          const char *new_copy_id;
+
+          SVN_ERR(copy_dup(&new_copy_id, old_copy_id, new_txn_id, trail,
+                           trail->pool, trail->pool));
+          APR_ARRAY_IDX(new_txn->copies, i, const char *) = new_copy_id;
+        }
+    }
+
+  /* ### TODO: Dup the "changes" that are keyed by the txn_id. */
+
+  /* Save the modified transaction */
+  SVN_ERR(svn_fs_bdb__put_txn(trail->fs, new_txn, new_txn_id, trail,
+                              trail->pool));
+
+  /* Make and return an in-memory txn object referring to the new txn */
+  *args->txn_p = make_txn(trail->fs, new_txn_id, args->based_on_rev,
+                          trail->pool);
+  return SVN_NO_ERROR;
+}
 
 
 /* Note:  it is acceptable for this function to call back into
@@ -729,7 +986,7 @@ svn_fs_base__begin_txn(svn_fs_txn_t **tx
   SVN_ERR(svn_fs__check_fs(fs, TRUE));
 
   args.txn_p = &txn;
-  args.rev   = rev;
+  args.based_on_rev = rev;
   args.flags = flags;
   SVN_ERR(svn_fs_base__retry_txn(fs, txn_body_begin_txn, &args, FALSE, pool));
 
@@ -746,11 +1003,17 @@ svn_fs_base__begin_txn(svn_fs_txn_t **tx
                                        &date, pool);
 }
 
-
+/* Create a new transaction in FS that is a mutable clone of the transaction
+ * in revision REPLACING_REV and is intended to replace it. Set *TXN_P to
+ * point to the new transaction.
+ *
+ * This is like svn_fs_base__begin_txn() except that is populates the new txn
+ * with a mutable clone of revision REPLACING_REV, and it does not support the
+ * CHECK_OOD and CHECK_LOCKS flags, and it does not change the date stamp. */
 svn_error_t *
 svn_fs_base__begin_obliteration_txn(svn_fs_txn_t **txn_p,
                                     svn_fs_t *fs,
-                                    svn_revnum_t rev,
+                                    svn_revnum_t replacing_rev,
                                     apr_pool_t *pool)
 {
   svn_fs_txn_t *txn;
@@ -758,15 +1021,18 @@ svn_fs_base__begin_obliteration_txn(svn_
 
   SVN_ERR(svn_fs__check_fs(fs, TRUE));
 
+  /* Create a new, empty txn. */
+  /* Dup replacing_rev's txn into the new txn. */
+  /* ### Does the dup need to be inside or outside the retry_txn? It's inside. */
   args.txn_p = &txn;
-  args.rev   = rev;
+  args.based_on_rev = replacing_rev - 1;
   args.flags = 0;
   SVN_ERR(svn_fs_base__retry_txn(fs, txn_body_begin_obliteration_txn, &args,
           FALSE, pool));
 
   *txn_p = txn;
 
-  return svn_error_create(SVN_ERR_UNSUPPORTED_FEATURE, NULL, NULL);
+  return SVN_NO_ERROR;
 }
 
 
Index: subversion/libsvn_fs_base/revs-txns.h
===================================================================
--- subversion/libsvn_fs_base/revs-txns.h	(revision 887172)
+++ subversion/libsvn_fs_base/revs-txns.h	(working copy)
@@ -91,7 +91,7 @@ svn_error_t *svn_fs_base__txn_get_revisi
                                            apr_pool_t *pool);
 
 
-/* Retrieve information about the Subversion transaction SVN_TXN from
+/* Retrieve information about the Subversion transaction TXN_NAME from
    the `transactions' table of FS, as part of TRAIL.
    Set *ROOT_ID_P to the ID of the transaction's root directory.
    Set *BASE_ROOT_ID_P to the ID of the root directory of the
@@ -191,7 +191,8 @@ svn_error_t *svn_fs_base__begin_txn(svn_
    revision REV.  The new transaction is returned in *TXN_P.  Allocate
    the new transaction structure from POOL. */
 svn_error_t *svn_fs_base__begin_obliteration_txn(svn_fs_txn_t **txn_p,
-                                                 svn_fs_t *fs, svn_revnum_t rev,
+                                                 svn_fs_t *fs,
+                                                 svn_revnum_t replacing_rev,
                                                  apr_pool_t *pool);
 
 svn_error_t *svn_fs_base__open_txn(svn_fs_txn_t **txn, svn_fs_t *fs,
Index: subversion/libsvn_fs_base/tree.c
===================================================================
--- subversion/libsvn_fs_base/tree.c	(revision 887172)
+++ subversion/libsvn_fs_base/tree.c	(working copy)
@@ -2780,8 +2780,8 @@ svn_fs_base__commit_obliteration_txn(svn
 {
   /* svn_fs_t *fs = txn->fs; */
 
-  return svn_error_create(SVN_ERR_UNSUPPORTED_FEATURE, NULL, NULL);
-  /* return SVN_NO_ERROR; */
+  /* return svn_error_create(SVN_ERR_UNSUPPORTED_FEATURE, NULL, NULL); */
+  return SVN_NO_ERROR;
 }
 
 
Index: subversion/tests/cmdline/obliterate_tests.py
===================================================================
--- subversion/tests/cmdline/obliterate_tests.py	(revision 0)
+++ subversion/tests/cmdline/obliterate_tests.py	(working copy)
@@ -0,0 +1,86 @@
+#!/usr/bin/env python
+#
+#  obliterate_tests.py:  testing Obliterate
+#
+#  Subversion is a tool for revision control.
+#  See http://subversion.tigris.org for more information.
+#
+# ====================================================================
+#    Licensed to the Apache Software Foundation (ASF) under one
+#    or more contributor license agreements.  See the NOTICE file
+#    distributed with this work for additional information
+#    regarding copyright ownership.  The ASF licenses this file
+#    to you under the Apache License, Version 2.0 (the
+#    "License"); you may not use this file except in compliance
+#    with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing,
+#    software distributed under the License is distributed on an
+#    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#    KIND, either express or implied.  See the License for the
+#    specific language governing permissions and limitations
+#    under the License.
+######################################################################
+
+# General modules
+import shutil, sys, re, os
+
+# Our testing module
+import svntest
+from svntest import wc, objects
+
+# (abbreviation)
+Item = wc.StateItem
+XFail = svntest.testcase.XFail
+Skip = svntest.testcase.Skip
+SkipUnless = svntest.testcase.SkipUnless
+
+
+######################################################################
+# Tests
+#
+#   Each test must return on success or raise on failure.
+
+#----------------------------------------------------------------------
+
+def obliterate_1(sbox):
+  "test svn obliterate"
+
+  sbox.build()
+  wc = objects.SvnWC(sbox)
+  repo = wc.repo
+  os.chdir(wc.wc_dir)
+
+  # Set up a repository containing scenarios for obliteration
+  wc.mkdir('d')
+  f = 'd/foo'
+  wc.file_create(f, 'Pear\n')
+  wc.commit()
+  wc.file_modify(f, 'Apple\n')
+  apple_rev = wc.commit()
+  wc.file_modify(f, 'Orange\n')
+  wc.commit()
+
+  # Dump the repository state
+  repo.dump('before.dump')
+
+  # Obliterate d/f...@{content=apple}
+  repo.obliterate_node_rev(f, apple_rev)
+
+  # Dump the repository state
+  repo.dump('after.dump')
+
+
+########################################################################
+# Run the tests
+
+# list all tests here, starting with None:
+test_list = [ None,
+              obliterate_1,
+             ]
+
+if __name__ == '__main__':
+  svntest.main.run_tests(test_list)
+  # NOTREACHED

Property changes on: subversion/tests/cmdline/obliterate_tests.py
___________________________________________________________________
Added: svn:executable
## -0,0 +1 ##
+*
Added: svn:mime-type
## -0,0 +1 ##
+text/x-python
Index: subversion/tests/cmdline/svntest/objects.py
===================================================================
--- subversion/tests/cmdline/svntest/objects.py	(revision 0)
+++ subversion/tests/cmdline/svntest/objects.py	(working copy)
@@ -0,0 +1,208 @@
+#!/usr/bin/env python
+#
+#  objects.py: Objects that keep track of state during a test
+#
+#  Subversion is a tool for revision control.
+#  See http://subversion.tigris.org for more information.
+#
+# ====================================================================
+#    Licensed to the Apache Software Foundation (ASF) under one
+#    or more contributor license agreements.  See the NOTICE file
+#    distributed with this work for additional information
+#    regarding copyright ownership.  The ASF licenses this file
+#    to you under the Apache License, Version 2.0 (the
+#    "License"); you may not use this file except in compliance
+#    with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing,
+#    software distributed under the License is distributed on an
+#    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#    KIND, either express or implied.  See the License for the
+#    specific language governing permissions and limitations
+#    under the License.
+######################################################################
+
+# General modules
+import shutil, sys, re, os, subprocess
+
+import csvn.wc
+
+# Our testing module
+import svntest
+from svntest import wc, actions, main, verify
+
+
+######################################################################
+# Helpers
+
+def local_path(path):
+  "Convert a path from '/' separators to the local style."
+  return os.sep.join(path.split('/'))
+
+
+######################################################################
+# Class SvnRepository
+
+class SvnRepository:
+  """An object of class SvnRepository represents a Subversion repository,
+     providing both a client-side view and a server-side view."""
+
+  def __init__(self, repo_url, repo_dir):
+    self.repo_url = repo_url
+    self.repo_dir = repo_dir
+    self.head_rev = 1
+
+  def dump(self, output_dir):
+    """Dump the repository into the directory OUTPUT_DIR"""
+    ldir = local_path(output_dir)
+    os.mkdir(ldir)
+    print "## SvnRepository::dump(rep_dir=" + self.repo_dir + ")"
+
+    """Run a BDB dump on the repository"""
+    subprocess.call(["/home/julianfoad/bin/svn-dump-bdb", self.repo_dir, ldir])
+
+    """Run 'svnadmin dump' on the repository."""
+    exit_code, stdout, stderr = \
+      actions.run_and_verify_svnadmin(None, None, None,
+                                      'dump', self.repo_dir)
+    ldumpfile = local_path(output_dir + "/svnadmin.dump")
+    main.file_write(ldumpfile, ''.join(stderr))
+    main.file_append(ldumpfile, ''.join(stdout))
+
+
+  def obliterate_node_rev(self, path, rev):
+    """Obliterate the single node-rev PATH in revision REV."""
+    actions.run_and_verify_svn(None, None, [],
+                               'obliterate',
+                               self.repo_url + '/' + path + '@' + str(rev))
+
+
+######################################################################
+# Class SvnWC
+
+class SvnWC:
+  """An object of class SvnWC represents a WC, and provides methods for
+     operating on it. It keeps track of the state of the WC and of the
+     repository, so that the expected results of common operations are
+     automatically known. Any relative paths are relative to the WC dir.
+     """
+
+  def __init__(self, sbox):
+    """Initialize the object to use the WC and repository of the given
+       sandbox SBOX, which must already be initialized and checked out.
+    """
+    self.wc_dir = os.path.join(os.getcwd(), sbox.wc_dir)
+    self.state = wc.State(self.wc_dir, {})
+
+    # ### experimenting with csvn (Python Ctypes bindings)
+    self.wc = csvn.wc.WC(self.wc_dir)
+
+    # The 'repo' object may not strictly belong in the WC object, but it
+    # is here for convenience, for now. When we write an SvnWC constructor
+    # that doesn't know the full repo details (only its URL), then this may
+    # have to be moved out.
+    self.repo = SvnRepository(sbox.repo_url, os.path.join(os.getcwd(), sbox.repo_dir))
+
+  def __str__(self):
+    return "SvnWC(head_rev=" + str(self.repo.head_rev) + ", state={" + \
+      str(self.state.desc) + \
+      "})"
+
+  def mkdir(self, path):
+    lpath = local_path(path)
+    actions.run_and_verify_svn(None, None, [], 'mkdir', lpath)
+
+    self.state.add({
+      path : wc.StateItem(status='A')
+    })
+
+#  def propset(self, pname, pvalue, *paths):
+#    "Set property 'pname' to value 'pvalue' on each path in 'paths'"
+#    local_paths = tuple([local_path(path) for path in paths])
+#    actions.run_and_verify_svn(None, None, [], 'propset', pname, pvalue,
+#                               *local_paths)
+
+  def set_props(self, path, props):
+    """Change the properties of PATH to be the dictionary {name -> value} PROPS.
+    """
+    lpath = local_path(path)
+    #for prop in path's existing props:
+    #  actions.run_and_verify_svn(None, None, [], 'propdel',
+    #                             prop, lpath)
+    for prop in props:
+      actions.run_and_verify_svn(None, None, [], 'propset',
+                                 prop, props[prop], lpath)
+    self.state.tweak(path, props=props)
+
+  def file_create(self, path, content=None, props=None):
+    "Make and add a file with some default content, and keyword expansion."
+    lpath = local_path(path)
+    ldirname, filename = os.path.split(lpath)
+    if content is None:
+      # Default content
+      content = "This is the file '" + filename + "'.\n" + \
+                "Last changed in '$Revision$'.\n"
+    main.file_write(lpath, content)
+    actions.run_and_verify_svn(None, None, [], 'add', lpath)
+
+    self.state.add({
+      path : wc.StateItem(status='A')
+    })
+    if props is None:
+      # Default props
+      props = {
+        'svn:keywords': 'Revision'
+      }
+    self.set_props(path, props)
+
+  def file_modify(self, path, content=None, props=None):
+    "Make text and property mods to a WC file."
+    lpath = local_path(path)
+    if content is not None:
+      #main.file_append(lpath, "An extra line.\n")
+      #actions.run_and_verify_svn(None, None, [], 'propset',
+      #                           'newprop', 'v', lpath)
+      main.file_write(lpath, content)
+      self.state.tweak(path, content=content)
+    if props is not None:
+      self.set_props(path, props)
+      self.state.tweak(path, props=props)
+
+#  def copy(self, s_rev, path1, path2):
+#    "Copy a WC path locally."
+#    lpath1 = local_path(path1)
+#    lpath2 = local_path(path2)
+#    actions.run_and_verify_svn(None, None, [], 'copy', '--parents',
+#                               '-r', s_rev, lpath1, lpath2)
+
+#  def delete(self, path):
+#    "Delete a WC path locally."
+#    lpath = local_path(path)
+#    actions.run_and_verify_svn(None, None, [], 'delete', lpath)
+
+  def commit(self, path='.'):
+    "Commit a WC path (recursively). Return the new revision number."
+    lpath = local_path(path)
+    actions.run_and_verify_svn(None, verify.AnyOutput, [],
+                               'commit', '-m', '', lpath)
+    self.repo.head_rev += 1
+    return self.repo.head_rev
+
+#  def merge(self, rev_spec, source, target, exp_out=None):
+#    """Merge a single change from path 'source' to path 'target'.
+#    SRC_CHANGE_NUM is either a number (to cherry-pick that specific change)
+#    or a command-line option revision range string such as '-r10:20'."""
+#    lsource = local_path(source)
+#    ltarget = local_path(target)
+#    if isinstance(rev_spec, int):
+#      rev_spec = '-c' + str(rev_spec)
+#    if exp_out is None:
+#      target_re = re.escape(target)
+#      exp_1 = "--- Merging r.* into '" + target_re + ".*':"
+#      exp_2 = "(A |D |[UG] | [UG]|[UG][UG])   " + target_re + ".*"
+#      exp_out = verify.RegexOutput(exp_1 + "|" + exp_2)
+#    actions.run_and_verify_svn(None, exp_out, [],
+#                               'merge', rev_spec, lsource, ltarget)
+

Property changes on: subversion/tests/cmdline/svntest/objects.py
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+text/x-python

Reply via email to