[prev in list] [next in list] [prev in thread] [next in thread] 

List:       subversion-commits
Subject:    svn commit: r1874634 [3/3] - in /subversion/branches/decouple-shelving-cli/subversion:
From:       julianfoad () apache ! org
Date:       2020-02-28 22:15:15
Message-ID: 20200228221515.B36DF17A10B () svn01-us-east ! apache ! org
[Download RAW message or body]

Added: subversion/branches/decouple-shelving-cli/subversion/svn/shelf2-cmd.c
URL: http://svn.apache.org/viewvc/subversion/branches/decouple-shelving-cli/subversion/svn/shelf2-cmd.c?rev=1874634&view=auto
 ==============================================================================
--- subversion/branches/decouple-shelving-cli/subversion/svn/shelf2-cmd.c (added)
+++ subversion/branches/decouple-shelving-cli/subversion/svn/shelf2-cmd.c Fri Feb 28 \
22:15:15 2020 @@ -0,0 +1,1369 @@
+/*
+ * shelf2-cmd.c -- Shelving commands.
+ *
+ * ====================================================================
+ *    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.
+ * ====================================================================
+ */
+
+/* We define this here to remove any further warnings about the usage of
+   experimental functions in this file. */
+#define SVN_EXPERIMENTAL
+
+#include "svn_client.h"
+#include "svn_error_codes.h"
+#include "svn_error.h"
+#include "svn_hash.h"
+#include "svn_path.h"
+#include "svn_props.h"
+#include "svn_pools.h"
+#include "svn_utf.h"
+
+#include "shelf2-cmd.h"
+#include "cl.h"
+
+#include "svn_private_config.h"
+#include "private/svn_sorts_private.h"
+#include "private/svn_client_private.h"
+#include "private/svn_client_shelf2.h"
+
+
+/* Open the newest version of SHELF; error if no versions found. */
+static svn_error_t *
+get_newest_version_existing(svn_client__shelf2_version_t **shelf_version_p,
+                            svn_client__shelf2_t *shelf,
+                            apr_pool_t *result_pool,
+                            apr_pool_t *scratch_pool)
+{
+  SVN_ERR(svn_client__shelf2_get_newest_version(shelf_version_p, shelf,
+                                              result_pool, scratch_pool));
+  if (!*shelf_version_p)
+    {
+      return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
+                               _("Shelf '%s': no versions found"),
+                               shelf->name);
+    }
+
+  return SVN_NO_ERROR;
+}
+
+/* Fetch the next argument. */
+static svn_error_t *
+get_next_argument(const char **arg,
+                  apr_getopt_t *os,
+                  apr_pool_t *result_pool,
+                  apr_pool_t *scratch_pool)
+{
+  apr_array_header_t *args;
+
+  SVN_ERR(svn_opt_parse_num_args(&args, os, 1, scratch_pool));
+  SVN_ERR(svn_utf_cstring_to_utf8(arg,
+                                  APR_ARRAY_IDX(args, 0, const char *),
+                                  result_pool));
+  return SVN_NO_ERROR;
+}
+
+/* Parse the remaining arguments as paths relative to a WC.
+ *
+ * TARGETS are relative to current working directory.
+ *
+ * Set *targets_by_wcroot to a hash mapping (char *)wcroot_abspath to
+ * (apr_array_header_t *)array of relpaths relative to that WC root.
+ */
+static svn_error_t *
+targets_relative_to_wcs(apr_hash_t **targets_by_wcroot_p,
+                        apr_array_header_t *targets,
+                        svn_client_ctx_t *ctx,
+                        apr_pool_t *result_pool,
+                        apr_pool_t *scratch_pool)
+{
+  apr_hash_t *targets_by_wcroot = apr_hash_make(result_pool);
+  int i;
+
+  /* Make each target relative to the WC root. */
+  for (i = 0; i < targets->nelts; i++)
+    {
+      const char *target = APR_ARRAY_IDX(targets, i, const char *);
+      const char *wcroot_abspath;
+      apr_array_header_t *paths;
+
+      SVN_ERR(svn_dirent_get_absolute(&target, target, result_pool));
+      SVN_ERR(svn_client_get_wc_root(&wcroot_abspath, target,
+                                     ctx, result_pool, scratch_pool));
+      paths = svn_hash_gets(targets_by_wcroot, wcroot_abspath);
+      if (! paths)
+        {
+          paths = apr_array_make(result_pool, 0, sizeof(char *));
+          svn_hash_sets(targets_by_wcroot, wcroot_abspath, paths);
+        }
+      target = svn_dirent_skip_ancestor(wcroot_abspath, target);
+
+      if (target)
+        APR_ARRAY_PUSH(paths, const char *) = target;
+    }
+  *targets_by_wcroot_p = targets_by_wcroot;
+  return SVN_NO_ERROR;
+}
+
+/* Return targets relative to a WC. Error if they refer to more than one WC. */
+static svn_error_t *
+targets_relative_to_a_wc(const char **wc_root_abspath_p,
+                         apr_array_header_t **paths_p,
+                         apr_getopt_t *os,
+                         const apr_array_header_t *known_targets,
+                         svn_client_ctx_t *ctx,
+                         apr_pool_t *result_pool,
+                         apr_pool_t *scratch_pool)
+{
+  apr_array_header_t *targets;
+  apr_hash_t *targets_by_wcroot;
+  apr_hash_index_t *hi;
+
+  SVN_ERR(svn_cl__args_to_target_array_print_reserved(&targets, os,
+                                                      known_targets,
+                                                      ctx, FALSE, result_pool));
+  svn_opt_push_implicit_dot_target(targets, result_pool);
+
+  SVN_ERR(targets_relative_to_wcs(&targets_by_wcroot, targets,
+                                  ctx, result_pool, scratch_pool));
+  if (apr_hash_count(targets_by_wcroot) != 1)
+    return svn_error_create(SVN_ERR_ILLEGAL_TARGET, NULL,
+                            _("All targets must be in the same WC"));
+
+  hi = apr_hash_first(scratch_pool, targets_by_wcroot);
+  *wc_root_abspath_p = apr_hash_this_key(hi);
+  *paths_p = apr_hash_this_val(hi);
+  return SVN_NO_ERROR;
+}
+
+/* Return a human-friendly description of DURATION.
+ */
+static char *
+friendly_age_str(apr_time_t mtime,
+                 apr_time_t time_now,
+                 apr_pool_t *result_pool)
+{
+  int minutes = (int)((time_now - mtime) / 1000000 / 60);
+  char *s;
+
+  if (minutes >= 60 * 24)
+    s = apr_psprintf(result_pool,
+                     Q_("%d day ago", "%d days ago",
+                        minutes / 60 / 24),
+                     minutes / 60 / 24);
+  else if (minutes >= 60)
+    s = apr_psprintf(result_pool,
+                     Q_("%d hour ago", "%d hours ago",
+                        minutes / 60),
+                     minutes / 60);
+  else
+    s = apr_psprintf(result_pool,
+                     Q_("%d minute ago", "%d minutes ago",
+                        minutes),
+                     minutes);
+  return s;
+}
+
+/* A comparison function for svn_sort__hash(), comparing the mtime of two
+   svn_client_shelf_info_t's. */
+static int
+compare_shelf_infos_by_mtime(const svn_sort__item_t *a,
+                             const svn_sort__item_t *b)
+{
+  svn_client__shelf2_info_t *a_val = a->value;
+  svn_client__shelf2_info_t *b_val = b->value;
+
+  return (a_val->mtime < b_val->mtime)
+    ? -1 : (a_val->mtime > b_val->mtime) ? 1 : 0;
+}
+
+/* Return a list of shelves sorted by their mtime, oldest first.
+ */
+static svn_error_t *
+list_sorted_by_date(apr_array_header_t **list,
+                    const char *local_abspath,
+                    svn_client_ctx_t *ctx,
+                    apr_pool_t *scratch_pool)
+{
+  apr_hash_t *shelf_infos;
+
+  SVN_ERR(svn_client__shelf2_list(&shelf_infos, local_abspath,
+                                ctx, scratch_pool, scratch_pool));
+  *list = svn_sort__hash(shelf_infos,
+                         compare_shelf_infos_by_mtime,
+                         scratch_pool);
+  return SVN_NO_ERROR;
+}
+
+/*  */
+static svn_error_t *
+stats(svn_client__shelf2_t *shelf,
+      int version,
+      svn_client__shelf2_version_t *shelf_version,
+      apr_time_t time_now,
+      svn_boolean_t with_logmsg,
+      apr_pool_t *scratch_pool)
+{
+  char *age_str;
+  char *version_str;
+  apr_hash_t *paths;
+  const char *paths_str = "";
+
+  if (! shelf_version)
+    {
+      return SVN_NO_ERROR;
+    }
+
+  age_str = friendly_age_str(shelf_version->mtime, time_now, scratch_pool);
+  if (version == shelf->max_version)
+    version_str = apr_psprintf(scratch_pool,
+                               _("version %d"), version);
+  else
+    version_str = apr_psprintf(scratch_pool,
+                               Q_("version %d of %d", "version %d of %d",
+                                  shelf->max_version),
+                               version, shelf->max_version);
+  SVN_ERR(svn_client__shelf2_paths_changed(&paths, shelf_version,
+                                         scratch_pool, scratch_pool));
+  paths_str = apr_psprintf(scratch_pool,
+                           Q_("%d path changed", "%d paths changed",
+                              apr_hash_count(paths)),
+                           apr_hash_count(paths));
+  SVN_ERR(svn_cmdline_printf(scratch_pool,
+                             "%-30s %s, %s, %s\n",
+                             shelf->name, version_str, age_str, paths_str));
+
+  if (with_logmsg)
+    {
+      char *log_message;
+
+      SVN_ERR(svn_client__shelf2_get_log_message(&log_message, shelf,
+                                               scratch_pool));
+      if (log_message)
+        {
+          SVN_ERR(svn_cmdline_printf(scratch_pool,
+                                     _(" %.50s\n"),
+                                     log_message));
+        }
+    }
+
+  return SVN_NO_ERROR;
+}
+
+/* Display a list of shelves */
+static svn_error_t *
+shelves_list(const char *local_abspath,
+             svn_boolean_t quiet,
+             svn_client_ctx_t *ctx,
+             apr_pool_t *scratch_pool)
+{
+  apr_time_t time_now = apr_time_now();
+  apr_array_header_t *list;
+  int i;
+
+  SVN_ERR(list_sorted_by_date(&list,
+                              local_abspath, ctx, scratch_pool));
+
+  for (i = 0; i < list->nelts; i++)
+    {
+      const svn_sort__item_t *item = &APR_ARRAY_IDX(list, i, svn_sort__item_t);
+      const char *name = item->key;
+      svn_client__shelf2_t *shelf;
+      svn_client__shelf2_version_t *shelf_version;
+
+      SVN_ERR(svn_client__shelf2_open_existing(&shelf, name, local_abspath,
+                                             ctx, scratch_pool));
+      SVN_ERR(svn_client__shelf2_get_newest_version(&shelf_version, shelf,
+                                                  scratch_pool, scratch_pool));
+      if (quiet)
+        SVN_ERR(svn_cmdline_printf(scratch_pool, "%s\n", shelf->name));
+      else if (!shelf_version)
+        SVN_ERR(svn_cmdline_printf(scratch_pool, "%-30s no versions\n",
+                                   shelf->name));
+      else
+        SVN_ERR(stats(shelf, shelf->max_version, shelf_version, time_now,
+                      TRUE /*with_logmsg*/, scratch_pool));
+      SVN_ERR(svn_client__shelf2_close(shelf, scratch_pool));
+    }
+
+  return SVN_NO_ERROR;
+}
+
+/* Print info about each checkpoint of the shelf named NAME.
+ */
+static svn_error_t *
+shelf_log(const char *name,
+          const char *local_abspath,
+          svn_client_ctx_t *ctx,
+          apr_pool_t *scratch_pool)
+{
+  apr_time_t time_now = apr_time_now();
+  svn_client__shelf2_t *shelf;
+  apr_array_header_t *versions;
+  int i;
+
+  SVN_ERR(svn_client__shelf2_open_existing(&shelf, name, local_abspath,
+                                         ctx, scratch_pool));
+  SVN_ERR(svn_client__shelf2_get_all_versions(&versions, shelf,
+                                            scratch_pool, scratch_pool));
+  for (i = 0; i < versions->nelts; i++)
+    {
+      svn_client__shelf2_version_t *shelf_version
+        = APR_ARRAY_IDX(versions, i, void *);
+
+      SVN_ERR(stats(shelf, i + 1, shelf_version, time_now,
+                    FALSE /*with_logmsg*/, scratch_pool));
+    }
+
+  SVN_ERR(svn_client__shelf2_close(shelf, scratch_pool));
+  return SVN_NO_ERROR;
+}
+
+/* Find the name of the youngest shelf.
+ */
+static svn_error_t *
+name_of_youngest(const char **name_p,
+                 const char *local_abspath,
+                 svn_client_ctx_t *ctx,
+                 apr_pool_t *result_pool,
+                 apr_pool_t *scratch_pool)
+{
+  apr_array_header_t *list;
+  const svn_sort__item_t *youngest_item;
+
+  SVN_ERR(list_sorted_by_date(&list,
+                              local_abspath, ctx, scratch_pool));
+  if (list->nelts == 0)
+    return svn_error_create(SVN_ERR_CL_INSUFFICIENT_ARGS, NULL,
+                            _("No shelves found"));
+
+  youngest_item = &APR_ARRAY_IDX(list, list->nelts - 1, svn_sort__item_t);
+  *name_p = apr_pstrdup(result_pool, youngest_item->key);
+  return SVN_NO_ERROR;
+}
+
+struct status_baton
+{
+  /* These fields correspond to the ones in the
+     svn_cl__print_status() interface. */
+  const char *target_abspath;
+  const char *target_path;
+
+  svn_boolean_t quiet;  /* don't display statuses while shelving them */
+  int num_paths_shelved;
+  int num_paths_not_shelved;
+  svn_client_ctx_t *ctx;
+};
+
+/* A status callback function for printing STATUS for PATH. */
+static svn_error_t *
+print_status(void *baton,
+             const char *path,
+             const svn_client_status_t *status,
+             apr_pool_t *scratch_pool)
+{
+  struct status_baton *sb = baton;
+  unsigned int conflicts;
+
+  return svn_cl__print_status(sb->target_abspath, sb->target_path,
+                              path, status,
+                              TRUE /*suppress_externals_placeholders*/,
+                              FALSE /*detailed*/,
+                              FALSE /*show_last_committed*/,
+                              TRUE /*skip_unrecognized*/,
+                              FALSE /*repos_locks*/,
+                              &conflicts, &conflicts, &conflicts,
+                              sb->ctx,
+                              scratch_pool);
+}
+
+/* A callback function for shelved paths. */
+static svn_error_t *
+was_shelved(void *baton,
+            const char *path,
+            const svn_client_status_t *status,
+            apr_pool_t *scratch_pool)
+{
+  struct status_baton *sb = baton;
+
+  if (!sb->quiet)
+    {
+      SVN_ERR(print_status(baton, path, status, scratch_pool));
+    }
+
+  ++sb->num_paths_shelved;
+  return SVN_NO_ERROR;
+}
+
+/* A callback function for not-shelved paths. */
+static svn_error_t *
+was_not_shelved(void *baton,
+                const char *path,
+                const svn_client_status_t *status,
+                apr_pool_t *scratch_pool)
+{
+  struct status_baton *sb = baton;
+
+  SVN_ERR(print_status(baton, path, status, scratch_pool));
+  SVN_ERR(svn_cmdline_printf(scratch_pool, "      >   not shelved\n"));
+  ++sb->num_paths_not_shelved;
+  return SVN_NO_ERROR;
+}
+
+/** Shelve/save a new version of changes.
+ *
+ * Shelve in shelf @a name the local modifications found by @a paths,
+ * @a depth, @a changelists. Revert the shelved changes from the WC
+ * unless @a keep_local is true.
+ *
+ * If no local modifications are found, throw an error.
+ *
+ * If @a dry_run is true, don't actually do it.
+ *
+ * Report in @a *new_version_p the new version number (or, with dry run,
+ * what it would be).
+ */
+static svn_error_t *
+shelve(int *new_version_p,
+       const char *name,
+       const apr_array_header_t *paths,
+       svn_depth_t depth,
+       const apr_array_header_t *changelists,
+       apr_hash_t *revprop_table,
+       svn_boolean_t keep_local,
+       svn_boolean_t dry_run,
+       svn_boolean_t quiet,
+       const char *local_abspath,
+       svn_client_ctx_t *ctx,
+       apr_pool_t *scratch_pool)
+{
+  svn_client__shelf2_t *shelf;
+  svn_client__shelf2_version_t *previous_version;
+  svn_client__shelf2_version_t *new_version;
+  struct status_baton sb;
+
+  SVN_ERR(svn_client__shelf2_open_or_create(&shelf,
+                                          name, local_abspath,
+                                          ctx, scratch_pool));
+  SVN_ERR(svn_client__shelf2_get_newest_version(&previous_version, shelf,
+                                              scratch_pool, scratch_pool));
+
+  if (! quiet)
+    {
+      SVN_ERR(svn_cmdline_printf(scratch_pool, keep_local
+                                 ? _("--- Save a new version of '%s' in WC root \
'%s'\n") +                                 : _("--- Shelve '%s' in WC root '%s'\n"),
+                                 shelf->name, shelf->wc_root_abspath));
+      SVN_ERR(stats(shelf, shelf->max_version, previous_version, apr_time_now(),
+                    TRUE /*with_logmsg*/, scratch_pool));
+    }
+
+  sb.target_abspath = shelf->wc_root_abspath;
+  sb.target_path = "";
+  sb.quiet = quiet;
+  sb.num_paths_shelved = 0;
+  sb.num_paths_not_shelved = 0;
+  sb.ctx = ctx;
+
+  if (! quiet)
+    SVN_ERR(svn_cmdline_printf(scratch_pool,
+                               keep_local ? _("--- Saving...\n")
+                               : _("--- Shelving...\n")));
+  SVN_ERR(svn_client__shelf2_save_new_version3(&new_version, shelf,
+                                             paths, depth, changelists,
+                                             was_shelved, &sb,
+                                             was_not_shelved, &sb,
+                                             scratch_pool));
+  if (sb.num_paths_not_shelved > 0)
+    {
+      SVN_ERR(svn_client__shelf2_delete_newer_versions(shelf, previous_version,
+                                                     scratch_pool));
+      SVN_ERR(svn_client__shelf2_close(shelf, scratch_pool));
+      return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
+                               Q_("%d path could not be shelved",
+                                  "%d paths could not be shelved",
+                                  sb.num_paths_not_shelved),
+                               sb.num_paths_not_shelved);
+    }
+  if (sb.num_paths_shelved == 0
+      || ! new_version)
+    {
+      SVN_ERR(svn_client__shelf2_close(shelf, scratch_pool));
+      return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
+                               keep_local ? _("No local modifications could be \
saved") +                               : _("No local modifications could be \
shelved")); +    }
+
+  /* Un-apply the changes, if required. */
+  if (!keep_local)
+    {
+      SVN_ERR(svn_client__shelf2_unapply(new_version,
+                                       dry_run, scratch_pool));
+    }
+
+  /* Fetch the log message and any other revprops */
+  if (ctx->log_msg_func3)
+    {
+      const char *tmp_file;
+      apr_array_header_t *commit_items
+        = apr_array_make(scratch_pool, 1, sizeof(void *));
+      const char *message = "";
+
+      SVN_ERR(ctx->log_msg_func3(&message, &tmp_file, commit_items,
+                                 ctx->log_msg_baton3, scratch_pool));
+      /* Abort the shelving if the log message callback requested so. */
+      if (! message)
+        return SVN_NO_ERROR;
+
+      if (message && !dry_run)
+        {
+          svn_string_t *propval = svn_string_create(message, scratch_pool);
+
+          if (! revprop_table)
+            revprop_table = apr_hash_make(scratch_pool);
+          svn_hash_sets(revprop_table, SVN_PROP_REVISION_LOG, propval);
+        }
+    }
+
+  SVN_ERR(svn_client__shelf2_revprop_set_all(shelf, revprop_table, scratch_pool));
+
+  if (new_version_p)
+    *new_version_p = shelf->max_version;
+
+  if (dry_run)
+    {
+      SVN_ERR(svn_client__shelf2_delete_newer_versions(shelf, previous_version,
+                                                     scratch_pool));
+    }
+
+  SVN_ERR(svn_client__shelf2_close(shelf, scratch_pool));
+  return SVN_NO_ERROR;
+}
+
+/* Return the single character representation of STATUS.
+ * (Similar to subversion/svn/status.c:generate_status_code()
+ * and subversion/tests/libsvn_client/client-test.c:status_to_char().) */
+static char
+status_to_char(enum svn_wc_status_kind status)
+{
+  switch (status)
+    {
+    case svn_wc_status_none:        return '.';
+    case svn_wc_status_unversioned: return '?';
+    case svn_wc_status_normal:      return ' ';
+    case svn_wc_status_added:       return 'A';
+    case svn_wc_status_missing:     return '!';
+    case svn_wc_status_deleted:     return 'D';
+    case svn_wc_status_replaced:    return 'R';
+    case svn_wc_status_modified:    return 'M';
+    case svn_wc_status_merged:      return 'G';
+    case svn_wc_status_conflicted:  return 'C';
+    case svn_wc_status_ignored:     return 'I';
+    case svn_wc_status_obstructed:  return '~';
+    case svn_wc_status_external:    return 'X';
+    case svn_wc_status_incomplete:  return ':';
+    default:                        return '*';
+    }
+}
+
+/* Throw an error if any path affected by SHELF_VERSION gives a conflict
+ * when applied (as a dry-run) to the WC. */
+static svn_error_t *
+test_apply(svn_client__shelf2_version_t *shelf_version,
+           svn_client_ctx_t *ctx,
+           apr_pool_t *scratch_pool)
+{
+  apr_hash_t *paths;
+  apr_hash_index_t *hi;
+
+  SVN_ERR(svn_client__shelf2_paths_changed(&paths, shelf_version,
+                                         scratch_pool, scratch_pool));
+  for (hi = apr_hash_first(scratch_pool, paths); hi; hi = apr_hash_next(hi))
+    {
+      const char *path = apr_hash_this_key(hi);
+      svn_boolean_t conflict;
+
+      SVN_ERR(svn_client__shelf2_test_apply_file(&conflict, shelf_version, path,
+                                               scratch_pool));
+      if (conflict)
+        {
+          char *to_wc_abspath
+            = svn_dirent_join(shelf_version->shelf->wc_root_abspath, path,
+                              scratch_pool);
+          svn_wc_status3_t *status;
+
+          SVN_ERR(svn_wc_status3(&status, ctx->wc_ctx, to_wc_abspath,
+                                 scratch_pool, scratch_pool));
+          return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
+                                   _("Shelved path '%s' already has "
+                                     "status '%c' in the working copy"),
+                                   path, status_to_char(status->node_status));
+        }
+    }
+  return SVN_NO_ERROR;
+}
+
+/** Restore/unshelve a given or newest version of changes.
+ *
+ * Restore local modifications from shelf @a name version @a arg,
+ * or the newest version is @a arg is null.
+ *
+ * If @a dry_run is true, don't actually do it.
+ *
+ * Error if any path would have a conflict, unless @a force_if_conflict.
+ */
+static svn_error_t *
+shelf_restore(const char *name,
+              const char *arg,
+              svn_boolean_t dry_run,
+              svn_boolean_t quiet,
+              svn_boolean_t force_if_conflict,
+              const char *local_abspath,
+              svn_client_ctx_t *ctx,
+              apr_pool_t *scratch_pool)
+{
+  int version, old_version;
+  apr_time_t time_now = apr_time_now();
+  svn_client__shelf2_t *shelf;
+  svn_client__shelf2_version_t *shelf_version;
+
+  SVN_ERR(svn_client__shelf2_open_existing(&shelf, name, local_abspath,
+                                         ctx, scratch_pool));
+
+  old_version = shelf->max_version;
+  if (arg)
+    {
+      SVN_ERR(svn_cstring_atoi(&version, arg));
+      SVN_ERR(svn_client__shelf2_version_open(&shelf_version,
+                                            shelf, version,
+                                            scratch_pool, scratch_pool));
+    }
+  else
+    {
+      version = shelf->max_version;
+      SVN_ERR(get_newest_version_existing(&shelf_version, shelf,
+                                          scratch_pool, scratch_pool));
+    }
+
+  if (! quiet)
+    {
+      SVN_ERR(svn_cmdline_printf(scratch_pool,
+                                 _("--- Unshelve '%s' in WC root '%s'\n"),
+                                 shelf->name, shelf->wc_root_abspath));
+      SVN_ERR(stats(shelf, version, shelf_version, time_now,
+                    TRUE /*with_logmsg*/, scratch_pool));
+    }
+  if (! force_if_conflict)
+    {
+      SVN_ERR_W(test_apply(shelf_version, ctx, scratch_pool),
+                _("Cannot unshelve/restore, as at least one shelved "
+                  "path would conflict with a local modification "
+                  "or other status in the working copy"));
+    }
+
+  SVN_ERR(svn_client__shelf2_apply(shelf_version,
+                                 dry_run, scratch_pool));
+
+  if (! dry_run)
+    {
+      SVN_ERR(svn_client__shelf2_delete_newer_versions(shelf, shelf_version,
+                                                     scratch_pool));
+    }
+
+  if (!quiet)
+    {
+      if (version < old_version)
+        SVN_ERR(svn_cmdline_printf(scratch_pool,
+                                   Q_("restored '%s' version %d and deleted %d newer \
version\n", +                                      "restored '%s' version %d and \
deleted %d newer versions\n", +                                      old_version - \
version), +                                   name, version, old_version - version));
+      else
+        SVN_ERR(svn_cmdline_printf(scratch_pool,
+                                   _("restored '%s' version %d (the newest \
version)\n"), +                                   name, version));
+    }
+
+  SVN_ERR(svn_client__shelf2_close(shelf, scratch_pool));
+  return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+shelf_diff(const char *name,
+           const char *arg,
+           const char *local_abspath,
+           svn_boolean_t summarize,
+           svn_depth_t depth,
+           svn_boolean_t ignore_ancestry,
+           svn_client_ctx_t *ctx,
+           apr_pool_t *scratch_pool)
+{
+  svn_client__shelf2_t *shelf;
+  svn_client__shelf2_version_t *shelf_version;
+  svn_stream_t *stream, *errstream;
+  svn_diff_tree_processor_t *diff_processor;
+
+  SVN_ERR(svn_client__shelf2_open_existing(&shelf, name, local_abspath,
+                                         ctx, scratch_pool));
+
+  if (arg)
+    {
+      int version;
+
+      SVN_ERR(svn_cstring_atoi(&version, arg));
+      SVN_ERR(svn_client__shelf2_version_open(&shelf_version,
+                                            shelf, version,
+                                            scratch_pool, scratch_pool));
+    }
+  else
+    {
+      SVN_ERR(get_newest_version_existing(&shelf_version, shelf,
+                                          scratch_pool, scratch_pool));
+    }
+
+  SVN_ERR(svn_stream_for_stdout(&stream, scratch_pool));
+  errstream = svn_stream_empty(scratch_pool);
+
+  if (summarize)
+    {
+      svn_client_diff_summarize_func_t func;
+      void *baton;
+
+      SVN_ERR(svn_cl__get_diff_summary_writer(&func, &baton,
+                                              FALSE /*xml*/,
+                                              FALSE /*ignore_properties*/,
+                                              "" /*anchor/prefix*/,
+                                              scratch_pool, scratch_pool));
+      SVN_ERR(svn_client__get_diff_summarize_callbacks(&diff_processor,
+                                                       func, baton,
+                                                       scratch_pool,
+                                                       scratch_pool));
+    }
+  else
+    {
+      SVN_ERR(svn_client__get_diff_writer_svn(
+                &diff_processor,
+                NULL /*anchor*/,
+                "", "", /*orig_path_1, orig_path_2,*/
+                NULL /*options*/,
+                "" /*relative_to_dir*/,
+                FALSE /*no_diff_added*/,
+                FALSE /*no_diff_deleted*/,
+                FALSE /*show_copies_as_adds*/,
+                FALSE /*ignore_content_type*/,
+                FALSE /*ignore_properties*/,
+                FALSE /*properties_only*/,
+                TRUE /*pretty_print_mergeinfo*/,
+                svn_cmdline_output_encoding(scratch_pool),
+                stream, errstream,
+                ctx, scratch_pool));
+    }
+
+  SVN_ERR(svn_client__shelf2_diff(shelf_version, "",
+                                 depth, ignore_ancestry,
+                                 diff_processor, scratch_pool));
+  SVN_ERR(svn_stream_close(stream));
+
+  SVN_ERR(svn_client__shelf2_close(shelf, scratch_pool));
+  return SVN_NO_ERROR;
+}
+
+/* This implements the `svn_opt_subcommand_t' interface. */
+static svn_error_t *
+shelf_drop(const char *name,
+           const char *local_abspath,
+           svn_boolean_t dry_run,
+           svn_boolean_t quiet,
+           svn_client_ctx_t *ctx,
+           apr_pool_t *scratch_pool)
+{
+  SVN_ERR(svn_client__shelf2_delete(name, local_abspath, dry_run,
+                                  ctx, scratch_pool));
+  if (! quiet)
+    SVN_ERR(svn_cmdline_printf(scratch_pool,
+                               _("deleted '%s'\n"),
+                               name));
+  return SVN_NO_ERROR;
+}
+
+/*  */
+static svn_error_t *
+shelf_shelve(int *new_version,
+             const char *name,
+             apr_array_header_t *targets,
+             svn_depth_t depth,
+             apr_array_header_t *changelists,
+             apr_hash_t *revprop_table,
+             svn_boolean_t keep_local,
+             svn_boolean_t dry_run,
+             svn_boolean_t quiet,
+             svn_client_ctx_t *ctx,
+             apr_pool_t *scratch_pool)
+{
+  const char *local_abspath;
+
+  if (depth == svn_depth_unknown)
+    depth = svn_depth_infinity;
+
+  SVN_ERR(svn_cl__check_targets_are_local_paths(targets));
+
+  SVN_ERR(svn_cl__eat_peg_revisions(&targets, targets, scratch_pool));
+
+  svn_opt_push_implicit_dot_target(targets, scratch_pool);
+
+  /* ### TODO: check all paths are in same WC; for now use first path */
+  SVN_ERR(svn_dirent_get_absolute(&local_abspath,
+                                  APR_ARRAY_IDX(targets, 0, char *),
+                                  scratch_pool));
+
+  SVN_ERR(shelve(new_version, name,
+                 targets, depth, changelists,
+                 revprop_table,
+                 keep_local, dry_run, quiet,
+                 local_abspath, ctx, scratch_pool));
+
+  return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+svn_cl__shelf_shelve(apr_getopt_t *os,
+                     void *baton,
+                     apr_pool_t *pool);
+
+/* This implements the `svn_opt_subcommand_t' interface. */
+static svn_error_t *
+svn_cl__shelf_save(apr_getopt_t *os,
+                   void *baton,
+                   apr_pool_t *pool)
+{
+  svn_cl__opt_state_t *opt_state = ((svn_cl__cmd_baton_t *) baton)->opt_state;
+
+  opt_state->keep_local = TRUE;
+  SVN_ERR(svn_cl__shelf_shelve(os, baton, pool));
+  return SVN_NO_ERROR;
+}
+
+/* This implements the `svn_opt_subcommand_t' interface. */
+static svn_error_t *
+svn_cl__shelf_shelve(apr_getopt_t *os,
+                     void *baton,
+                     apr_pool_t *pool)
+{
+  svn_cl__opt_state_t *opt_state = ((svn_cl__cmd_baton_t *) baton)->opt_state;
+  svn_client_ctx_t *ctx = ((svn_cl__cmd_baton_t *) baton)->ctx;
+  const char *name;
+  apr_array_header_t *targets;
+
+  if (opt_state->quiet)
+    ctx->notify_func2 = NULL; /* Easy out: avoid unneeded work */
+
+  SVN_ERR(get_next_argument(&name, os, pool, pool));
+
+  /* Parse the remaining arguments as paths. */
+  SVN_ERR(svn_cl__args_to_target_array_print_reserved(&targets, os,
+                                                      opt_state->targets,
+                                                      ctx, FALSE, pool));
+  {
+    int new_version;
+    svn_error_t *err;
+
+    if (ctx->log_msg_func3)
+      SVN_ERR(svn_cl__make_log_msg_baton(&ctx->log_msg_baton3,
+                                         opt_state, NULL, ctx->config,
+                                         pool));
+    err = shelf_shelve(&new_version, name,
+                       targets, opt_state->depth, opt_state->changelists,
+                       opt_state->revprop_table,
+                       opt_state->keep_local, opt_state->dry_run,
+                       opt_state->quiet, ctx, pool);
+    if (ctx->log_msg_func3)
+      SVN_ERR(svn_cl__cleanup_log_msg(ctx->log_msg_baton3,
+                                      err, pool));
+    else
+      SVN_ERR(err);
+
+      if (! opt_state->quiet)
+      {
+        if (opt_state->keep_local)
+          SVN_ERR(svn_cmdline_printf(pool,
+                                     _("saved '%s' version %d\n"),
+                                     name, new_version));
+        else
+          SVN_ERR(svn_cmdline_printf(pool,
+                                     _("shelved '%s' version %d\n"),
+                                     name, new_version));
+      }
+  }
+
+  return SVN_NO_ERROR;
+}
+
+/* This implements the `svn_opt_subcommand_t' interface. */
+static svn_error_t *
+svn_cl__shelf_unshelve(apr_getopt_t *os,
+                       void *baton,
+                       apr_pool_t *scratch_pool)
+{
+  svn_cl__opt_state_t *opt_state = ((svn_cl__cmd_baton_t *) baton)->opt_state;
+  svn_client_ctx_t *ctx = ((svn_cl__cmd_baton_t *) baton)->ctx;
+  const char *local_abspath;
+  const char *name;
+  const char *arg = NULL;
+
+  SVN_ERR(svn_dirent_get_absolute(&local_abspath, "", scratch_pool));
+
+  if (os->ind < os->argc)
+    {
+      SVN_ERR(get_next_argument(&name, os, scratch_pool, scratch_pool));
+    }
+  else
+    {
+      SVN_ERR(name_of_youngest(&name,
+                               local_abspath, ctx, scratch_pool, scratch_pool));
+      SVN_ERR(svn_cmdline_printf(scratch_pool,
+                                 _("unshelving the youngest shelf, '%s'\n"),
+                                 name));
+    }
+
+  /* Which checkpoint number? */
+  if (os->ind < os->argc)
+    SVN_ERR(get_next_argument(&arg, os, scratch_pool, scratch_pool));
+
+  if (os->ind < os->argc)
+    return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
+                            _("Too many arguments"));
+
+  if (opt_state->quiet)
+    ctx->notify_func2 = NULL; /* Easy out: avoid unneeded work */
+
+  SVN_ERR(shelf_restore(name, arg,
+                        opt_state->dry_run, opt_state->quiet,
+                        opt_state->force /*force_already_modified*/,
+                        local_abspath, ctx, scratch_pool));
+
+  if (opt_state->drop)
+    {
+      SVN_ERR(shelf_drop(name, local_abspath,
+                         opt_state->dry_run, opt_state->quiet,
+                         ctx, scratch_pool));
+    }
+  return SVN_NO_ERROR;
+}
+
+/* This implements the `svn_opt_subcommand_t' interface. */
+static svn_error_t *
+svn_cl__shelf_list(apr_getopt_t *os,
+                   void *baton,
+                   apr_pool_t *pool)
+{
+  svn_cl__opt_state_t *opt_state = ((svn_cl__cmd_baton_t *) baton)->opt_state;
+  svn_client_ctx_t *ctx = ((svn_cl__cmd_baton_t *) baton)->ctx;
+  apr_array_header_t *targets = NULL;
+  apr_pool_t *iterpool = svn_pool_create(pool);
+  int i;
+
+  SVN_ERR(svn_cl__args_to_target_array_print_reserved(&targets, os,
+                                                      opt_state->targets,
+                                                      ctx, FALSE, pool));
+  /* Add "." if user passed 0 arguments */
+  svn_opt_push_implicit_dot_target(targets, pool);
+
+  for (i = 0; i < targets->nelts; ++i)
+    {
+      const char *local_abspath;
+      const char *target = APR_ARRAY_IDX(targets, i, const char *);
+
+      svn_pool_clear(iterpool);
+
+      SVN_ERR(svn_dirent_get_absolute(&local_abspath, target, iterpool));
+
+      SVN_ERR(shelves_list(local_abspath,
+                           opt_state->quiet,
+                           ctx, iterpool));
+    }
+
+  svn_pool_destroy(iterpool);
+
+  return SVN_NO_ERROR;
+}
+
+/* "svn shelf-list-by-paths [PATH...]"
+ *
+ * TARGET_RELPATHS are all within the same WC, relative to WC_ROOT_ABSPATH.
+ */
+static svn_error_t *
+shelf_list_by_paths(apr_array_header_t *target_relpaths,
+                    const char *wc_root_abspath,
+                    svn_client_ctx_t *ctx,
+                    apr_pool_t *scratch_pool)
+{
+  apr_array_header_t *shelves;
+  apr_hash_t *paths_to_shelf_name = apr_hash_make(scratch_pool);
+  apr_array_header_t *array;
+  int i;
+
+  SVN_ERR(list_sorted_by_date(&shelves,
+                              wc_root_abspath, ctx, scratch_pool));
+
+  /* Check paths are valid */
+  for (i = 0; i < target_relpaths->nelts; i++)
+    {
+      char *target_relpath = APR_ARRAY_IDX(target_relpaths, i, char *);
+
+      if (svn_path_is_url(target_relpath))
+        return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
+                                 _("'%s' is not a local path"), target_relpath);
+      SVN_ERR_ASSERT(svn_relpath_is_canonical(target_relpath));
+    }
+
+  /* Find the most recent shelf for each affected path */
+  for (i = 0; i < shelves->nelts; i++)
+    {
+      svn_sort__item_t *item = &APR_ARRAY_IDX(shelves, i, svn_sort__item_t);
+      const char *name = item->key;
+      svn_client__shelf2_t *shelf;
+      svn_client__shelf2_version_t *shelf_version;
+      apr_hash_t *shelf_paths;
+      int j;
+
+      SVN_ERR(svn_client__shelf2_open_existing(&shelf,
+                                              name, wc_root_abspath,
+                                              ctx, scratch_pool));
+      SVN_ERR(svn_client__shelf2_get_newest_version(&shelf_version, shelf,
+                                                   scratch_pool, scratch_pool));
+      if (!shelf_version)
+        continue;
+      SVN_ERR(svn_client__shelf2_paths_changed(&shelf_paths,
+                                              shelf_version,
+                                              scratch_pool, scratch_pool));
+      for (j = 0; j < target_relpaths->nelts; j++)
+        {
+          char *target_relpath = APR_ARRAY_IDX(target_relpaths, j, char *);
+          apr_hash_index_t *hi;
+
+          for (hi = apr_hash_first(scratch_pool, shelf_paths);
+               hi; hi = apr_hash_next(hi))
+            {
+              const char *shelf_path = apr_hash_this_key(hi);
+
+              if (svn_relpath_skip_ancestor(target_relpath, shelf_path))
+                {
+                  if (! svn_hash_gets(paths_to_shelf_name, shelf_path))
+                    {
+                      svn_hash_sets(paths_to_shelf_name, shelf_path, shelf->name);
+                    }
+                }
+            }
+        }
+    }
+
+  /* Print the results. */
+  array = svn_sort__hash(paths_to_shelf_name,
+                         svn_sort_compare_items_as_paths,
+                         scratch_pool);
+  for (i = 0; i < array->nelts; i++)
+    {
+      svn_sort__item_t *item = &APR_ARRAY_IDX(array, i, svn_sort__item_t);
+      const char *path = item->key;
+      const char *name = item->value;
+
+      SVN_ERR(svn_cmdline_printf(scratch_pool, "%-20.20s %s\n",
+                                 name,
+                                 svn_dirent_local_style(path, scratch_pool)));
+    }
+  return SVN_NO_ERROR;
+}
+
+/* This implements the `svn_opt_subcommand_t' interface. */
+static svn_error_t *
+svn_cl__shelf_list_by_paths(apr_getopt_t *os,
+                            void *baton,
+                            apr_pool_t *pool)
+{
+  svn_cl__opt_state_t *opt_state = ((svn_cl__cmd_baton_t *) baton)->opt_state;
+  svn_client_ctx_t *ctx = ((svn_cl__cmd_baton_t *) baton)->ctx;
+  const char *wc_root_abspath;
+  apr_array_header_t *targets;
+
+  /* Parse the remaining arguments as paths. */
+  SVN_ERR(targets_relative_to_a_wc(&wc_root_abspath, &targets,
+                                   os, opt_state->targets,
+                                   ctx, pool, pool));
+
+  SVN_ERR(shelf_list_by_paths(targets, wc_root_abspath, ctx, pool));
+  return SVN_NO_ERROR;
+}
+
+/* This implements the `svn_opt_subcommand_t' interface. */
+static svn_error_t *
+svn_cl__shelf_diff(apr_getopt_t *os,
+                   void *baton,
+                   apr_pool_t *pool)
+{
+  svn_cl__opt_state_t *opt_state = ((svn_cl__cmd_baton_t *) baton)->opt_state;
+  svn_client_ctx_t *ctx = ((svn_cl__cmd_baton_t *) baton)->ctx;
+  const char *local_abspath;
+  const char *name;
+  const char *arg = NULL;
+
+  SVN_ERR(svn_dirent_get_absolute(&local_abspath, "", pool));
+
+  SVN_ERR(get_next_argument(&name, os, pool, pool));
+
+  /* Which checkpoint number? */
+  if (os->ind < os->argc)
+    SVN_ERR(get_next_argument(&arg, os, pool, pool));
+
+  if (os->ind < os->argc)
+    return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
+                            _("Too many arguments"));
+
+  SVN_ERR(shelf_diff(name, arg, local_abspath,
+                     opt_state->diff.summarize,
+                     opt_state->depth, opt_state->ignore_ancestry,
+                     ctx, pool));
+
+  return SVN_NO_ERROR;
+}
+
+/* This implements the `svn_opt_subcommand_t' interface. */
+static svn_error_t *
+svn_cl__shelf_drop(apr_getopt_t *os,
+                   void *baton,
+                   apr_pool_t *pool)
+{
+  svn_cl__opt_state_t *opt_state = ((svn_cl__cmd_baton_t *) baton)->opt_state;
+  svn_client_ctx_t *ctx = ((svn_cl__cmd_baton_t *) baton)->ctx;
+  const char *name;
+  apr_array_header_t *targets = NULL;
+  apr_pool_t *iterpool = svn_pool_create(pool);
+  int i;
+
+  SVN_ERR(get_next_argument(&name, os, pool, pool));
+
+  SVN_ERR(svn_cl__args_to_target_array_print_reserved(&targets, os,
+                                                      opt_state->targets,
+                                                      ctx, FALSE, pool));
+  svn_opt_push_implicit_dot_target(targets, pool);
+
+  for (i = 0; i < targets->nelts; ++i)
+    {
+      const char *local_abspath;
+      const char *target = APR_ARRAY_IDX(targets, i, const char *);
+
+      svn_pool_clear(iterpool);
+
+      SVN_ERR(svn_dirent_get_absolute(&local_abspath, target, iterpool));
+      SVN_ERR(shelf_drop(name, local_abspath,
+                         opt_state->dry_run, opt_state->quiet,
+                         ctx, iterpool));
+    }
+
+  svn_pool_destroy(iterpool);
+
+  return SVN_NO_ERROR;
+}
+
+/* This implements the `svn_opt_subcommand_t' interface. */
+static svn_error_t *
+svn_cl__shelf_log(apr_getopt_t *os,
+                  void *baton,
+                  apr_pool_t *pool)
+{
+  svn_cl__opt_state_t *opt_state = ((svn_cl__cmd_baton_t *) baton)->opt_state;
+  svn_client_ctx_t *ctx = ((svn_cl__cmd_baton_t *) baton)->ctx;
+  const char *name;
+  apr_array_header_t *targets = NULL;
+  apr_pool_t *iterpool = svn_pool_create(pool);
+  int i;
+
+  SVN_ERR(get_next_argument(&name, os, pool, pool));
+
+  SVN_ERR(svn_cl__args_to_target_array_print_reserved(&targets, os,
+                                                      opt_state->targets,
+                                                      ctx, FALSE, pool));
+  svn_opt_push_implicit_dot_target(targets, pool);
+
+  for (i = 0; i < targets->nelts; ++i)
+    {
+      const char *local_abspath;
+      const char *target = APR_ARRAY_IDX(targets, i, const char *);
+
+      svn_pool_clear(iterpool);
+
+      SVN_ERR(svn_dirent_get_absolute(&local_abspath, target, iterpool));
+      SVN_ERR(shelf_log(name, local_abspath, ctx, iterpool));
+    }
+
+  svn_pool_destroy(iterpool);
+
+  return SVN_NO_ERROR;
+}
+
+const svn_opt_subcommand_desc3_t
+svn_cl__cmd_table_shelf2[] =
+{
+  { "x-shelf-diff", svn_cl__shelf_diff, {0}, {N_(
+     "Show shelved changes as a diff.\n"
+     "usage: x-shelf-diff SHELF [VERSION]\n"
+     "\n"), N_(
+     "  Show the changes in SHELF:VERSION (default: latest) as a diff.\n"
+     "\n"), N_(
+     "  See also: 'svn diff --cl=svn:shelf:SHELF' which supports most options of\n"
+     "  'svn diff'.\n"
+     "\n"), N_(
+     "  The shelving feature is EXPERIMENTAL. This command is likely to change\n"
+     "  in the next release, and there is no promise of backward compatibility.\n"
+    )},
+    {opt_summarize},
+  },
+
+  { "x-shelf-drop", svn_cl__shelf_drop, {0}, {N_(
+     "Delete a shelf.\n"
+     "usage: x-shelf-drop SHELF [PATH ...]\n"
+     "\n"), N_(
+     "  Delete the shelves named SHELF from the working copies containing PATH\n"
+     "  (default PATH is '.')\n"
+     "\n"), N_(
+     "  The shelving feature is EXPERIMENTAL. This command is likely to change\n"
+     "  in the next release, and there is no promise of backward compatibility.\n"
+    )},
+  },
+
+  { "x-shelf-list", svn_cl__shelf_list, {"x-shelves"}, {N_(
+     "List shelves.\n"
+     "usage: x-shelf-list [PATH ...]\n"
+     "\n"), N_(
+     "  List shelves for each working copy containing PATH (default is '.')\n"
+     "  Include the first line of any log message and some details about the\n"
+     "  contents of the shelf, unless '-q' is given.\n"
+     "\n"), N_(
+     "  The shelving feature is EXPERIMENTAL. This command is likely to change\n"
+     "  in the next release, and there is no promise of backward compatibility.\n"
+    )},
+    {'q', 'v'}
+  },
+
+  { "x-shelf-list-by-paths", svn_cl__shelf_list_by_paths, {0}, {N_(
+     "List which shelf affects each path.\n"
+     "usage: x-shelf-list-by-paths [PATH...]\n"
+     "\n"), N_(
+     "  List which shelf most recently affects each path below the given PATHs.\n"
+     "\n"), N_(
+     "  The shelving feature is EXPERIMENTAL. This command is likely to change\n"
+     "  in the next release, and there is no promise of backward compatibility.\n"
+    )},
+  },
+
+  { "x-shelf-log", svn_cl__shelf_log, {0}, {N_(
+     "Show the versions of a shelf.\n"
+     "usage: x-shelf-log SHELF [PATH...]\n"
+     "\n"), N_(
+     "  Show all versions of SHELF for each working copy containing PATH (the\n"
+     "  default PATH is '.').\n"
+     "\n"), N_(
+     "  The shelving feature is EXPERIMENTAL. This command is likely to change\n"
+     "  in the next release, and there is no promise of backward compatibility.\n"
+    )},
+    {'q', 'v'}
+  },
+
+  { "x-shelf-save", svn_cl__shelf_save, {0}, {N_(
+     "Copy local changes onto a new version of a shelf.\n"
+     "usage: x-shelf-save SHELF [PATH...]\n"
+     "\n"), N_(
+     "  Save local changes in the given PATHs as a new version of SHELF.\n"
+     "  The shelf's log message can be set with -m, -F, etc.\n"
+     "\n"), N_(
+     "  The same as 'svn shelve --keep-local'.\n"
+     "\n"), N_(
+     "  The shelving feature is EXPERIMENTAL. This command is likely to change\n"
+     "  in the next release, and there is no promise of backward compatibility.\n"
+    )},
+    {'q', opt_dry_run,
+     opt_depth, opt_targets, opt_changelist,
+     SVN_CL__LOG_MSG_OPTIONS,
+    }
+  },
+
+  { "x-shelve", svn_cl__shelf_shelve, {0}, {N_(
+     "Move local changes onto a shelf.\n"
+     "usage: x-shelve [--keep-local] SHELF [PATH...]\n"
+     "\n"), N_(
+     "  Save the local changes in the given PATHs to a new or existing SHELF.\n"
+     "  Revert those changes from the WC unless '--keep-local' is given.\n"
+     "  The shelf's log message can be set with -m, -F, etc.\n"
+     "\n"), N_(
+     "  'svn shelve --keep-local' is the same as 'svn shelf-save'.\n"
+     "\n"), N_(
+     "  The kinds of change you can shelve are committable changes to files and\n"
+     "  properties, except the following kinds which are not yet supported:\n"
+     "     * copies and moves\n"
+     "     * mkdir and rmdir\n"
+     "  Uncommittable states such as conflicts, unversioned and missing cannot\n"
+     "  be shelved.\n"
+     "\n"), N_(
+     "  To bring back shelved changes, use 'svn unshelve SHELF'.\n"
+     "\n"), N_(
+     "  Shelves are currently stored under <WC>/.svn/experimental/shelves/ .\n"
+     "  (In Subversion 1.10, shelves were stored under <WC>/.svn/shelves/ as\n"
+     "  patch files. To recover a shelf created by 1.10, either use a 1.10\n"
+     "  client to find and unshelve it, or find the patch file and use any\n"
+     "  1.10 or later 'svn patch' to apply it.)\n"
+     "\n"), N_(
+     "  The shelving feature is EXPERIMENTAL. This command is likely to change\n"
+     "  in the next release, and there is no promise of backward compatibility.\n"
+    )},
+    {'q', opt_dry_run, opt_keep_local,
+     opt_depth, opt_targets, opt_changelist,
+     SVN_CL__LOG_MSG_OPTIONS,
+    } },
+
+  { "x-unshelve", svn_cl__shelf_unshelve, {0}, {N_(
+     "Copy shelved changes back into the WC.\n"
+     "usage: x-unshelve [--drop] [SHELF [VERSION]]\n"
+     "\n"), N_(
+     "  Apply the changes stored in SHELF to the working copy.\n"
+     "  SHELF defaults to the newest shelf.\n"
+     "\n"), N_(
+     "  Apply the newest version of the shelf, by default. If VERSION is\n"
+     "  specified, apply that version and discard all versions newer than that.\n"
+     "  In any case, retain the unshelved version and versions older than that\n"
+     "  (unless --drop is specified).\n"
+     "\n"), N_(
+     "  With --drop, delete the entire shelf (like 'svn shelf-drop') after\n"
+     "  successfully unshelving with no conflicts.\n"
+     "\n"), N_(
+     "  The working files involved should be in a clean, unmodified state\n"
+     "  before using this command. To roll back to an older version of the\n"
+     "  shelf, first ensure any current working changes are removed, such as\n"
+     "  by shelving or reverting them, and then unshelve the desired version.\n"
+     "\n"), N_(
+     "  Unshelve normally refuses to apply any changes if any path involved is\n"
+     "  already modified (or has any other abnormal status) in the WC. With\n"
+     "  --force, it does not check and may error out and/or produce partial or\n"
+     "  unexpected results.\n"
+     "\n"), N_(
+     "  The shelving feature is EXPERIMENTAL. This command is likely to change\n"
+     "  in the next release, and there is no promise of backward compatibility.\n"
+    )},
+    {opt_drop, 'q', opt_dry_run, opt_force} },
+
+  { NULL, NULL, {0}, {NULL}, {0} }
+};
+

