I believe we should make 'svn' reject any peg revision specifier on an
argument where it doesn't use one.  This is a rather long email to
record some of the details behind my reasoning, and a question mainly
for Stefan Sperling about issue #3651 [3].

First some context.

$ svn cat ^/trunk/readme@123
  # Makes sense.

$ svn mkdir ^/trunk/doc@123
  # Nonsense: mkdir with a URL can only operate on the head. [2]

$ svn add foo@123
  # Nonsense: the target must be an unversioned local path.

We decided [1] that we should allow the user to consistently escape any
target argument by appending an '@' sign, without having to know whether
a peg revision is relevant.  This was implemented in 1.6.5 and trunk by
parsing off and discarding any peg revision specifier, using the
function 'svn_cl__eat_peg_revisions()'.

That current solution is great for allowing consistent escaping, but as
a side effect 'svn' silently ignores any peg revision it isn't going to
use, which I think is bad, as I have noted in the doc string of
svn_cl__eat_peg_revisions().

So, when does a peg not make sense?

  (a) When the target is supposed to be an unversioned local path, such
as in 'svn add'.  A peg never makes sense on an unversioned path.  A
versioned local path is allowed with '--force', but it would still be
wrong to specify any peg revision on it because only the path on disk is
relevant to the 'add' operation.

  (b) When the target necessarily refers to a path at the head of the
repository, such as in 'svn lock TARGET' or 'svn delete TARGET-URL'.
Logically, a peg of 'HEAD' is always valid in these commands.
Logically, these commands could accept a non-head peg where TARGET is
known to exist.  Then, if someone else moves TARGET to another path
while we are composing and executing this command, Subversion could do
its best to follow the move or error out if it can't do so.  But unless
and until 'svn' actually uses the peg revision in this way, it should
complain, not silently ignore it.

  (c) When the target refers to a new path that will be created in a new
revision on top of the old head, such as in 'svn mkdir TARGET-URL' or
the destination of 'svn copy SOURCE DEST-URL'.  In most of these cases,
the target does not exist in the repository at the time the command is
issued so there is no possible peg revision specifier.  The target of a
copy may be specified as an existing directory, and in that case it
could accept a peg of 'HEAD' or even a non-head peg as described in (b).

My question is about this case:

  $ svn copy SOURCE TARGET@HEAD

This case falls under the last sentence of point (C) above.  Issue #3651
[3] claims that the 'HEAD' peg should be ignored in this case, and that
claim is explicitly tested by copy_tests 62 [4].  (It is the only test
that fails with the attached patch.)

But is 'HEAD' supposed to be a special case on the basis that it is
redundant but still correct?  If so, then we should also allow '123' iff
that is currently the head, and 'BASE' for a local path iff that is
currently the head, and so on.  In other words I don't think we have
reason to claim that 'HEAD' is a special case.

The logic gets very messy if we think we must support redundant pegs.
Even if we only implement a subset of the messy logic (such as just
allowing 'HEAD') the conceptual logic is messy which is a bad thing for
documentation and learning.  So my conclusion is that we should reject
any peg revision specifier, even 'HEAD', in places where we're not
prepared to use it.

If that's agreeable, I'll make it happen.  (The attached patch is not
quite finished; for example it has no doc string for one of the new
functions.)

- Julian


[1] Issue #3416 "Cannot add or commit 'dir/@file.txt'",
<http://subversion.tigris.org/issues/show_bug.cgi?id=3416>.
[2] The only ways I can imagine the second example could make sense are
if it were to mean, "Make this directory, assuming that HEAD is 123, and
fail if not" or, "Make this directory, assuming that HEAD is 122 so that
the committed revision will be 123, and fail if not."  But we don't have
any plans to implement such semantics, and the syntax does not make
sense for the semantics we have implemented.
[3] Issue #3651 "svn copy does not eat peg revision within copy target
path", <http://subversion.tigris.org/issues/show_bug.cgi?id=3651>.
[4] <http://svn.apache.org/viewvc?revision=952992&view=revision>

