From: Michael Albinus Date: Wed, 20 Nov 2019 12:45:30 +0000 (+0100) Subject: Add renaming of remote buffer file names to Tramp X-Git-Tag: emacs-27.0.90~563 X-Git-Url: http://git.eshelyaron.com/gitweb/?a=commitdiff_plain;h=035931777bd89b939436fd1d8a2b8d5a80ede095;p=emacs.git Add renaming of remote buffer file names to Tramp * 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. --- diff --git a/doc/misc/tramp.texi b/doc/misc/tramp.texi index 67472a87a32..dd1378a8da7 100644 --- a/doc/misc/tramp.texi +++ b/doc/misc/tramp.texi @@ -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}? diff --git a/etc/NEWS b/etc/NEWS index 4887b8e6819..dd08675f425 100644 --- 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 --- diff --git a/lisp/net/tramp-cmds.el b/lisp/net/tramp-cmds.el index 56ccf738070..96b11c7f524 100644 --- a/lisp/net/tramp-cmds.el +++ b/lisp/net/tramp-cmds.el @@ -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. diff --git a/lisp/net/tramp-integration.el b/lisp/net/tramp-integration.el index 0bb19ed9c4d..0c3107603fb 100644 --- a/lisp/net/tramp-integration.el +++ b/lisp/net/tramp-integration.el @@ -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