Propchange: subversion/branches/decouple-shelving-cli/subversion/svn/shelf2-cmd.c
------------------------------------------------------------------------------
    svn:eol-style = native

Added: subversion/branches/decouple-shelving-cli/subversion/svn/shelf2-cmd.h
URL: http://svn.apache.org/viewvc/subversion/branches/decouple-shelving-cli/subversion/svn/shelf2-cmd.h?rev=1874634&view=auto
 ==============================================================================
--- subversion/branches/decouple-shelving-cli/subversion/svn/shelf2-cmd.h (added)
+++ subversion/branches/decouple-shelving-cli/subversion/svn/shelf2-cmd.h Fri Feb 28 \
22:15:15 2020 @@ -0,0 +1,49 @@
+/*
+ * shelf2-cmd.h:  experimental shelving v2
+ *
+ * ====================================================================
+ *    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.
+ * ====================================================================
+ */
+
+/* ==================================================================== */
+
+
+
+#ifndef SVN_SHELF2_CMD_H
+#define SVN_SHELF2_CMD_H
+
+/*** Includes. ***/
+#include <apr_getopt.h>
+
+#include "svn_opt.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+
+extern const svn_opt_subcommand_desc3_t svn_cl__cmd_table_shelf2[];
+/*extern const apr_getopt_option_t svn_cl__options_shelving2[];*/
+
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+
+#endif /* SVN_SHELF2_CMD_H */

