On Sat, Apr 16, 2011 at 10:17 AM, Johan Corveleyn <jcor...@gmail.com> wrote:
> Hi,
>
> Following discussion in [1], I tried to write a patch for issue #3702
> ("svn ren TODO todo" not work on windows). As mentioned by Bert in
> comment to the issue [2], we need to avoid letting 'svn move' convert
> the destination path to on-disk casing in this case, so that's what
> the below patch does.
>
> However, it seems that's not enough. I'm getting further, but now the
> move is blocked libsvn_client/copy.c. The error I'm getting now is:
>
>    svn: E155007: Path 'C:\Temp\test\todo' is not a directory
>
> The problem is that, in copy.c#svn_client_move6, it first tries the
> move, but if the dst_path already exists (which comes down to an
> apr_stat inside copy.c#verify_wc_srcs_and_dsts), it tries the move
> again with moving the src_path to a child of (the presumed directory)
> dst_path:
>
> libsvn_client/copy.c: 2313
> [[[
>  err = try_copy(sources, dst_path,
>                 TRUE /* is_move */,
>                 make_parents,
>                 FALSE,
>                 revprop_table,
>                 commit_callback, commit_baton,
>                 ctx,
>                 subpool);
>
>  /* If the destination exists, try to move the sources as children of the
>     destination. */
>  if (move_as_child && err && (src_paths->nelts == 1)
>        && (err->apr_err == SVN_ERR_ENTRY_EXISTS
>            || err->apr_err == SVN_ERR_FS_ALREADY_EXISTS))
>    {
>      ...
>      err = try_copy(sources,
>                     dst_is_url
>                         ? svn_path_url_add_component2(dst_path,
>                                                       src_basename, pool)
>                         : svn_dirent_join(dst_path, src_basename, pool),
>                      ...
> ]]]
>
> So, a fix in the client layer is needed as well. Any suggestions on
> how to best approach this?
>
> Right now, I'm thinking the fix should be in verify_wc_srcs_and_dsts
> (similar to the "special case" of my below patch for move-cmd.c):
> while checking each copy_pair, if the dst_path already exists, check
> the "true path" of dst_path (APR_FILEPATH_TRUENAME), and see if it's
> the same as the src_path. If so, don't return an already-exists-error.
> But I'm not sure if that's correct, may have side-effects, ...

In attachment is a second attempted patch that implements the above
suggestion: in copy.c, when verifying srcs_and_dsts, if a dst clashes
with an existing file, check if its truepath is the same as the src
(if so, let it go through). This uses some copy-pasted code from
opt.c, so maybe some refactoring is needed here ... Or another way to
pass both the truepath and the originally-requested-path.

After that the "svn move" went through, but the moved file was deleted
from the file system. I fixed that by setting keep_local to TRUE in
the call to svn_wc_delete4 in do_wc_to_wc_moves_with_locks2. At first
sight this seems ok to me, but I'm very much in unfamiliar territory,
so I'm not sure at all :-). See the comment in the patch for some
additional explanation.

Comments, review, ... very much appreciated.

With this patch, one can perform case-only renames on Windows.

BUT it raises some additional questions/issues:

- How to commit such a move? Committing the parent directory
recursively works fine, but if you try to specify only the move
targets (src and dst paths), commit runs into the same problem as what
I was trying to address here: both paths are converted to their
"truepath", which means only the added side is seen by commit (the
only part that's still on the filesystem):

    C:\Temp\test4>svn mv foo2 Foo2
    A         Foo2
    D         foo2

    C:\Temp\test4>svn commit -mtest foo2 Foo2
    Adding         Foo2

    Committed revision 96322.

This is very undesirable, because after this commit this cannot be
checked out or updated anymore on a Windows client (case-clashing
files).

- In fact: the same problem holds true for other commands as well: how
to revert both sides of this move? Ok, one can revert in two steps ...

- Maybe a more general solution is needed, so all commands can
adequately see which path the user actually means? The "truepath"
corresponding to a given path (modulo case), or really the path in the
case given by the user? A shot in the dark: (1) first look in the
wc-db - if the path matches a path in the wc-db, accept it as is, else
(2) convert it to its truepath (path on the filesystem that matches
modulo case). Except for "svn move", as implemented in this patch ...

- Note that the above problem is already present now on trunk (without
my patch): since we can now represent case-clashing paths in the WC
even on a case-insensitive filesystem. (See Bert's example in [2],
"svn ren TODO todoQ; svn ren todoQ todo").

Cheers,
-- 
Johan

> [1] http://svn.haxx.se/dev/archive-2011-04/0232.shtml
> [2] http://subversion.tigris.org/issues/show_bug.cgi?id=3702#desc6
Log message:
[[[
Allow case-only renames on case-insensitive filesystems (issue #3702).

* subversion/svn/move-cmd.c
  (svn_cl__move): If the targets are paths, and there are exactly two, and
   the normalized DST_PATH and SRC_PATH are equal, canonicalize the original
   DST_PATH again, but without converting it to on-disk casing.

* subversion/libsvn_client/copy.c
  (do_wc_to_wc_moves_with_locks2): Pass TRUE for keep_local when calling
   svn_wc_delete4 after just having performed the svn_io_file_rename. Because
   it has just been renamed, the src_abspath_or_url should already be gone.
   This avoids deleting the destination in case of a case-only rename.
  (verify_wc_srcs_and_dsts): When the dst_abspath_or_url already exists,
   see if its "truepath" (matching path on the filesystem, module case) is
   the same as the src_abspath_or_url, to account for a possible case-only
   rename. In that case, don't complain about that the dst already exists.
]]]
Index: subversion/svn/move-cmd.c
===================================================================
--- subversion/svn/move-cmd.c   (revision 1092816)
+++ subversion/svn/move-cmd.c   (working copy)
@@ -30,6 +30,7 @@
 #include "svn_client.h"
 #include "svn_error.h"
 #include "svn_path.h"
+#include "svn_utf.h"
 #include "cl.h"

 #include "svn_private_config.h"
@@ -76,6 +77,27 @@ svn_cl__move(apr_getopt_t *os,
           (SVN_ERR_CL_UNNECESSARY_LOG_MESSAGE, NULL,
            _("Local, non-commit operations do not take a log message "
              "or revision properties"));
+
+      /* For allowing case-only renames on Windows (issue #3702):
+         If DST_PATH is a path, and there is exactly one source path, check if
+         the normalized DST_PATH and SRC_PATH are equal. If so, canonicalize
+         the original DST_PATH again, but without converting it to on-disk
+         casing, so we can pass the intended destination to
+         svn_client_move6. */
+      if (targets->nelts == 1)
+        {
+          const char *src_path = APR_ARRAY_IDX(targets, targets->nelts - 1,
+                                               const char *);
+
+          if (strcmp(src_path, dst_path) == 0)
+            {
+              const char *utf8_target;
+
+              SVN_ERR(svn_utf_cstring_to_utf8(&utf8_target,
+                                              os->argv[os->argc - 1], pool));
+              dst_path = svn_dirent_canonicalize(utf8_target, pool);
+            }
+        }
     }

   if (ctx->log_msg_func3)
Index: subversion/libsvn_client/copy.c
===================================================================
--- subversion/libsvn_client/copy.c     (revision 1092816)
+++ subversion/libsvn_client/copy.c     (working copy)
@@ -347,8 +347,14 @@ do_wc_to_wc_moves_with_locks2(void *baton,
   SVN_ERR(svn_io_file_rename(b->pair->src_abspath_or_url, dst_abspath,
                              scratch_pool));

+  /* ### JC: Deleting with keep_local==TRUE should be fine, because the above
+     rename should have already (re)moved src_abspath_or_url, so it should no
+     longer be present on the filesystem. Without keep_local, a case-only
+     rename on a case-insensitive filesystem doesn't work: the dst of the
+     rename is deleted here (because it's equal to the src, modulo case). */
   SVN_ERR(svn_wc_delete4(b->ctx->wc_ctx, b->pair->src_abspath_or_url,
-                         FALSE, FALSE,
+                         TRUE /* keep_local */,
+                         FALSE /* delete_unversioned_target */,
                          b->ctx->cancel_func, b->ctx->cancel_baton,
                          b->ctx->notify_func2, b->ctx->notify_baton2,
                          scratch_pool));
@@ -482,11 +488,46 @@ verify_wc_srcs_and_dsts(const apr_array_header_t *
       SVN_ERR(svn_io_check_path(pair->dst_abspath_or_url, &dst_kind,
                                 iterpool));
       if (dst_kind != svn_node_none)
-        return svn_error_createf(
-          SVN_ERR_ENTRY_EXISTS, NULL,
-          _("Path '%s' already exists"),
-          svn_dirent_local_style(pair->dst_abspath_or_url, pool));
+        {
+          /* ### JC: Get the dst's truepath, just in case we passed a
+             non-truepath dst for doing a case-only rename, so we can check
+             here if we're doing a case-only rename (which shouldn't be
+             blocked here). This code is mainly copy-pasted from
+             libsvn_subr/opt.c#svn_opt__arg_canonicalize_path - should
+             probably end up somewhere else ... */
+          const char *apr_target;
+          const char *dst_truepath;
+          const char *dst_truepath_utf8;
+          apr_status_t apr_err;
+
+          SVN_ERR(svn_path_cstring_from_utf8(&apr_target,
+                                             pair->dst_abspath_or_url,
+                                             iterpool));
+          apr_err = apr_filepath_merge(&dst_truepath, "", apr_target,
+                                       APR_FILEPATH_TRUENAME, iterpool);
+          /* convert back to UTF-8. */
+          SVN_ERR(svn_path_cstring_to_utf8(&dst_truepath_utf8,
+                                           dst_truepath, iterpool));
+          dst_truepath_utf8 = svn_dirent_canonicalize(dst_truepath_utf8,
+                                                      iterpool);

+          if (dst_kind == pair->src_kind
+              && strcmp(pair->src_abspath_or_url,
+                        dst_truepath_utf8) == 0
+              && strcmp(pair->src_abspath_or_url,
+                        pair->dst_abspath_or_url) != 0)
+            {
+              /* It's a case-only rename, just let it go through */
+            }
+          else
+            {
+              return svn_error_createf(
+                SVN_ERR_ENTRY_EXISTS, NULL,
+                _("Path '%s' already exists"),
+                svn_dirent_local_style(pair->dst_abspath_or_url, pool));
+            }
+        }
+
       svn_dirent_split(&pair->dst_parent_abspath, &pair->base_name,
                        pair->dst_abspath_or_url, pool);

Reply via email to