Disallow a non-empty peg revision specifier on any 'svn' command-line
argument where a peg revision is not wanted.  The previous behaviour was to
silently ignore any such peg revision specifier.  That was done so that an
empty specifier (for example, 'file@') could be used consistently on any
argument in order to escape any earlier '@' sign in the argument.  This is
related to issue #3416 "Cannot add or commit 'dir/@file.txt'" and issue
#3651 "svn copy does not eat peg revision within copy target path".

* subversion/svn/cl.h,
  subversion/svn/util.c
  (svn_cl__eat_peg_revision): New function.
  (svn_cl__eat_peg_revisions): Raise an error if a non-empty peg revision is
    found. Document the new behaviour and remove the "TODO" note about this.

* subversion/svn/copy-cmd.c
  (svn_cl__copy): Instead of calling svn_cl__eat_peg_revisions() on the
    whole 'targets' array, perform the equivalent check on just the relevant
    target, but also allow '@HEAD' to be specified if the target is a URL
    because issue #3651 is about that specific case and there is a test for
    it. Also eliminate two unnecessary variables.

* subversion/svn/export-cmd.c
  (svn_cl__export): Use svn_cl__eat_peg_revision() instead of in-line code
    that discarded a peg revision from the destination path.
--This line, and those below, will be ignored--

Index: subversion/svn/cl.h
===================================================================
--- subversion/svn/cl.h	(revision 1140544)
+++ subversion/svn/cl.h	(working copy)
@@ -767,35 +767,18 @@ svn_cl__node_description(const svn_wc_co
                          const char *wc_repos_root_URL,
                          apr_pool_t *pool);
 
-/* Return, in @a *true_targets_p, a copy of @a targets with peg revision
- * specifiers snipped off the end of each element.
- *
- * ### JAF TODO: This function is not good because it does not allow the
- * ### caller to detect if an invalid peg revision was specified.
- * ###
- * ### Callers should never have a need to silently *discard* all peg
- * ### revisions, even if they are doing this *after* saving any peg
- * ### revisions that might be of interest on certain arguments: I don't
- * ### think it can ever be correct to silently ignore a peg revision that
- * ### is specified, whether it makes semantic sense or not.
- * ###
- * ### Instead, callers should parse all the arguments and silently
- * ### ignore an *empty* peg revision part (just an "@", which can be
- * ### used to escape an earlier "@" in the argument) on any argument,
- * ### even an argument on which a peg revision does not make sense,
- * ### but should not silently ignore a non-empty peg when it does not
- * ### make sense.
- * ###
- * ### Something like:
- * ###   For each (URL-like?) argument that doesn't accept a peg rev:
- * ###     Parse into peg-rev and true-path parts;
- * ###     If (peg rev != unspecified)
- * ###       Error("This arg doesn't accept a peg rev.").
- * ###     Use the true-path part.
+svn_error_t *
+svn_cl__eat_peg_revision(const char **true_target,
+                         const char *target,
+                         apr_pool_t *pool);
+
+/* Return, in @a *true_targets_p, a copy of @a targets with empty peg
+ * revision specifiers snipped off the end of each element. If any element
+ * has a non-empty peg revision specifier, throw an error.
  *
  * This function is useful for subcommands for which peg revisions
- * do not make any sense. Such subcommands still need to allow peg
- * revisions to be specified on the command line so that users of
+ * do not make any sense. Such subcommands still need to parse off any
+ * empty peg revision specifier on the command line so that users of
  * the command line client can consistently escape '@' characters
  * in filenames by appending an '@' character, regardless of the
  * subcommand being used.
Index: subversion/svn/copy-cmd.c
===================================================================
--- subversion/svn/copy-cmd.c	(revision 1140544)
+++ subversion/svn/copy-cmd.c	(working copy)
@@ -46,7 +46,7 @@ svn_cl__copy(apr_getopt_t *os,
   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, *sources;
-  const char *src_path, *dst_path;
+  const char *dst_path;
   svn_boolean_t srcs_are_urls, dst_is_url;
   svn_error_t *err;
   int i;
@@ -64,28 +64,35 @@ svn_cl__copy(apr_getopt_t *os,
     {
       const char *target = APR_ARRAY_IDX(targets, i, const char *);
       svn_client_copy_source_t *source = apr_palloc(pool, sizeof(*source));
-      const char *src;
       svn_opt_revision_t *peg_revision = apr_palloc(pool,
                                                     sizeof(*peg_revision));
 
-      SVN_ERR(svn_opt_parse_path(peg_revision, &src, target, pool));
-      source->path = src;
+      SVN_ERR(svn_opt_parse_path(peg_revision, &source->path, target, pool));
       source->revision = &(opt_state->start_revision);
       source->peg_revision = peg_revision;
 
       APR_ARRAY_PUSH(sources, svn_client_copy_source_t *) = source;
     }
 
-  SVN_ERR(svn_cl__eat_peg_revisions(&targets, targets, pool));
-
   /* Figure out which type of notification to use.
      (There is no need to check that the src paths are homogeneous;
      svn_client_copy6() through its subroutine try_copy() will return an
      error if they are not.) */