Propchange: subversion/branches/decouple-shelving-cli/subversion/svn/shelf2-cmd.h
------------------------------------------------------------------------------
    svn:eol-style = native

Modified: subversion/branches/decouple-shelving-cli/subversion/svn/svn.c
URL: http://svn.apache.org/viewvc/subversion/branches/decouple-shelving-cli/subversion/svn/svn.c?rev=1874634&r1=1874633&r2=1874634&view=diff
 ==============================================================================
--- subversion/branches/decouple-shelving-cli/subversion/svn/svn.c (original)
+++ subversion/branches/decouple-shelving-cli/subversion/svn/svn.c Fri Feb 28 \
22:15:15 2020 @@ -52,6 +52,7 @@
 #include "svn_hash.h"
 #include "svn_version.h"
 #include "cl.h"
+#include "shelf2-cmd.h"
 #include "shelf-cmd.h"
 
 #include "private/svn_opt_private.h"
@@ -2026,6 +2027,7 @@ sub_main(int *exit_code, int argc, const
   svn_cl__opt_state_t opt_state = { 0, { 0 } };
   svn_client_ctx_t *ctx;
   apr_array_header_t *received_opts;
+  const char *exp_cmds;
   int i;
   const svn_opt_subcommand_desc3_t *subcommand = NULL;
   const char *dash_F_arg = NULL;
@@ -2066,8 +2068,16 @@ sub_main(int *exit_code, int argc, const
   /* Init the temporary buffer. */
   svn_membuf__create(&buf, 0, pool);
 
-  /* Add externally defined commands */
-  add_commands(svn_cl__cmd_table_shelf3, pool);
+  /* Add experimental commands, if requested */
+  exp_cmds = getenv("SVN_EXPERIMENTAL_COMMANDS");
+  if (!exp_cmds || strstr(exp_cmds, "shelf3"))
+    {
+      add_commands(svn_cl__cmd_table_shelf3, pool);
+    }
+  else if (exp_cmds && strstr(exp_cmds, "shelf2"))
+    {
+      add_commands(svn_cl__cmd_table_shelf2, pool);
+    }
 
   /* Begin processing arguments. */
   opt_state.start_revision.kind = svn_opt_revision_unspecified;

Added: subversion/branches/decouple-shelving-cli/subversion/tests/cmdline/shelf2_tests.py
                
URL: http://svn.apache.org/viewvc/subversion/branches/decouple-shelving-cli/subversion/tests/cmdline/shelf2_tests.py?rev=1874634&view=auto
 ==============================================================================
--- subversion/branches/decouple-shelving-cli/subversion/tests/cmdline/shelf2_tests.py \
                (added)
+++ subversion/branches/decouple-shelving-cli/subversion/tests/cmdline/shelf2_tests.py \
Fri Feb 28 22:15:15 2020 @@ -0,0 +1,1030 @@
+#!/usr/bin/env python
+#
+#  shelf_tests.py:  testing shelving
+#
+#  Subversion is a tool for revision control.
+#  See http://subversion.apache.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, stat, re, os, logging
+
+logger = logging.getLogger()
+
+# Our testing module
+import svntest
+from svntest import wc
+from svntest.verify import make_diff_header, make_no_diff_deleted_header, \
+                           make_git_diff_header, make_diff_prop_header, \
+                           make_diff_prop_val, make_diff_prop_deleted, \
+                           make_diff_prop_added, make_diff_prop_modified
+
+# (abbreviation)
+Skip = svntest.testcase.Skip_deco
+SkipUnless = svntest.testcase.SkipUnless_deco
+XFail = svntest.testcase.XFail_deco
+Issues = svntest.testcase.Issues_deco
+Issue = svntest.testcase.Issue_deco
+Wimp = svntest.testcase.Wimp_deco
+Item = wc.StateItem
+
+def shelf2_enabled():
+  v = os.getenv('SVN_EXPERIMENTAL_COMMANDS')
+  return v is not None and v.find('shelf2') >= 0
+
+#----------------------------------------------------------------------
+
+def state_from_status(wc_dir,
+                      v=True, u=True, q=True):
+  opts = ()
+  if v:
+    opts += ('-v',)
+  if u:
+    opts += ('-u',)
+  if q:
+    opts += ('-q',)
+  _, output, _ = svntest.main.run_svn(None, 'status', wc_dir, *opts)
+  return svntest.wc.State.from_status(output, wc_dir)
+
+def get_wc_state(wc_dir):
+  """Return a description of the WC state. Include as much info as shelving
+     should be capable of restoring.
+  """
+  return (state_from_status(wc_dir),
+          svntest.wc.State.from_wc(wc_dir, load_props=True),
+          )
+
+def check_wc_state(wc_dir, expected):
+  """Check a description of the WC state. Include as much info as shelving
+     should be capable of restoring.
+  """
+  expect_st, expect_wc = expected
+  actual_st, actual_wc = get_wc_state(wc_dir)
+
+  # Verify actual status against expected status.
+  try:
+    expect_st.compare_and_display('status', actual_st)
+  except svntest.tree.SVNTreeError:
+    svntest.actions._log_tree_state("EXPECT STATUS TREE:", expect_st.old_tree(),
+                                    wc_dir)
+    svntest.actions._log_tree_state("ACTUAL STATUS TREE:", actual_st.old_tree(),
+                                    wc_dir)
+    raise
+
+  # Verify actual WC against expected WC.
+  try:
+    expect_wc.compare_and_display('status', actual_wc)
+  except svntest.tree.SVNTreeError:
+    svntest.actions._log_tree_state("EXPECT WC TREE:", expect_wc.old_tree(),
+                                    wc_dir)
+    svntest.actions._log_tree_state("ACTUAL WC TREE:", actual_wc.old_tree(),
+                                    wc_dir)
+    raise
+
+def shelve_unshelve_verify(sbox, modifier, cannot_shelve=False):
+  """Round-trip: shelve; verify all changes are reverted;
+     unshelve; verify all changes are restored.
+  """
+
+  wc_dir = sbox.wc_dir
+  virginal_state = get_wc_state(wc_dir)
+
+  # Make some changes to the working copy
+  modifier(sbox)
+
+  # Save the modified state
+  modified_state = get_wc_state(wc_dir)
+
+  if cannot_shelve:
+    svntest.actions.run_and_verify_svn(None, '.* could not be shelved.*',
+                                       'x-shelve', 'foo')
+    return
+
+  # Shelve; check there are no longer any modifications
+  svntest.actions.run_and_verify_svn(None, [],
+                                     'x-shelve', 'foo')
+  check_wc_state(wc_dir, virginal_state)
+
+  # List; ensure the shelf is listed
+  expected_output = svntest.verify.RegexListOutput(
+    [r'foo\s*version \d+.*',
+     r' ',
+    ])
+  svntest.actions.run_and_verify_svn(expected_output, [], 'x-shelves')
+
+  # Unshelve; check the original modifications are here again
+  svntest.actions.run_and_verify_svn(None, [],
+                                     'x-unshelve', 'foo')
+  check_wc_state(wc_dir, modified_state)
+
+#----------------------------------------------------------------------
+
+def shelve_unshelve(sbox, modifier, cannot_shelve=False):
+  """Round-trip: build 'sbox'; apply changes by calling 'modifier(sbox)';
+     shelve and unshelve; verify changes are fully reverted and restored.
+  """
+
+  if not sbox.is_built():
+    sbox.build()
+  was_cwd = os.getcwd()
+  os.chdir(sbox.wc_dir)
+  sbox.wc_dir = ''
+
+  shelve_unshelve_verify(sbox, modifier, cannot_shelve)
+
+  os.chdir(was_cwd)
+
+######################################################################
+# Tests
+#
+#   Each test must return on success or raise on failure.
+
+@SkipUnless(shelf2_enabled)
+def shelve_text_mods(sbox):
+  "shelve text mods"
+
+  def modifier(sbox):
+    sbox.simple_append('A/mu', 'appended mu text')
+
+  shelve_unshelve(sbox, modifier)
+
+#----------------------------------------------------------------------
+
+@SkipUnless(shelf2_enabled)
+def shelve_prop_changes(sbox):
+  "shelve prop changes"
+
+  def modifier(sbox):
+    sbox.simple_propset('p', 'v', 'A')
+    sbox.simple_propset('p', 'v', 'A/mu')
+
+  shelve_unshelve(sbox, modifier)
+
+#----------------------------------------------------------------------
+
+@SkipUnless(shelf2_enabled)
+def shelve_adds(sbox):
+  "shelve adds"
+
+  def modifier(sbox):
+    sbox.simple_add_text('A new file\n', 'A/new')
+    sbox.simple_add_text('A new file\n', 'A/new2')
+    sbox.simple_propset('p', 'v', 'A/new2')
+
+  shelve_unshelve(sbox, modifier)
+
+#----------------------------------------------------------------------
+
+@Issue(4709)
+@SkipUnless(shelf2_enabled)
+def shelve_deletes(sbox):
+  "shelve deletes"
+
+  def modifier(sbox):
+    sbox.simple_rm('A/mu')
+
+  shelve_unshelve(sbox, modifier)
+
+#----------------------------------------------------------------------
+
+@SkipUnless(shelf2_enabled)
+def shelve_replace(sbox):
+  "shelve replace"
+
+  def modifier(sbox):
+    sbox.simple_rm('A/mu')
+    sbox.simple_add_text('Replacement\n', 'A/mu')
+    sbox.simple_propset('p', 'v', 'A/mu')
+
+  shelve_unshelve(sbox, modifier)
+
+#----------------------------------------------------------------------
+
+@SkipUnless(shelf2_enabled)
+def shelve_empty_adds(sbox):
+  "shelve empty adds"
+  sbox.build(empty=True)
+
+  def modifier(sbox):
+    sbox.simple_add_text('', 'empty')
+    sbox.simple_add_text('', 'empty-with-prop')
+    sbox.simple_propset('p', 'v', 'empty-with-prop')
+
+  shelve_unshelve(sbox, modifier)
+
+#----------------------------------------------------------------------
+
+@SkipUnless(shelf2_enabled)
+def shelve_empty_deletes(sbox):
+  "shelve empty deletes"
+  sbox.build(empty=True)
+  sbox.simple_add_text('', 'empty')
+  sbox.simple_add_text('', 'empty-with-prop')
+  sbox.simple_propset('p', 'v', 'empty-with-prop')
+  sbox.simple_commit()
+
+  def modifier(sbox):
+    sbox.simple_rm('empty', 'empty-with-prop')
+
+  shelve_unshelve(sbox, modifier)
+
+#----------------------------------------------------------------------
+
+@SkipUnless(shelf2_enabled)
+def shelve_from_inner_path(sbox):
+  "shelve from inner path"
+
+  def modifier(sbox):
+    sbox.simple_append('A/mu', 'appended mu text')
+
+  sbox.build()
+  was_cwd = os.getcwd()
+  os.chdir(sbox.ospath('A'))
+  sbox.wc_dir = '..'
+
+  shelve_unshelve_verify(sbox, modifier)
+
+  os.chdir(was_cwd)
+
+#----------------------------------------------------------------------
+
+def save_revert_restore(sbox, modifier1, modifier2):
+  "Save 2 checkpoints; revert; restore 1st"
+
+  sbox.build()
+  was_cwd = os.getcwd()
+  os.chdir(sbox.wc_dir)
+  sbox.wc_dir = ''
+  wc_dir = ''
+
+  initial_state = get_wc_state(wc_dir)
+
+  # Make some changes to the working copy
+  modifier1(sbox)
+
+  # Remember the modified state
+  modified_state1 = get_wc_state(wc_dir)
+
+  # Save a checkpoint; check nothing changed
+  svntest.actions.run_and_verify_svn(None, [],
+                                     'x-shelf-save', 'foo')
+  check_wc_state(wc_dir, modified_state1)
+
+  # Modify again; remember the state; save a checkpoint
+  modifier2(sbox)
+  modified_state2 = get_wc_state(wc_dir)
+  svntest.actions.run_and_verify_svn(None, [],
+                                     'x-shelf-save', 'foo')
+  check_wc_state(wc_dir, modified_state2)
+
+  # Revert
+  svntest.actions.run_and_verify_svn(None, [],
+                                     'revert', '-R', '.')
+  check_wc_state(wc_dir, initial_state)
+
+  # Restore; check the original modifications are here again
+  svntest.actions.run_and_verify_svn(None, [],
+                                     'x-unshelve', 'foo', '1')
+  check_wc_state(wc_dir, modified_state1)
+
+  os.chdir(was_cwd)
+
+#----------------------------------------------------------------------
+
+@SkipUnless(shelf2_enabled)
+def checkpoint_basic(sbox):
+  "checkpoint basic"
+
+  def modifier1(sbox):
+    sbox.simple_append('A/mu', 'appended mu text\n')
+
+  def modifier2(sbox):
+    sbox.simple_append('iota', 'appended iota text\n')
+    sbox.simple_append('A/mu', 'appended another line\n')
+
+  save_revert_restore(sbox, modifier1, modifier2)
+
+#----------------------------------------------------------------------
+
+@Issue(3747)
+@SkipUnless(shelf2_enabled)
+def shelve_mergeinfo(sbox):
+  "shelve mergeinfo"
+
+  def modifier(sbox):
+    sbox.simple_propset('svn:mergeinfo', '/trunk/A:1-3,10', 'A')
+    sbox.simple_propset('svn:mergeinfo', '/trunk/A/mu:1-3,10', 'A/mu')
+
+  shelve_unshelve(sbox, modifier)
+
+#----------------------------------------------------------------------
+
+@SkipUnless(shelf2_enabled)
+def unshelve_refuses_if_conflicts(sbox):
+  "unshelve refuses if conflicts"
+
+  def modifier1(sbox):
+    sbox.simple_append('alpha', 'A-mod1\nB\nC\nD\n', truncate=True)
+    sbox.simple_append('beta', 'A-mod1\nB\nC\nD\n', truncate=True)
+
+  def modifier2(sbox):
+    sbox.simple_append('beta', 'A-mod2\nB\nC\nD\n', truncate=True)
+
+  sbox.build(empty=True)
+  was_cwd = os.getcwd()
+  os.chdir(sbox.wc_dir)
+  sbox.wc_dir = ''
+  wc_dir = ''
+
+  sbox.simple_add_text('A\nB\nC\nD\n', 'alpha')
+  sbox.simple_add_text('A\nB\nC\nD\n', 'beta')
+  sbox.simple_commit()
+  initial_state = get_wc_state(wc_dir)
+
+  # Make initial mods; remember this modified state
+  modifier1(sbox)
+  modified_state1 = get_wc_state(wc_dir)
+  assert modified_state1 != initial_state
+
+  # Shelve; check there are no longer any local mods
+  svntest.actions.run_and_verify_svn(None, [],
+                                     'x-shelve', 'foo')
+  check_wc_state(wc_dir, initial_state)
+
+  # Make a different local mod that will conflict with the shelf
+  modifier2(sbox)
+  modified_state2 = get_wc_state(wc_dir)
+
+  # Try to unshelve; check it fails with an error about a conflict
+  svntest.actions.run_and_verify_svn(None, '.*[Cc]onflict.*',
+                                     'x-unshelve', 'foo')
+  # Check nothing changed in the attempt
+  check_wc_state(wc_dir, modified_state2)
+
+#----------------------------------------------------------------------
+
+@SkipUnless(shelf2_enabled)
+def shelve_binary_file_mod(sbox):
+  "shelve binary file mod"
+
+  sbox.build(empty=True)
+
+  existing_files = ['A/B/existing']
+  mod_files = ['bin', 'A/B/bin']
+
+  sbox.simple_mkdir('A', 'A/B')
+  for f in existing_files + mod_files:
+    sbox.simple_add_text('\0\1\2\3\4\5', f)
+  sbox.simple_commit()
+
+  def modifier(sbox):
+    for f in mod_files:
+      sbox.simple_append(f, '\6\5\4\3\2\1\0', truncate=True)
+
+  shelve_unshelve(sbox, modifier)
+
+#----------------------------------------------------------------------
+
+@SkipUnless(shelf2_enabled)
+def shelve_binary_file_add(sbox):
+  "shelve binary file add"
+
+  sbox.build(empty=True)
+
+  existing_files = ['A/B/existing']
+  mod_files = ['bin', 'A/B/bin']
+
+  sbox.simple_mkdir('A', 'A/B')
+  for f in existing_files:
+    sbox.simple_add_text('\0\1\2\3\4\5', f)
+  sbox.simple_commit()
+
+  def modifier(sbox):
+    for f in mod_files:
+      sbox.simple_add_text('\0\1\2\3\4\5', f)
+
+  shelve_unshelve(sbox, modifier)
+
+#----------------------------------------------------------------------
+
+@SkipUnless(shelf2_enabled)
+def shelve_binary_file_del(sbox):
+  "shelve binary file del"
+
+  sbox.build(empty=True)
+
+  existing_files = ['A/B/existing']
+  mod_files = ['bin', 'A/B/bin']
+
+  sbox.simple_mkdir('A', 'A/B')
+  for f in existing_files + mod_files:
+    sbox.simple_add_text('\0\1\2\3\4\5', f)
+  sbox.simple_commit()
+
+  def modifier(sbox):
+    for f in mod_files:
+      sbox.simple_rm(f)
+
+  shelve_unshelve(sbox, modifier)
+
+#----------------------------------------------------------------------
+
+@SkipUnless(shelf2_enabled)
+def shelve_binary_file_replace(sbox):
+  "shelve binary file replace"
+
+  sbox.build(empty=True)
+
+  existing_files = ['A/B/existing']
+  mod_files = ['bin', 'A/B/bin']
+
+  sbox.simple_mkdir('A', 'A/B')
+  for f in existing_files + mod_files:
+    sbox.simple_add_text('\0\1\2\3\4\5', f)
+  sbox.simple_commit()
+
+  def modifier(sbox):
+    for f in mod_files:
+      sbox.simple_rm(f)
+      sbox.simple_add_text('\6\5\4\3\2\1\0', f)
+
+  shelve_unshelve(sbox, modifier)
+
+#----------------------------------------------------------------------
+
+@SkipUnless(shelf2_enabled)
+def shelve_with_log_message(sbox):
+  "shelve with log message"
+
+  sbox.build(empty=True)
+  was_cwd = os.getcwd()
+  os.chdir(sbox.wc_dir)
+  sbox.wc_dir = ''
+
+  sbox.simple_add_text('New file', 'f')
+  log_message = 'Log message for foo'
+  svntest.actions.run_and_verify_svn(None, [],
+                                     'x-shelve', 'foo', '-m', log_message)
+  expected_output = svntest.verify.RegexListOutput(
+    ['foo .*',
+     ' ' + log_message
+    ])
+  svntest.actions.run_and_verify_svn(expected_output, [],
+                                     'x-shelf-list')
+
+  os.chdir(was_cwd)
+
+#----------------------------------------------------------------------
+
+def run_and_verify_status(wc_dir_name, status_tree, changelists=[]):
+  """Run 'status' on WC_DIR_NAME and compare it with the
+  expected STATUS_TREE.
+  Returns on success, raises on failure."""
+
+  if not isinstance(status_tree, wc.State):
+    raise TypeError('wc.State tree expected')
+
+  cl_opts = ('--cl=' + cl for cl in changelists)
+  exit_code, output, errput = svntest.main.run_svn(None, 'status', '-q',
+                                                   wc_dir_name, *cl_opts)
+
+  actual_status = svntest.wc.State.from_status(output, wc_dir=wc_dir_name)
+
+  # Verify actual output against expected output.
+  try:
+    status_tree.compare_and_display('status', actual_status)
+  except svntest.tree.SVNTreeError:
+    svntest.actions._log_tree_state("ACTUAL STATUS TREE:", actual_status.old_tree(),
+                                    wc_dir_name)
+    raise
+
+def run_and_verify_shelf_status(wc_dir, expected_status, shelf):
+  run_and_verify_status(wc_dir, expected_status,
+                        changelists=['svn:shelf:' + shelf])
+
+@SkipUnless(shelf2_enabled)
+def shelf_status(sbox):
+  "shelf status"
+
+  sbox.build()
+  was_cwd = os.getcwd()
+  os.chdir(sbox.wc_dir)
+  sbox.wc_dir = ''
+
+  sbox.simple_add_text('New file', 'f')
+  sbox.simple_append('iota', 'New text')
+  sbox.simple_propset('p', 'v', 'A/mu')
+  sbox.simple_rm('A/B/lambda')
+  # Not yet supported:
+  #sbox.simple_rm('A/B/E')
+  expected_status = state_from_status(sbox.wc_dir, v=False, u=False, q=False)
+  run_and_verify_status(sbox.wc_dir, expected_status)
+
+  svntest.actions.run_and_verify_svn(None, [],
+                                     'x-shelve', 'foo')
+  run_and_verify_shelf_status(sbox.wc_dir, expected_status, shelf='foo')
+
+  os.chdir(was_cwd)
+
+#----------------------------------------------------------------------
+
+@SkipUnless(shelf2_enabled)
+def shelve_mkdir(sbox):
+  "shelve mkdir"
+
+  sbox.build()
+
+  def modifier(sbox):
+    sbox.simple_mkdir('D', 'D/D2')
+    sbox.simple_propset('p', 'v', 'D', 'D/D2')
+
+  shelve_unshelve(sbox, modifier, cannot_shelve=True)
+
+#----------------------------------------------------------------------
+
+@SkipUnless(shelf2_enabled)
+def shelve_rmdir(sbox):
+  "shelve rmdir"
+
+  sbox.build()
+  sbox.simple_propset('p', 'v', 'A/C')
+  sbox.simple_commit()
+
+  def modifier(sbox):
+    sbox.simple_rm('A/C', 'A/D/G')
+
+  shelve_unshelve(sbox, modifier, cannot_shelve=True)
+
+#----------------------------------------------------------------------
+
+@SkipUnless(shelf2_enabled)
+def shelve_replace_dir(sbox):
+  "shelve replace dir"
+
+  sbox.build()
+  sbox.simple_propset('p', 'v', 'A/C')
+  sbox.simple_commit()
+
+  def modifier(sbox):
+    sbox.simple_rm('A/C', 'A/D/G')
+    sbox.simple_mkdir('A/C', 'A/C/D2')
+
+  shelve_unshelve(sbox, modifier, cannot_shelve=True)
+
+#----------------------------------------------------------------------
+
+@SkipUnless(shelf2_enabled)
+def shelve_file_copy(sbox):
+  "shelve file copy"
+
+  sbox.build()
+
+  def modifier(sbox):
+    sbox.simple_copy('iota', 'A/ii')
+    sbox.simple_propset('p', 'v', 'A/ii')
+
+  shelve_unshelve(sbox, modifier, cannot_shelve=True)
+
+#----------------------------------------------------------------------
+
+@SkipUnless(shelf2_enabled)
+def shelve_dir_copy(sbox):
+  "shelve dir copy"
+
+  sbox.build()
+
+  def modifier(sbox):
+    sbox.simple_copy('A/B', 'BB')
+    sbox.simple_propset('p', 'v', 'BB')
+
+  shelve_unshelve(sbox, modifier, cannot_shelve=True)
+
+#----------------------------------------------------------------------
+
+@SkipUnless(shelf2_enabled)
+def list_shelves(sbox):
+  "list_shelves"
+
+  sbox.build()
+  was_cwd = os.getcwd()
+  os.chdir(sbox.wc_dir)
+  sbox.wc_dir = ''
+
+  # an empty list
+  svntest.actions.run_and_verify_svn([], [],
+                                     'x-shelf-list', '-q')
+
+  # make two shelves
+  sbox.simple_append('A/mu', 'appended mu text')
+  svntest.actions.run_and_verify_svn(None, [],
+                                     'x-shelf-save', 'foo')
+  sbox.simple_append('A/mu', 'appended more text')
+  svntest.actions.run_and_verify_svn(None, [],
+                                     'x-shelf-save', 'foo', '-m', 'log msg')
+  svntest.actions.run_and_verify_svn(None, [],
+                                     'x-shelf-save', 'bar', '-m', 'log msg')
+
+  # We don't check for time-ordering of the shelves. If we want to do so, we
+  # would need to sleep for timestamps to differ, between creating them.
+
+  # a quiet list
+  expected_out = svntest.verify.UnorderedRegexListOutput(['foo', 'bar'])
+  svntest.actions.run_and_verify_svn(expected_out, [],
+                                     'x-shelf-list', '-q')
+
+  # a detailed list
+  expected_out = svntest.verify.UnorderedRegexListOutput(['foo .* 1 path.*',
+                                                          ' log msg',
+                                                          'bar .* 1 path.*',
+                                                          ' log msg'])
+  svntest.actions.run_and_verify_svn(expected_out, [],
+                                     'x-shelf-list')
+
+  os.chdir(was_cwd)
+
+#----------------------------------------------------------------------
+
+@SkipUnless(shelf2_enabled)
+def refuse_to_shelve_conflict(sbox):
+  "refuse to shelve conflict"
+
+  sbox.build(empty=True)
+  was_cwd = os.getcwd()
+  os.chdir(sbox.wc_dir)
+  sbox.wc_dir = ''
+
+  # create a tree conflict victim at an unversioned path
+  sbox.simple_mkdir('topdir')
+  sbox.simple_commit()
+  sbox.simple_mkdir('topdir/subdir')
+  sbox.simple_commit()
+  sbox.simple_update()
+  sbox.simple_rm('topdir')
+  sbox.simple_commit()
+  sbox.simple_update()
+  svntest.actions.run_and_verify_svn(
+    None, [],
+    'merge', '-c2', '.', '--ignore-ancestry', '--accept', 'postpone')
+  svntest.actions.run_and_verify_svn(
+    None, 'svn: E155015:.*existing.*conflict.*',
+    'merge', '-c1', '.', '--ignore-ancestry', '--accept', 'postpone')
+
+  # attempt to shelve
+  expected_out = svntest.verify.RegexListOutput([
+    r'--- .*',
+    r'--- .*',
+    r'\?     C topdir',
+    r'      > .*',
+    r'      >   not shelved'])
+  svntest.actions.run_and_verify_svn(expected_out,
+                                     '.* 1 path could not be shelved',
+                                     'x-shelf-save', 'foo')
+
+  os.chdir(was_cwd)
+
+#----------------------------------------------------------------------
+
+def unshelve_with_merge(sbox, setup, modifier1, modifier2, tweak_expected_state):
+  """Run a test scenario in which 'unshelve' needs to merge some shelved
+     changes made by modifier1() with some committed changes made by
+     modifier2(). tweak_expected_state() must produce the expected WC state.
+  """
+  sbox.build()
+  was_cwd = os.getcwd()
+  os.chdir(sbox.wc_dir)
+  sbox.wc_dir = ''
+  wc_dir = sbox.wc_dir
+
+  setup(sbox)
+  sbox.simple_commit()
+  initial_state = get_wc_state(wc_dir)
+
+  # Make some changes to the working copy
+  modifier1(sbox)
+  modified_state = get_wc_state(wc_dir)
+
+  # Shelve; check there are no longer any modifications
+  svntest.actions.run_and_verify_svn(None, [],
+                                     'x-shelve', 'foo')
+  check_wc_state(wc_dir, initial_state)
+
+  # Make a different change, with which we shall merge
+  modifier2(sbox)
+  sbox.simple_commit()
+  modified_state[0].tweak('A/mu', wc_rev='3')
+
+  # Unshelve; check the expected result of the merge
+  svntest.actions.run_and_verify_svn(None, [],
+                                     'x-unshelve', 'foo')
+  tweak_expected_state(modified_state)
+  check_wc_state(wc_dir, modified_state)
+
+  os.chdir(was_cwd)
+
+@SkipUnless(shelf2_enabled)
+def unshelve_text_mod_merge(sbox):
+  "unshelve text mod merge"
+
+  orig_contents='A\nB\nC\nD\nE\n'
+  mod1_contents='A\nBB\nC\nD\nE\n'
+  mod2_contents='A\nB\nC\nDD\nE\n'
+  merged_contents='A\nBB\nC\nDD\nE\n'
+
+  def setup(sbox):
+    sbox.simple_append('A/mu', orig_contents, truncate=True)
+
+  def modifier1(sbox):
+    sbox.simple_append('A/mu', mod1_contents, truncate=True)
+
+  def modifier2(sbox):
+    sbox.simple_append('A/mu', mod2_contents, truncate=True)
+
+  def tweak_expected_state(modified_state):
+    modified_state[1].tweak('A/mu', contents=merged_contents)
+
+  unshelve_with_merge(sbox, setup, modifier1, modifier2, tweak_expected_state)
+
+#----------------------------------------------------------------------
+
+@SkipUnless(shelf2_enabled)
+def unshelve_text_mod_conflict(sbox):
+  "unshelve text mod conflict"
+
+  orig_contents='A\nB\nC\nD\nE\n'
+  mod1_contents='A\nBB\nC\nD\nE\n'
+  mod2_contents='A\nBCD\nC\nD\nE\n'
+  merged_contents = 'A\n<<<<<<< .working\nBCD\n||||||| \
.merge-left\nB\n=======\nBB\n>>>>>>> .merge-right\nC\nD\nE\n' +
+  def setup(sbox):
+    sbox.simple_append('A/mu', orig_contents, truncate=True)
+
+  def modifier1(sbox):
+    sbox.simple_append('A/mu', mod1_contents, truncate=True)
+
+  def modifier2(sbox):
+    sbox.simple_append('A/mu', mod2_contents, truncate=True)
+
+  def tweak_expected_state(modified_state):
+    modified_state[0].tweak('A/mu', status='C ')
+    modified_state[1].tweak('A/mu', contents=merged_contents)
+    modified_state[1].add({
+      'A/mu.merge-left':  Item(contents=orig_contents),
+      'A/mu.merge-right': Item(contents=mod1_contents),
+      'A/mu.working':     Item(contents=mod2_contents),
+      })
+
+  unshelve_with_merge(sbox, setup, modifier1, modifier2, tweak_expected_state)
+
+#----------------------------------------------------------------------
+
+@SkipUnless(shelf2_enabled)
+def unshelve_undeclared_binary_mod_conflict(sbox):
+  "unshelve undeclared binary mod conflict"
+
+  orig_contents='\1\2\3\4\5'
+  mod1_contents='\1\2\2\3\4\5'
+  mod2_contents='\1\2\3\4\3\4\5'
+  merged_contents = '<<<<<<< .working\n' + mod2_contents + '||||||| .merge-left\n' + \
orig_contents + '=======\n' + mod1_contents + '>>>>>>> .merge-right\n' +
+  def setup(sbox):
+    sbox.simple_append('A/mu', orig_contents, truncate=True)
+
+  def modifier1(sbox):
+    sbox.simple_append('A/mu', mod1_contents, truncate=True)
+
+  def modifier2(sbox):
+    sbox.simple_append('A/mu', mod2_contents, truncate=True)
+
+  def tweak_expected_state(modified_state):
+    modified_state[0].tweak('A/mu', status='C ')
+    modified_state[1].tweak('A/mu', contents=merged_contents)
+    modified_state[1].add({
+      'A/mu.merge-left':  Item(contents=orig_contents),
+      'A/mu.merge-right': Item(contents=mod1_contents),
+      'A/mu.working':     Item(contents=mod2_contents),
+      })
+
+  unshelve_with_merge(sbox, setup, modifier1, modifier2, tweak_expected_state)
+
+#----------------------------------------------------------------------
+
+@SkipUnless(shelf2_enabled)
+def unshelve_binary_mod_conflict(sbox):
+  "unshelve binary mod conflict"
+
+  orig_contents='\1\2\3\4\5'
+  mod1_contents='\1\2\2\3\4\5'
+  mod2_contents='\1\2\3\4\3\4\5'
+
+  def setup(sbox):
+    sbox.simple_append('A/mu', orig_contents, truncate=True)
+    sbox.simple_propset('svn:mime-type', 'application/octet-stream', 'A/mu')
+
+  def modifier1(sbox):
+    sbox.simple_append('A/mu', mod1_contents, truncate=True)
+
+  def modifier2(sbox):
+    sbox.simple_append('A/mu', mod2_contents, truncate=True)
+
+  def tweak_expected_state(modified_state):
+    modified_state[0].tweak('A/mu', status='C ')
+    modified_state[1].tweak('A/mu', contents=mod2_contents)
+    modified_state[1].add({
+      'A/mu.merge-left':  Item(contents=orig_contents),
+      'A/mu.merge-right': Item(contents=mod1_contents),
+      })
+
+  unshelve_with_merge(sbox, setup, modifier1, modifier2, tweak_expected_state)
+
+#----------------------------------------------------------------------
+
+@SkipUnless(shelf2_enabled)
+def unshelve_text_prop_merge(sbox):
+  "unshelve text prop merge"
+
+  def setup(sbox):
+    sbox.simple_propset('p1', 'v', 'A/mu')
+    sbox.simple_propset('p2', 'v', 'A/mu')
+
+  def modifier1(sbox):
+    sbox.simple_propset('p1', 'changed', 'A/mu')
+
+  def modifier2(sbox):
+    sbox.simple_propset('p2', 'changed', 'A/mu')
+
+  def tweak_expected_state(wc_state):
+    wc_state[1].tweak('A/mu', props={'p1':'changed',
+                                     'p2':'changed'})
+
+  unshelve_with_merge(sbox, setup, modifier1, modifier2, tweak_expected_state)
+
+#----------------------------------------------------------------------
+
+@SkipUnless(shelf2_enabled)
+def unshelve_text_prop_conflict(sbox):
+  "unshelve text prop conflict"
+
+  orig_contents='A'
+  mod1_contents='B'
+  mod2_contents='C'
+  merged_contents='C'
+  prej_contents='''Trying to change property 'p'
+but the local property value conflicts with the incoming change.
+<<<<<<< (local property value)
+C||||||| (incoming 'changed from' value)
+A=======
+B>>>>>>> (incoming 'changed to' value)
+'''
+
+  def setup(sbox):
+    sbox.simple_propset('p', orig_contents, 'A/mu')
+
+  def modifier1(sbox):
+    sbox.simple_propset('p', mod1_contents, 'A/mu')
+
+  def modifier2(sbox):
+    sbox.simple_propset('p', mod2_contents, 'A/mu')
+
+  def tweak_expected_state(wc_state):
+    wc_state[0].tweak('A/mu', status=' C')
+    wc_state[1].tweak('A/mu', props={'p':merged_contents})
+    wc_state[1].add({
+      'A/mu.prej':     Item(contents=prej_contents),
+      })
+
+  unshelve_with_merge(sbox, setup, modifier1, modifier2, tweak_expected_state)
+
+#----------------------------------------------------------------------
+
+def run_and_verify_shelf_diff_summarize(output_tree, shelf, *args):
+  """Run 'svn shelf-diff --summarize' with the arguments *ARGS.
+
+  The subcommand output will be verified against OUTPUT_TREE.  Returns
+  on success, raises on failure.
+  """
+
+  if isinstance(output_tree, wc.State):
+    output_tree = output_tree.old_tree()
+
+  exit_code, output, errput = svntest.actions.run_and_verify_svn(
+                                None, [],
+                                'x-shelf-diff', '--summarize', shelf, *args)
+
+  actual = svntest.tree.build_tree_from_diff_summarize(output)
+
+  # Verify actual output against expected output.
+  try:
+    svntest.tree.compare_trees("output", actual, output_tree)
+  except svntest.tree.SVNTreeError:
+    svntest.verify.display_trees(None, 'DIFF OUTPUT TREE', output_tree, actual)
+    raise
+
+# Exercise a very basic case of shelf-diff.
+@SkipUnless(shelf2_enabled)
+def shelf_diff_simple(sbox):
+  "shelf diff simple"
+
+  sbox.build()
+  was_cwd = os.getcwd()
+  os.chdir(sbox.wc_dir)
+  sbox.wc_dir = ''
+  wc_dir = sbox.wc_dir
+
+  def setup(sbox):
+    sbox.simple_propset('p1', 'v', 'A/mu')
+    sbox.simple_propset('p2', 'v', 'A/mu')
+
+  def modifier1(sbox):
+    sbox.simple_append('A/mu', 'New line.\n')
+    sbox.simple_propset('p1', 'changed', 'A/mu')
+
+  setup(sbox)
+  sbox.simple_commit()
+  initial_state = get_wc_state(wc_dir)
+
+  # Make some changes to the working copy
+  modifier1(sbox)
+  modified_state = get_wc_state(wc_dir)
+
+  svntest.actions.run_and_verify_svn(None, [],
+                                     'x-shelf-save', 'foo')
+
+  # basic svn-style diff
+  expected_output = make_diff_header('A/mu', 'revision 2', 'working copy') + [
+                      "@@ -1 +1,2 @@\n",
+                      " This is the file 'mu'.\n",
+                      "+New line.\n",
+                    ] + make_diff_prop_header('A/mu') \
+                    + make_diff_prop_modified('p1', 'v', 'changed')
+  svntest.actions.run_and_verify_svn(expected_output, [],
+                                     'x-shelf-diff', 'foo')
+
+  # basic summary diff
+  expected_diff = svntest.wc.State(wc_dir, {
+    'A/mu':           Item(status='MM'),
+  })
+  run_and_verify_shelf_diff_summarize(expected_diff, 'foo')
+
+
+########################################################################
+# Run the tests
+
+# list all tests here, starting with None:
+test_list = [ None,
+              shelve_text_mods,
+              shelve_prop_changes,
+              shelve_adds,
+              shelve_deletes,
+              shelve_replace,
+              shelve_empty_adds,
+              shelve_empty_deletes,
+              shelve_from_inner_path,
+              checkpoint_basic,
+              shelve_mergeinfo,
+              unshelve_refuses_if_conflicts,
+              shelve_binary_file_mod,
+              shelve_binary_file_add,
+              shelve_binary_file_del,
+              shelve_binary_file_replace,
+              shelve_with_log_message,
+              shelf_status,
+              shelve_mkdir,
+              shelve_rmdir,
+              shelve_replace_dir,
+              shelve_file_copy,
+              shelve_dir_copy,
+              list_shelves,
+              refuse_to_shelve_conflict,
+              unshelve_text_mod_merge,
+              unshelve_text_mod_conflict,
+              unshelve_undeclared_binary_mod_conflict,
+              unshelve_binary_mod_conflict,
+              unshelve_text_prop_merge,
+              unshelve_text_prop_conflict,
+              shelf_diff_simple,
+             ]
+
+if __name__ == '__main__':
+  svntest.main.run_tests(test_list)
+  # NOTREACHED
+
+
+### End of file.

Propchange: subversion/branches/decouple-shelving-cli/subversion/tests/cmdline/shelf2_tests.py
                
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: subversion/branches/decouple-shelving-cli/subversion/tests/cmdline/shelf2_tests.py
                
------------------------------------------------------------------------------
    svn:executable = *

Propchange: subversion/branches/decouple-shelving-cli/subversion/tests/cmdline/shelf2_tests.py
                
------------------------------------------------------------------------------
    svn:mime-type = text/x-python


[prev in list] [next in list] [prev in thread] [next in thread] 

Configure | About | News | Add a list | Sponsored by KoreLogic