]> git.eshelyaron.com Git - emacs.git/commitdiff
Add renaming of remote buffer file names to Tramp
authorMichael Albinus <michael.albinus@gmx.de>
Wed, 20 Nov 2019 12:45:30 +0000 (13:45 +0100)
committerMichael Albinus <michael.albinus@gmx.de>
Wed, 20 Nov 2019 12:45:30 +0000 (13:45 +0100)
* doc/misc/tramp.texi (Default User): Fix typo.
(Cleanup remote connections): Adapt arguments of
`tramp-cleanup-connection'.
(Renaming remote files): New node.
(Frequently Asked Questions): New item "How to save files when a
remote host isn't reachable anymore?".

* etc/NEWS: Add `tramp-rename-files' and `tramp-rename-these-files'.

* lisp/net/tramp-cmds.el (tramp-default-rename-alist)
(tramp-confirm-rename-file-names): New defcustoms.
(tramp-rename-read-file-name-dir)
(tramp-rename-read-file-name-init): New defsubsts.
(tramp-default-rename-file, tramp-rename-files)
(tramp-rename-these-files): New defuns.

* lisp/net/tramp-integration.el (ido, ivy): Integrate with them.

doc/misc/tramp.texi
etc/NEWS
lisp/net/tramp-cmds.el
lisp/net/tramp-integration.el

index 67472a87a326fdb099251b7fc31883ae5af8e3d7..dd1378a8da79bc6b759759549543b2f192abc70f 100644 (file)
@@ -153,6 +153,7 @@ Using @value{tramp}
 * Ad-hoc multi-hops::           Declaring multiple hops in the file name.
 * Remote processes::            Integration with other Emacs packages.
 * Cleanup remote connections::  Cleanup remote connections.
+* Renaming remote files::       Renaming remote files.
 * Archive file names::          Access to files in file archives.
 
 How file names, directories and localnames are mangled and managed
@@ -1409,7 +1410,7 @@ use the @samp{john} as the default user for the domain
 A Caution: @value{tramp} will override any default user specified in
 the configuration files outside Emacs, such as @file{~/.ssh/config}.
 To stop @value{tramp} from applying the default value, set the
-corresponding alist entry to nil:
+corresponding alist entry to @code{nil}:
 
 @lisp
 @group
@@ -2710,6 +2711,7 @@ is a feature of Emacs that may cause missed prompts when using
 * Ad-hoc multi-hops::           Declaring multiple hops in the file name.
 * Remote processes::            Integration with other Emacs packages.
 * Cleanup remote connections::  Cleanup remote connections.
+* Renaming remote files::       Renaming remote files.
 * Archive file names::          Access to files in file archives.
 @end menu
 
@@ -3371,9 +3373,9 @@ To open @command{powershell} as a remote shell, use this:
 
 @value{tramp} provides several ways to flush remote connections.
 
-@deffn Command tramp-cleanup-connection vec
-This command flushes all connection related objects.  @option{vec} is
-the internal representation of a remote connection.  When called
+@deffn Command tramp-cleanup-connection vec &optional keep-debug keep-password
+This command flushes all connection related objects.  @var{vec} is the
+internal representation of a remote connection.  When called
 interactively, this command lists active remote connections in the
 minibuffer.  Each connection is of the format
 @file{@trampfn{method,user@@host,}}.
@@ -3383,11 +3385,14 @@ Flushing remote connections also cleans the password cache
 (@pxref{Connection caching}), and recentf cache (@pxref{File
 Conveniences, , , emacs}).  It also deletes session timers
 (@pxref{Predefined connection information}) and connection buffers.
+
+If @var{keep-debug} is non-@code{nil}, the debug buffer is kept.  A
+non-@code{nil} @var{keep-password} preserves the password cache.
 @end deffn
 
 @deffn Command tramp-cleanup-this-connection
-Flushes only the current buffer's remote connection objects, the same
-as in @code{tramp-cleanup-connection}.
+Flushes the current buffer's remote connection objects, the same as in
+@code{tramp-cleanup-connection}.
 @end deffn
 
 @deffn Command tramp-cleanup-all-connections