-  src_path = APR_ARRAY_IDX(targets, 0, const char *);
-  srcs_are_urls = svn_path_is_url(src_path);
-  dst_path = APR_ARRAY_IDX(targets, targets->nelts - 1, const char *);
-  apr_array_pop(targets);
+  srcs_are_urls = svn_path_is_url(APR_ARRAY_IDX(targets, 0, const char *));
+
+  /* Find DST_PATH and check it has no peg (or is in the form 'URL@HEAD'). */
+  {
+    const char *target = APR_ARRAY_IDX(targets, targets->nelts - 1,
+                                       const char *);
+    svn_opt_revision_t peg;
+
+    SVN_ERR(svn_opt_parse_path(&peg, &dst_path, target, pool));
+    if (! (peg.kind == svn_opt_revision_unspecified
+           || (peg.kind == svn_opt_revision_head && svn_path_is_url(target))))
+      return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
+                               _("Unexpected peg revision on '%s'"),
+                               target);
+  }
   dst_is_url = svn_path_is_url(dst_path);
 
   if ((! srcs_are_urls) && (! dst_is_url))
Index: subversion/svn/export-cmd.c
===================================================================
--- subversion/svn/export-cmd.c	(revision 1140544)
+++ subversion/svn/export-cmd.c	(working copy)
@@ -81,11 +81,9 @@ svn_cl__export(apr_getopt_t *os,
     }
   else
     {
-      to = APR_ARRAY_IDX(targets, 1, const char *);
+      const char *target = APR_ARRAY_IDX(targets, 1, const char *);
 
-      if (strcmp("", to) != 0)
-        /* svn_cl__eat_peg_revisions() but only on one target */
-        SVN_ERR(svn_opt__split_arg_at_peg_revision(&to, NULL, to, pool));
+      SVN_ERR(svn_cl__eat_peg_revision(&to, target, pool));
     }
 
   SVN_ERR(svn_cl__check_target_is_local_path(to));
Index: subversion/svn/util.c
===================================================================
--- subversion/svn/util.c	(revision 1140544)
+++ subversion/svn/util.c	(working copy)
@@ -1335,6 +1335,23 @@ svn_cl__node_description(const svn_wc_co
 }
 
 svn_error_t *
+svn_cl__eat_peg_revision(const char **true_target,
+                         const char *target,
+                         apr_pool_t *pool)
+{
+  const char *peg;
+
+  SVN_ERR(svn_opt__split_arg_at_peg_revision(true_target, &peg,
+                                             target, pool));
+  if (peg[0] == '@' && peg[1] != '\0')
+    return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
+                             _("Unexpected peg revision on '%s'"),
+                             target);
+
+  return SVN_NO_ERROR;
+}
+
+svn_error_t *
 svn_cl__eat_peg_revisions(apr_array_header_t **true_targets_p,
                           const apr_array_header_t *targets,
                           apr_pool_t *pool)
@@ -1349,8 +1366,7 @@ svn_cl__eat_peg_revisions(apr_array_head
       const char *target = APR_ARRAY_IDX(targets, i, const char *);
       const char *true_target;
 
-      SVN_ERR(svn_opt__split_arg_at_peg_revision(&true_target, NULL,
-                                                 target, pool));
+      SVN_ERR(svn_cl__eat_peg_revision(&true_target, target, pool));
       APR_ARRAY_PUSH(true_targets, const char *) = true_target;
     }
 

Reply via email to