Subject: bug#61050: mv: add support for --one-file-system

From: Paul Eggert <[email-masked]>
Date: Wed, 25 Jan 2023 01:07:48 -0800

On 2023-01-24 17:20, Mike Frysinger wrote:
> i'd like to require that the mv be
> atomic when relocating a directory, and if it isn't, fallback to other
> logic

Calling the new option "--one-file-system" sounds a bit wrong, as 
--one-file-system is for programs like 'du' and 'cp' and causes them to 
not cross a file system boundary when recursing - something that 'mv' 
could also do (when it falls back on copying), but which is a different 
thing than what you're asking for.

How about if we call the new option --no-copy instead? Something like 
the attached incomplete patch, say. (It needs more documentation and 
ideally a test case.)

diff --git a/src/copy.c b/src/copy.c
index 98f2ba45a..99834434f 100644
--- a/src/copy.c
+++ b/src/copy.c
@@ -2617,7 +2617,7 @@ copy_internal (char const *src_name, char const *dst_name,
          where you'd replace '18' with the integer in parentheses that
          was output from the perl one-liner above.
          If necessary, of course, change '/tmp' to some other directory.  */
-      if (rename_errno != EXDEV)
+      if (rename_errno != EXDEV || x->no_copy)
         {
           /* There are many ways this can happen due to a race condition.
              When something happens between the initial follow_fstatat and the
diff --git a/src/copy.h b/src/copy.h
index d775db047..15dcfa8af 100644
--- a/src/copy.h
+++ b/src/copy.h
@@ -134,9 +134,9 @@ struct cp_options
      Create destination directories as usual. */
   bool hard_link;
 
-  /* If true, rather than copying, first attempt to use rename.
-     If that fails, then resort to copying.  */
-  bool move_mode;
+  /* If MOVE_MODE, first attempt to use rename.
+     If that fails and !NO_COPY, resort to copying.  */
+  bool move_mode, no_copy;
 
   /* If true, install(1) is the caller.  */
   bool install_mode;
diff --git a/src/mv.c b/src/mv.c
index 72bbe8e46..f27a07a1c 100644
--- a/src/mv.c
+++ b/src/mv.c
@@ -48,7 +48,8 @@
    non-character as a pseudo short option, starting with CHAR_MAX + 1.  */
 enum
 {
-  STRIP_TRAILING_SLASHES_OPTION = CHAR_MAX + 1
+  NO_COPY_OPTION = CHAR_MAX + 1,
+  STRIP_TRAILING_SLASHES_OPTION
 };
 
 static struct option const long_options[] =
@@ -58,6 +59,7 @@ static struct option const long_options[] =
   {"force", no_argument, NULL, 'f'},
   {"interactive", no_argument, NULL, 'i'},
   {"no-clobber", no_argument, NULL, 'n'},
+  {"no-copy", no_argument, NULL, NO_COPY_OPTION},
   {"no-target-directory", no_argument, NULL, 'T'},
   {"strip-trailing-slashes", no_argument, NULL, STRIP_TRAILING_SLASHES_OPTION},
   {"suffix", required_argument, NULL, 'S'},
@@ -260,6 +262,7 @@ Rename SOURCE to DEST, or move SOURCE(s) to DIRECTORY.\n\
 If you specify more than one of -i, -f, -n, only the final one takes effect.\n\
 "), stdout);
       fputs (_("\
+      --no-copy                do not copy if renaming fails\n\
       --strip-trailing-slashes  remove any trailing slashes from each SOURCE\n\
                                  argument\n\
   -S, --suffix=SUFFIX          override the usual backup suffix\n\
@@ -330,6 +333,9 @@ main (int argc, char **argv)
         case 'n':
           x.interactive = I_ALWAYS_NO;
           break;
+        case NO_COPY_OPTION:
+          x.no_copy = true;
+          break;
         case STRIP_TRAILING_SLASHES_OPTION:
           remove_trailing_slashes = true;
           break;