@@ -3404,6 +3409,112 @@ killing all buffers related to remote connections.
 @end deffn
 
 
+@node Renaming remote files
+@section Renaming remote files
+@cindex save remote files
+
+Sometimes, it is desirable to safe file contents of buffers visiting a
+given remote host.  This could happen for example, if the local host
+changes its network integration, and the remote host is not reachable
+anymore.
+
+@deffn Command tramp-rename-files source target
+Replace in all buffers the visiting file name from @var{source} to
+@var{target}.  @var{source} is a remote directory name, which could
+contain also a localname part.  @var{target} is the directory name
+@var{source} is replaced with.  Often, @var{target} is a remote
+directory name on another host, but it can also be a local directory
+name.  If @var{target} has no local part, the local part from
+@var{source} is used.
+
+If @var{target} is @code{nil}, it is selected according to the first
+match in @code{tramp-default-rename-alist}.  If called interactively,
+this match is offered as initial value for selection.
+
+On all buffers, which have a @code{buffer-file-name} matching
+@var{source}, this name is modified by replacing @var{source} with
+@var{target}.  This is applied by calling
+@code{set-visited-file-name}.  The new @code{buffer-file-name} is
+prompted for modification in the minibuffer.  The buffers are marked
+modified, and must be saved explicitly.
+
+If user option @code{tramp-confirm-rename-file-names} is nil, changing
+the file name happens without confirmation.  This requires a
+matching entry in @code{tramp-default-rename-alist}.
+
+Remote buffers related to the remote connection identified by
+@var{source}, which are not visiting files, or which are visiting
+files not matching @var{source}, are not modified.
+
+Interactively, @var{target} is selected from
+@code{tramp-default-rename-alist} without confirmation if the prefix
+argument is non-@code{nil}.
+
+The remote connection identified by @var{source} is flushed by
+@code{tramp-cleanup-connection}.
+@end deffn
+
+@deffn Command tramp-rename-these-files target
+Replace visiting file names to @var{target}.  The current buffer must
+be related to a remote connection.  In all buffers, which are visiting
+a file with the same directory name, the buffer file name is changed.
+
+Interactively, @var{target} is selected from
+@code{tramp-default-rename-alist} without confirmation if the prefix
+argument is non-@code{nil}.
+@end deffn
+
+@defopt tramp-default-rename-alist
+The default target for renaming remote buffer file names.  This is an
+alist of cons cells @code{(source . target)}.  The first matching item
+specifies the target to be applied for renaming buffer file names from
+source via @code{tramp-rename-files}.  @code{source} is a regular
+expressions, which matches a remote file name.  @code{target} must be
+a directory name, which could be remote (including remote directories
+Tramp infers by default, such as @samp{@trampfn{method,user@@host,}}).
+
+@code{target} can contain the patterns @code{%m}, @code{%u} or
+@code{%h}, which are replaced by the method name, user name or host
+name of @code{source} when calling @code{tramp-rename-files}.
+
+@code{source} could also be a Lisp form, which will be evaluated.  The
+result must be a string or nil, which is interpreted as a regular
+expression which always matches.
+
+Example entries:
+
+@lisp
+@group
+("@trampfn{ssh,badhost,/path/to/dir/}"
+ . "@trampfn{ssh,goodhost,/path/to/another/dir/}")
+@end group
+@end lisp
+
+would trigger renaming of buffer file names on @samp{badhost} to
+@samp{goodhost}, including changing the directory name.
+
+@lisp
+("@trampfn{ssh,.+\\\\.company\\\\.org,}" . "@value{prefix}ssh@value{postfixhop}multi.hop|ssh@value{postfixhop}%h@value{postfix}")
+@end lisp
+
+routes all connections to a host in @samp{company.org} via
+@samp{@trampfn{ssh,multi.hop,}}, which might be useful when using
+Emacs outside the company network.
+
+@lisp
+(nil . "~/saved-files/%m:%u@@%h/")
+@end lisp
+
+saves all remote files locally, with a directory name including method
+name, user name and host name of the remote connection.
+@end defopt
+
+@defopt tramp-confirm-rename-file-names
+Whether renaming a buffer file name by @code{tramp-rename-files} or
+@code{tramp-rename-these-files} must be confirmed.
+@end defopt
+
+
 @node Archive file names
 @section Archive file names
 @cindex file archives
@@ -3412,7 +3523,7 @@ killing all buffers related to remote connections.
 @cindex archive method
 
 @value{tramp} offers also transparent access to files inside file
-archives.  This is possible only on machines which have installed
+archives.  This is possible only on hosts which have installed
 @acronym{GVFS, the GNOME Virtual File System}, @ref{GVFS-based
 methods}.  Internally, file archives are mounted via the
 @acronym{GVFS} @option{archive} method.
@@ -4439,6 +4550,21 @@ the buffer is remote.  See the optional arguments of
 @code{file-remote-p} for determining details of the remote connection.
 
 
+@item
+How to save files when a remote host isn't reachable anymore?
+
+If the local machine Emacs is running on changes its network
+integration, remote hosts could become unreachable.  This happens for
+example, if the local machine is moved between your office and your
+home without restarting Emacs.
+
+In such cases, the command @code{tramp-rename-files} can be used to
+alter remote buffers’ method, host, and/or directory names.  This
+permits saving their contents in the same location via another network
+path, or somewhere else entirely (including locally).  @pxref{Renaming
+remote files}.
+
+
 @item
 How to disable other packages from calling @value{tramp}?
 
index 4887b8e6819b36fd9761ddde97481ae0f0a607cb..dd08675f4253fd2a5b934662d64af96a998b2f10 100644 (file)
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -1837,6 +1837,11 @@ possible to configure the remote login shell.  This avoids problems
 with remote hosts, where "/bin/sh" is a link to a shell which
 cooperates badly with Tramp.
 
++++
+*** New commands 'tramp-rename-files' and 'tramp-rename-these-files'.
+They allow to save remote files somewhere else when the corresponding
+host is not reachable anymore.
+
 ** Rcirc
 
 ---
index 56ccf7380700f5c724787bf2ecb85088fe17944e..96b11c7f5240d73734c039f5db76559ba3fb461c 100644 (file)
@@ -195,6 +195,268 @@ This includes password cache, file cache, connection cache, buffers."
   (dolist (name (tramp-list-remote-buffers))
     (when (bufferp (get-buffer name)) (kill-buffer name))))
 
+;;;###tramp-autoload
+(defcustom tramp-default-rename-alist nil
+  "Default target for renaming remote buffer file names.
+This is an alist of cons cells (SOURCE . TARGET).  The first
+matching item specifies the target to be applied for renaming
+buffer file names from source via `tramp-rename-files'.  SOURCE
+is a regular expressions, which matches a remote file name.
+TARGET must be a directory name, which could be remote (including
+remote directories Tramp infers by default, such as
+\"/method:user@host:\").
+
+TARGET can contain the patterns %m, %u or %h, which are replaced
+by the method name, user name or host name of SOURCE when calling
+`tramp-rename-files'.
+
+SOURCE could also be a Lisp form, which will be evaluated.  The
+result must be a string or nil, which is interpreted as a regular
+expression which always matches."
+  :group 'tramp
+  :version "27.1"
+  :type '(repeat (cons (choice :tag "Source regexp" regexp sexp)
+                      (choice :tag "Target   name" string (const nil)))))
+
+;;;###tramp-autoload
+(defcustom tramp-confirm-rename-file-names t
+  "Whether renaming a buffer file name must be confirmed."
+  :group 'tramp
+  :version "27.1"
+  :type 'boolean)
+
+(defun tramp-default-rename-file (string)
+  "Determine default file name for renaming according to STRING.
+The user option `tramp-default-rename-alist' is consulted,
+finding the default mapping.  If there is no matching entry, the
+function returns nil"
+  (when (tramp-tramp-file-p string)
+    (let ((tdra tramp-default-rename-alist)
+         (method (or (file-remote-p string 'method) ""))
+         (user (or (file-remote-p string 'user) ""))
+         (host (or (file-remote-p string 'host) ""))
+         item result)
+      (while (setq item (pop tdra))
+       (when (string-match-p (or (eval (car item)) "") string)
+         (setq tdra nil
+               result
+               (format-spec
+                (cdr item) (format-spec-make ?m method ?u user ?h host)))))
+      result)))
+
+(defsubst tramp-rename-read-file-name-dir (string)
+  "Return the DIR entry to be applied in `read-file-name', based on STRING."
+  (when (tramp-tramp-file-p string)
+    (substring (file-remote-p string) 0 -1)))
+
+(defsubst tramp-rename-read-file-name-init (string)
+  "Return the INIT entry to be applied in `read-file-name', based on STRING."
+  (when (tramp-tramp-file-p string)
+    (string-remove-prefix (tramp-rename-read-file-name-dir string) string)))
+
+;;;###tramp-autoload
+(defun tramp-rename-files (source target)
+  "Replace in all buffers the visiting file name from SOURCE to TARGET.
+SOURCE is a remote directory name, which could contain also a
+localname part.  TARGET is the directory name SOURCE is replaced
+with.  Often, TARGET is a remote directory name on another host,
+but it can also be a local directory name.  If TARGET has no
+local part, the local part from SOURCE is used.
+
+If TARGET is nil, it is selected according to the first match in
+`tramp-default-rename-alist'.  If called interactively, this
+match is offered as initial value for selection.
+
+On all buffers, which have a `buffer-file-name' matching SOURCE,
+this name is modified by replacing SOURCE with TARGET.  This is
+applied by calling `set-visited-file-name'.  The new
+`buffer-file-name' is prompted for modification in the
+minibuffer.  The buffers are marked modified, and must be saved
+explicitly.
+
+If user option `tramp-confirm-rename-file-names' is nil, changing
+the file name happens without confirmation.  This requires a
+matching entry in `tramp-default-rename-alist'.
+
+Remote buffers related to the remote connection identified by
+SOURCE, which are not visiting files, or which are visiting files
+not matching SOURCE, are not modified.
+
+Interactively, TARGET is selected from `tramp-default-rename-alist'
+without confirmation if the prefix argument is non-nil.
+
+The remote connection identified by SOURCE is flushed by
+`tramp-cleanup-connection'."
+  (interactive
+   (let ((connections
+         (mapcar #'tramp-make-tramp-file-name (tramp-list-connections)))
+        ;; Completion packages do their voodoo in `completing-read'
+        ;; and `read-file-name', which is often incompatible with
+        ;; Tramp.  Ignore them.
+        (completing-read-function #'completing-read-default)
+        (read-file-name-function #'read-file-name-default)
+         source target)
+     (if (null connections)
+        (tramp-user-error nil "There are no remote connections.")
+       (setq source
+            ;; Likely, the source remote connection is broken. So we
+            ;; shall avoid any action on it.
+            (let (non-essential)
+              (completing-read-default
+               "Enter old Tramp connection: "
+               ;; Completion function.
+               (completion-table-dynamic
+                (lambda (string)
+                  (cond
+                   ;; Initially, show existing remote connections.
+                   ((not (tramp-tramp-file-p string))
+                    (all-completions string connections))
+                   ;; There is a selected remote connection.  Show
+                   ;; its longest common directory path of respective
+                   ;; buffers.
+                   (t (mapcar
+                       (lambda (buffer)
+                         (let ((bfn (buffer-file-name buffer)))
+                           (and (buffer-live-p buffer)
+                                (tramp-equal-remote string bfn)
+                                (stringp bfn) (file-name-directory bfn))))
+                       (tramp-list-remote-buffers))))))
+               #'tramp-tramp-file-p t
+               ;; If the current buffer is a remote one, it is likely
+               ;; that this connection is meant.  So we offer it as
+               ;; initial value.  Otherwise, use the longest remote
+               ;; connection path as initial value.
+               (or (file-remote-p default-directory)
+                   (try-completion "" connections))))
+
+            target
+            (when (null current-prefix-arg)
+              ;; The source remote connection shall not trigger any action.
+              ;; FIXME: Better error prompt when trying to access source host.
+              (let* ((default (or (tramp-default-rename-file source) source))
+                     (dir (tramp-rename-read-file-name-dir default))
+                     (init (tramp-rename-read-file-name-init default))
+                     (tramp-ignored-file-name-regexp
+                      (regexp-quote (file-remote-p source))))
+                (read-file-name-default
+                 "Enter new Tramp connection: "
+                 dir default 'confirm init #'file-directory-p)))))
+
+     (list source target)))
+
+  (unless (tramp-tramp-file-p source)
+    (tramp-user-error nil "Source %s must be remote." source))
+  (when (null target)
+    (or (setq target (tramp-default-rename-file source))
+       (tramp-user-error
+        nil
+        (eval-when-compile
+          (concat "There is no target specified.  "
+                  "Check `tramp-default-rename-alist' for a proper entry.")))))
+  (when (tramp-equal-remote source target)
+    (tramp-user-error nil "Source and target must have different remote."))
+
+  ;; Append local file name if none is specified.
+  (when (string-equal (file-remote-p target) target)
+    (setq target (concat target (file-remote-p source 'localname))))
+  ;; Make them directoy names.
+  (setq source (directory-file-name source)
+       target (directory-file-name target))
+
+  ;; Rename visited file names of source buffers.
+  (save-window-excursion
+    (save-current-buffer
+      (let ((help-form "\
+Type SPC or `y' to set visited file name,
+DEL or `n' to skip to next,
+`e' to edit the visited file name,
+ESC or `q' to quit without changing further buffers,
+`!' to change all remaining buffers with no more questions.")
+           (query-choices '(?y ?\s ?n ?\177 ?! ?e ?q ?\e))
+           (query (unless tramp-confirm-rename-file-names ?!))
+           changed-buffers)
+       (dolist (buffer (tramp-list-remote-buffers))
+          (switch-to-buffer buffer)
+         (let* ((bfn (buffer-file-name))
+                (new-bfn (and (stringp bfn)
+                              (replace-regexp-in-string
+                               (regexp-quote source) target bfn)))
+                (prompt (format-message
+                         "Set visited file name to `%s' [Type yn!eq or %s] "
+                         new-bfn (key-description (vector help-char)))))
+           (when (and (buffer-live-p buffer) (stringp bfn)
+                      (string-prefix-p source bfn)
+                      ;; Skip, and don't ask again.
+                      (not (memq query '(?q ?\e))))
+             ;; Read prompt.
+             (unless (eq query ?!)
+               (setq query (read-char-choice prompt query-choices)))
+             ;; Edit the new buffer file name.
+             (when (eq query ?e)
+               (setq new-bfn
+                     (read-file-name
+                      "New visited file name: "
+                      (file-name-directory new-bfn) new-bfn)))
+             ;; Set buffer file name.  Remember the change.
+             (when (memq query '(?y ?\s ?! ?e))
+               (setq changed-buffers
+                     (cons (list buffer bfn (buffer-modified-p))
+                           changed-buffers))
+                (set-visited-file-name new-bfn))
+             ;; Quit.  Revert changes if prompted by user.
+             (when (and (memq query '(?q ?\e)) changed-buffers
+                        (y-or-n-p "Do you want to revert applied changes?"))
+               (dolist (item changed-buffers)
+                 (with-current-buffer (car item)
+                   (set-visited-file-name (nth 1 item))
+                   (set-buffer-modified-p (nth 2 item)))))
+             ;; Cleanup echo area.
+             (message nil)))))))
+
+  ;; Cleanup.
+  (tramp-cleanup-connection (tramp-dissect-file-name source)))
+
+;;;###tramp-autoload
+(defun tramp-rename-these-files (target)
+  "Replace visiting file names to TARGET.
+The current buffer must be related to a remote connection.  In
+all buffers, which are visiting a file with the same directory
+name, the buffer file name is changed.
+
+Interactively, TARGET is selected from `tramp-default-rename-alist'
+without confirmation if the prefix argument is non-nil.
+
+For details, see `tramp-rename-files'."
+  (interactive
+   (let ((source default-directory)
+        target
+        ;; Completion packages do their voodoo in `completing-read'
+        ;; and `read-file-name', which is often incompatible with
+        ;; Tramp.  Ignore them.
+        (completing-read-function #'completing-read-default)
+        (read-file-name-function #'read-file-name-default))
+     (if (not (tramp-tramp-file-p source))
+        (tramp-user-error
+         nil
+         (substitute-command-keys
+          (concat "Current buffer is not remote.  "
+                  "Consider `\\[tramp-rename-files]' instead.")))
+       (setq target
+            (when (null current-prefix-arg)
+              ;; The source remote connection shall not trigger any action.
+              ;; FIXME: Better error prompt when trying to access source host.
+              (let* ((default (or (tramp-default-rename-file source) source))
+                     (dir (tramp-rename-read-file-name-dir default))
+                     (init (tramp-rename-read-file-name-init default))
+                     (tramp-ignored-file-name-regexp
+                      (regexp-quote (file-remote-p source))))
+                (read-file-name-default
+                 (format "Change Tramp connection `%s': " source)
+                 dir default 'confirm init #'file-directory-p)))))
+     (list target)))
+
+  (tramp-rename-files default-directory target))
+
 ;; Tramp version is useful in a number of situations.
 
 ;;;###tramp-autoload
@@ -424,11 +686,6 @@ please ensure that the buffers are attached to your email.\n\n"))
 
 ;; * Clean up unused *tramp/foo* buffers after a while.  (Pete Forman)
 ;;
-;; * WIBNI there was an interactive command prompting for Tramp
-;;   method, hostname, username and filename and translates the user
-;;   input into the correct filename syntax (depending on the Emacs
-;;   flavor)  (Reiner Steib)
-;;
 ;; * Let the user edit the connection properties interactively.
 ;;   Something like `gnus-server-edit-server' in Gnus' *Server* buffer.
 
index 0bb19ed9c4d147dfd1c3a45fa82d806371b6f097..0c3107603fb5786e826f8787b16c7e0d5700f9ec 100644 (file)
@@ -36,6 +36,8 @@
 (declare-function tramp-file-name-equal-p "tramp")
 (declare-function tramp-tramp-file-p "tramp")
 (defvar eshell-path-env)
+(defvar ido-read-file-name-non-ido)
+(defvar ivy-completing-read-handlers-alist)
 (defvar recentf-exclude)
 (defvar tramp-current-connection)
 (defvar tramp-postfix-host-format)
@@ -170,6 +172,20 @@ NAME must be equal to `tramp-current-connection'."
              (remove-hook 'tramp-cleanup-all-connections-hook
                           #'tramp-recentf-cleanup-all))))
 
+;;; Integration of ido.el:
+
+(with-eval-after-load 'ido
+  (add-to-list 'ido-read-file-name-non-ido 'tramp-rename-files)
+  (add-to-list 'ido-read-file-name-non-ido 'tramp-these-rename-files))
+
+;;; Integration of ivy.el:
+
+(with-eval-after-load 'ivy
+  (add-to-list 'ivy-completing-read-handlers-alist
+              '(tramp-rename-files . completing-read-default))
+  (add-to-list 'ivy-completing-read-handlers-alist
+              '(tramp-these-rename-files . completing-read-default)))
+
 ;;; Default connection-local variables for Tramp:
 
 (defconst tramp-connection-local-default-profile