diff options
| author | Michael Albinus | 2018-07-18 16:51:56 +0200 |
|---|---|---|
| committer | Michael Albinus | 2018-07-18 16:51:56 +0200 |
| commit | 7a258fa0bb9f9e7600229b95ed54ee5ee0bc9a0c (patch) | |
| tree | 9654a226674f2153c703512f1298adc1866d1ceb /lisp | |
| parent | cb50077b1eb7c1467f2f200e01599b391d025bfa (diff) | |
| download | emacs-7a258fa0bb9f9e7600229b95ed54ee5ee0bc9a0c.tar.gz emacs-7a258fa0bb9f9e7600229b95ed54ee5ee0bc9a0c.zip | |
Adapt shadowfile.el for Tramp (Bug#4526, Bug#4846)
* etc/NEWS: Mention changes in shadowfile.el.
* lisp/shadowfile.el (top): Require 'tramp instead of 'ange-ftp.
(shadow-cluster): New defstruct.
(shadow-make-cluster, shadow-cluster-name, shadow-cluster-primary)
(shadow-cluster-regexp, shadow-get-user)
(shadow-parse-fullname): Remove.
(shadow-info-file, shadow-todo-file, shadow-system-name)
(shadow-homedir, shadow-regexp-superquote, shadow-suffix)
(shadow-set-cluster, shadow-get-cluster, shadow-site-name)
(shadow-name-site, shadow-site-primary, shadow-site-cluster)
(shadow-read-site, shadow-parse-name, shadow-make-fullname)
(shadow-replace-name-component, shadow-local-file)
(shadow-expand-cluster-in-file-name, shadow-contract-file-name)
(shadow-same-site, shadow-file-match, shadow-define-cluster)
(shadow-define-literal-group, shadow-define-regexp-group)
(shadow-make-group, shadow-shadows-of-1, shadow-read-files)
(shadow-write-info-file, shadow-write-todo-file)
(shadow-initialize): Adapt variables and functions.
* test/lisp/shadowfile-tests.el: New file.
Diffstat (limited to 'lisp')
| -rw-r--r-- | lisp/shadowfile.el | 462 |
1 files changed, 230 insertions, 232 deletions
diff --git a/lisp/shadowfile.el b/lisp/shadowfile.el index 0095d6959ef..e1a9b8e1d98 100644 --- a/lisp/shadowfile.el +++ b/lisp/shadowfile.el | |||
| @@ -25,37 +25,38 @@ | |||
| 25 | ;; This package helps you to keep identical copies of files in more than one | 25 | ;; This package helps you to keep identical copies of files in more than one |
| 26 | ;; place - possibly on different machines. When you save a file, it checks | 26 | ;; place - possibly on different machines. When you save a file, it checks |
| 27 | ;; whether it is on the list of files with "shadows", and if so, it tries to | 27 | ;; whether it is on the list of files with "shadows", and if so, it tries to |
| 28 | ;; copy it when you exit Emacs (or use the shadow-copy-files command). | 28 | ;; copy it when you exit Emacs (or use the `shadow-copy-files' command). |
| 29 | 29 | ||
| 30 | ;; Installation & Use: | 30 | ;; Installation & Use: |
| 31 | 31 | ||
| 32 | ;; Add clusters (if necessary) and file groups with shadow-define-cluster, | 32 | ;; Add clusters (if necessary) and file groups with `shadow-define-cluster', |
| 33 | ;; shadow-define-literal-group, and shadow-define-regexp-group (see the | 33 | ;; `shadow-define-literal-group', and `shadow-define-regexp-group' (see the |
| 34 | ;; documentation for these functions for information on how and when to use | 34 | ;; documentation for these functions for information on how and when to use |
| 35 | ;; them). After doing this once, everything should be automatic. | 35 | ;; them). After doing this once, everything should be automatic. |
| 36 | 36 | ||
| 37 | ;; The lists of clusters and shadows are saved in a ~/.emacs.d/shadows | 37 | ;; The lists of clusters and shadows are saved in `shadow-info-file', |
| 38 | ;; (`shadow-info-file') file, so that they can be remembered from one | 38 | ;; so that they can be remembered from one Emacs session to another, |
| 39 | ;; Emacs session to another, even (as much as possible) if the Emacs | 39 | ;; even (as much as possible) if the Emacs session terminates |
| 40 | ;; session terminates abnormally. The files needing to be copied are | 40 | ;; abnormally. The files needing to be copied are stored in |
| 41 | ;; stored in `shadow-todo-file'; if a file cannot be copied for any | 41 | ;; `shadow-todo-file'; if a file cannot be copied for any reason, it |
| 42 | ;; reason, it will stay on the list to be tried again next time. The | 42 | ;; will stay on the list to be tried again next time. The |
| 43 | ;; `shadow-info-file' file should itself have shadows on all your accounts | 43 | ;; `shadow-info-file' file should itself have shadows on all your |
| 44 | ;; so that the information in it is consistent everywhere, but | 44 | ;; accounts so that the information in it is consistent everywhere, |
| 45 | ;; `shadow-todo-file' is local information and should have no shadows. | 45 | ;; but `shadow-todo-file' is local information and should have no |
| 46 | ;; shadows. | ||
| 46 | 47 | ||
| 47 | ;; If you do not want to copy a particular file, you can answer "no" and | 48 | ;; If you do not want to copy a particular file, you can answer "no" and |
| 48 | ;; be asked again next time you hit C-x 4 s or exit Emacs. If you do not | 49 | ;; be asked again next time you hit "C-x 4 s" or exit Emacs. If you do not |
| 49 | ;; want to be asked again, use shadow-cancel, and you will not be asked | 50 | ;; want to be asked again, use "M-x shadow-cancel", and you will not be asked |
| 50 | ;; until you change the file and save it again. If you do not want to | 51 | ;; until you change the file and save it again. If you do not want to |
| 51 | ;; shadow that file ever again, you can edit it out of the shadows | 52 | ;; shadow that file ever again, you can edit it out of the shadows |
| 52 | ;; buffer. Anytime you edit the shadows buffer, you must type M-x | 53 | ;; buffer. Anytime you edit the shadows buffer, you must type "M-x |
| 53 | ;; shadow-read-files to load in the new information, or your changes will | 54 | ;; shadow-read-files" to load in the new information, or your changes will |
| 54 | ;; be overwritten! | 55 | ;; be overwritten! |
| 55 | 56 | ||
| 56 | ;; Bugs & Warnings: | 57 | ;; Bugs & Warnings: |
| 57 | ;; | 58 | ;; |
| 58 | ;; - It is bad to have two emacses both running shadowfile at the same | 59 | ;; - It is bad to have two Emacsen both running shadowfile at the same |
| 59 | ;; time. It tries to detect this condition, but is not always successful. | 60 | ;; time. It tries to detect this condition, but is not always successful. |
| 60 | ;; | 61 | ;; |
| 61 | ;; - You have to be careful not to edit a file in two locations | 62 | ;; - You have to be careful not to edit a file in two locations |
| @@ -64,19 +65,16 @@ | |||
| 64 | ;; | 65 | ;; |
| 65 | ;; - It ought to check modification times of both files to make sure | 66 | ;; - It ought to check modification times of both files to make sure |
| 66 | ;; it is doing the right thing. This will have to wait until | 67 | ;; it is doing the right thing. This will have to wait until |
| 67 | ;; file-newer-than-file-p works between machines. | 68 | ;; `file-newer-than-file-p' works between machines. |
| 68 | ;; | 69 | ;; |
| 69 | ;; - It will not make directories for you, it just fails to copy files | 70 | ;; - It will not make directories for you, it just fails to copy files |
| 70 | ;; that belong in non-existent directories. | 71 | ;; that belong in non-existent directories. |
| 71 | ;; | ||
| 72 | ;; Please report any bugs to me (boris@gnu.org). Also let me know | ||
| 73 | ;; if you have suggestions or would like to be informed of updates. | ||
| 74 | 72 | ||
| 75 | 73 | ||
| 76 | ;;; Code: | 74 | ;;; Code: |
| 77 | 75 | ||
| 78 | (require 'cl-lib) | 76 | (require 'cl-lib) |
| 79 | (require 'ange-ftp) | 77 | (require 'tramp) |
| 80 | 78 | ||
| 81 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | 79 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; |
| 82 | ;;; Variables | 80 | ;;; Variables |
| @@ -107,35 +105,35 @@ files that have been changed and need to be copied to other systems." | |||
| 107 | :type 'boolean | 105 | :type 'boolean |
| 108 | :group 'shadow) | 106 | :group 'shadow) |
| 109 | 107 | ||
| 110 | ;; FIXME in a sense, this changed in 24.4 (addition of locate-user-emacs-file), | 108 | (defcustom shadow-info-file (locate-user-emacs-file "shadows" ".shadows") |
| 111 | ;; but due to the weird way this variable is initialized to nil, it didn't | ||
| 112 | ;; literally change. Same for shadow-todo-file. | ||
| 113 | (defcustom shadow-info-file nil | ||
| 114 | "File to keep shadow information in. | 109 | "File to keep shadow information in. |
| 115 | The `shadow-info-file' should be shadowed to all your accounts to | 110 | The `shadow-info-file' should be shadowed to all your accounts to |
| 116 | ensure consistency. Default: ~/.emacs.d/shadows" | 111 | ensure consistency. Default: ~/.emacs.d/shadows" |
| 117 | :type '(choice (const nil) file) | 112 | :type 'file |
| 118 | :group 'shadow) | 113 | :group 'shadow |
| 114 | :version "26.2") | ||
| 119 | 115 | ||
| 120 | (defcustom shadow-todo-file nil | 116 | (defcustom shadow-todo-file |
| 117 | (locate-user-emacs-file "shadow_todo" ".shadow_todo") | ||
| 121 | "File to store the list of uncopied shadows in. | 118 | "File to store the list of uncopied shadows in. |
| 122 | This means that if a remote system is down, or for any reason you cannot or | 119 | This means that if a remote system is down, or for any reason you cannot or |
| 123 | decide not to copy your shadow files at the end of one Emacs session, it will | 120 | decide not to copy your shadow files at the end of one Emacs session, it will |
| 124 | remember and ask you again in your next Emacs session. | 121 | remember and ask you again in your next Emacs session. |
| 125 | This file must NOT be shadowed to any other system, it is host-specific. | 122 | This file must NOT be shadowed to any other system, it is host-specific. |
| 126 | Default: ~/.emacs.d/shadow_todo" | 123 | Default: ~/.emacs.d/shadow_todo" |
| 127 | :type '(choice (const nil) file) | 124 | :type 'file |
| 128 | :group 'shadow) | 125 | :group 'shadow |
| 126 | :version "26.2") | ||
| 129 | 127 | ||
| 130 | 128 | ||
| 131 | ;;; The following two variables should in most cases initialize themselves | 129 | ;;; The following two variables should in most cases initialize themselves |
| 132 | ;;; correctly. They are provided as variables in case the defaults are wrong | 130 | ;;; correctly. They are provided as variables in case the defaults are wrong |
| 133 | ;;; on your machine (and for efficiency). | 131 | ;;; on your machine (and for efficiency). |
| 134 | 132 | ||
| 135 | (defvar shadow-system-name (system-name) | 133 | (defvar shadow-system-name (concat "/" (system-name) ":") |
| 136 | "The complete hostname of this machine.") | 134 | "The identification for local files on this machine.") |
| 137 | 135 | ||
| 138 | (defvar shadow-homedir nil | 136 | (defvar shadow-homedir "~" |
| 139 | "Your home directory on this machine.") | 137 | "Your home directory on this machine.") |
| 140 | 138 | ||
| 141 | ;;; | 139 | ;;; |
| @@ -186,12 +184,12 @@ created by `shadow-define-regexp-group'.") | |||
| 186 | (car list)) | 184 | (car list)) |
| 187 | 185 | ||
| 188 | (defun shadow-regexp-superquote (string) | 186 | (defun shadow-regexp-superquote (string) |
| 189 | "Like `regexp-quote', but includes the ^ and $. | 187 | "Like `regexp-quote', but includes the \\` and \\'. |
| 190 | This makes sure regexp matches nothing but STRING." | 188 | This makes sure regexp matches nothing but STRING." |
| 191 | (concat "^" (regexp-quote string) "$")) | 189 | (concat "\\`" (regexp-quote string) "\\'")) |
| 192 | 190 | ||
| 193 | (defun shadow-suffix (prefix string) | 191 | (defun shadow-suffix (prefix string) |
| 194 | "If PREFIX begins STRING, return the rest. | 192 | "If PREFIX begins with STRING, return the rest. |
| 195 | Return value is non-nil if PREFIX and STRING are `string=' up to the length of | 193 | Return value is non-nil if PREFIX and STRING are `string=' up to the length of |
| 196 | PREFIX." | 194 | PREFIX." |
| 197 | (let ((lp (length prefix)) | 195 | (let ((lp (length prefix)) |
| @@ -204,70 +202,66 @@ PREFIX." | |||
| 204 | ;;; Clusters and sites | 202 | ;;; Clusters and sites |
| 205 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | 203 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; |
| 206 | 204 | ||
| 207 | ;;; I use the term `site' to refer to a string which may be the name of a | 205 | ;;; I use the term `site' to refer to a string which may be the |
| 208 | ;;; cluster or a literal hostname. All user-level commands should accept | 206 | ;;; cluster identification "/name:", a remote identification |
| 209 | ;;; either. | 207 | ;;; "/method:user@host:", or "/system-name:' (the value of |
| 210 | 208 | ;;; `shadow-system-name') for the location of local files. All | |
| 211 | (defun shadow-make-cluster (name primary regexp) | 209 | ;;; user-level commands should accept either. |
| 212 | "Create a shadow cluster. | ||
| 213 | It is called NAME, uses the PRIMARY hostname and REGEXP matching all | ||
| 214 | hosts in the cluster. The variable `shadow-clusters' associates the | ||
| 215 | names of clusters to these structures. This function is for program | ||
| 216 | use: to create clusters interactively, use `shadow-define-cluster' | ||
| 217 | instead." | ||
| 218 | (list name primary regexp)) | ||
| 219 | |||
| 220 | (defmacro shadow-cluster-name (cluster) | ||
| 221 | "Return the name of the CLUSTER." | ||
| 222 | (list 'elt cluster 0)) | ||
| 223 | 210 | ||
| 224 | (defmacro shadow-cluster-primary (cluster) | 211 | (cl-defstruct (shadow-cluster (:type list) :named) name primary regexp) |
| 225 | "Return the primary hostname of a CLUSTER." | ||
| 226 | (list 'elt cluster 1)) | ||
| 227 | |||
| 228 | (defmacro shadow-cluster-regexp (cluster) | ||
| 229 | "Return the regexp matching hosts in a CLUSTER." | ||
| 230 | (list 'elt cluster 2)) | ||
| 231 | 212 | ||
| 232 | (defun shadow-set-cluster (name primary regexp) | 213 | (defun shadow-set-cluster (name primary regexp) |
| 233 | "Put cluster NAME on the list of clusters. | 214 | "Put cluster NAME on the list of clusters. |
| 234 | Replace old definition, if any. PRIMARY and REGEXP are the | 215 | Replace old definition, if any. PRIMARY and REGEXP are the |
| 235 | information defining the cluster. For interactive use, call | 216 | information defining the cluster. For interactive use, call |
| 236 | `shadow-define-cluster' instead." | 217 | `shadow-define-cluster' instead." |
| 237 | (let ((rest (cl-remove-if (lambda (x) (equal name (car x))) | 218 | (let ((rest (cl-remove-if (lambda (x) (equal name (shadow-cluster-name x))) |
| 238 | shadow-clusters))) | 219 | shadow-clusters))) |
| 239 | (setq shadow-clusters | 220 | (setq shadow-clusters |
| 240 | (cons (shadow-make-cluster name primary regexp) | 221 | (cons (make-shadow-cluster :name name :primary primary :regexp regexp) |
| 241 | rest)))) | 222 | rest)))) |
| 242 | 223 | ||
| 243 | (defmacro shadow-get-cluster (name) | 224 | (defun shadow-get-cluster (name) |
| 244 | "Return cluster named NAME, or nil." | 225 | "Return cluster named NAME, or nil." |
| 245 | (list 'assoc name 'shadow-clusters)) | 226 | (shadow-find |
| 227 | (lambda (x) (string-equal (shadow-cluster-name x) name)) | ||
| 228 | shadow-clusters)) | ||
| 229 | |||
| 230 | ;;; SITES | ||
| 231 | |||
| 232 | (defun shadow-site-name (site) | ||
| 233 | "Return name if SITE has the form \"/name:\", otherwise SITE." | ||
| 234 | (if (string-match "\\`/\\(\\w+\\):\\'" site) | ||
| 235 | (match-string 1 site) site)) | ||
| 236 | |||
| 237 | (defun shadow-name-site (name) | ||
| 238 | "Return \"/name:\" if NAME has word syntax, otherwise NAME." | ||
| 239 | (if (string-match "\\`\\w+\\'" name) | ||
| 240 | (format "/%s:"name) name)) | ||
| 246 | 241 | ||
| 247 | (defun shadow-site-primary (site) | 242 | (defun shadow-site-primary (site) |
| 248 | "If SITE is a cluster, return primary host, otherwise return SITE." | 243 | "If SITE is a cluster, return primary identification, otherwise return SITE." |
| 249 | (let ((c (shadow-get-cluster site))) | 244 | (let ((cluster (shadow-get-cluster (shadow-site-name site)))) |
| 250 | (if c | 245 | (if cluster |
| 251 | (shadow-cluster-primary c) | 246 | (shadow-cluster-primary cluster) |
| 252 | site))) | 247 | site))) |
| 253 | 248 | ||
| 254 | ;;; SITES | ||
| 255 | |||
| 256 | (defun shadow-site-cluster (site) | 249 | (defun shadow-site-cluster (site) |
| 257 | "Given a SITE (hostname or cluster name), return cluster it is in, or nil." | 250 | "Given a SITE, return cluster it is in, or nil." |
| 258 | (or (assoc site shadow-clusters) | 251 | (or (shadow-get-cluster (shadow-site-name site)) |
| 259 | (shadow-find | 252 | (shadow-find |
| 260 | (function (lambda (x) | 253 | (lambda (x) |
| 261 | (string-match (shadow-cluster-regexp x) | 254 | (string-match (shadow-cluster-regexp x) (shadow-name-site site))) |
| 262 | site))) | ||
| 263 | shadow-clusters))) | 255 | shadow-clusters))) |
| 264 | 256 | ||
| 265 | (defun shadow-read-site () | 257 | (defun shadow-read-site () |
| 266 | "Read a cluster name or hostname from the minibuffer." | 258 | "Read a cluster name or host identification from the minibuffer." |
| 267 | (let ((ans (completing-read "Host or cluster name [RET when done]: " | 259 | (let ((ans (completing-read "Host identification or cluster name: " |
| 268 | shadow-clusters))) | 260 | shadow-clusters))) |
| 269 | (if (equal "" ans) | 261 | (when (or (shadow-get-cluster (shadow-site-name ans)) |
| 270 | nil | 262 | (string-equal ans shadow-system-name) |
| 263 | (string-equal ans (shadow-site-name shadow-system-name)) | ||
| 264 | (setq ans (file-remote-p ans))) | ||
| 271 | ans))) | 265 | ans))) |
| 272 | 266 | ||
| 273 | (defun shadow-site-match (site1 site2) | 267 | (defun shadow-site-match (site1 site2) |
| @@ -281,63 +275,95 @@ be matched against the primary of SITE2." | |||
| 281 | (string-match (shadow-cluster-regexp cluster1) primary2) | 275 | (string-match (shadow-cluster-regexp cluster1) primary2) |
| 282 | (string-equal site1 primary2))))) | 276 | (string-equal site1 primary2))))) |
| 283 | 277 | ||
| 284 | (defun shadow-get-user (site) | ||
| 285 | "Return the default username for a SITE." | ||
| 286 | (ange-ftp-get-user (shadow-site-primary site))) | ||
| 287 | |||
| 288 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | 278 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; |
| 289 | ;;; Filename manipulation | 279 | ;;; Filename manipulation |
| 290 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | 280 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; |
| 291 | 281 | ||
| 292 | (defun shadow-parse-fullname (fullname) | ||
| 293 | "Parse FULLNAME into (site user path) list. | ||
| 294 | Leave it alone if it already is one. Return nil if the argument is | ||
| 295 | not a full ange-ftp pathname." | ||
| 296 | (if (listp fullname) | ||
| 297 | fullname | ||
| 298 | (ange-ftp-ftp-name fullname))) | ||
| 299 | |||
| 300 | (defun shadow-parse-name (name) | 282 | (defun shadow-parse-name (name) |
| 301 | "Parse any NAME into (site user name) list. | 283 | "Parse any NAME into a `tramp-file-name' structure. |
| 302 | Argument can be a simple name, full ange-ftp name, or already a hup list." | 284 | Argument can be a simple name, remote file name, or already a |
| 303 | (or (shadow-parse-fullname name) | 285 | `tramp-file-name' structure." |
| 304 | (list shadow-system-name | 286 | (cond |
| 305 | (user-login-name) | 287 | ((null name) nil) |
| 306 | name))) | 288 | ((tramp-file-name-p name) name) |
| 307 | 289 | ((file-remote-p name) (tramp-dissect-file-name name)) | |
| 308 | (defsubst shadow-make-fullname (host user name) | 290 | ((shadow-local-file name) |
| 309 | "Make an ange-ftp style fullname out of HOST, USER (optional), and NAME. | 291 | (make-tramp-file-name |
| 310 | This is probably not as general as it ought to be." | 292 | :host (shadow-site-name shadow-system-name) |
| 311 | (concat "/" | 293 | :localname (shadow-local-file name))) |
| 312 | (if user (concat user "@")) | 294 | ;; Cluster name. |
| 313 | host ":" | 295 | ((string-match "^/\\([^:/]+\\):\\([^:]*\\)$" name) |
| 314 | name)) | 296 | (let ((name (match-string 1 name)) |
| 297 | (file (match-string 2 name))) | ||
| 298 | (when (shadow-get-cluster name) | ||
| 299 | (make-tramp-file-name :host name :localname file)))))) | ||
| 300 | |||
| 301 | (defsubst shadow-make-fullname (hup &optional host name) | ||
| 302 | "Make a Tramp style fullname out of HUP, a `tramp-file-name' structure. | ||
| 303 | Replace HOST, and NAME when non-nil." | ||
| 304 | (let ((hup (copy-tramp-file-name hup))) | ||
| 305 | (when host (setf (tramp-file-name-host hup) host)) | ||
| 306 | (when name (setf (tramp-file-name-localname hup) name)) | ||
| 307 | (if (null (tramp-file-name-method hup)) | ||
| 308 | (format | ||
| 309 | "/%s:%s" (tramp-file-name-host hup) (tramp-file-name-localname hup)) | ||
| 310 | (tramp-make-tramp-file-name | ||
| 311 | (tramp-file-name-method hup) | ||
| 312 | (tramp-file-name-user hup) | ||
| 313 | (tramp-file-name-domain hup) | ||
| 314 | (tramp-file-name-host hup) | ||
| 315 | (tramp-file-name-port hup) | ||
| 316 | (tramp-file-name-localname hup) | ||
| 317 | (tramp-file-name-hop hup))))) | ||
| 315 | 318 | ||
| 316 | (defun shadow-replace-name-component (fullname newname) | 319 | (defun shadow-replace-name-component (fullname newname) |
| 317 | "Return FULLNAME with the name component changed to NEWNAME." | 320 | "Return FULLNAME with the name component changed to NEWNAME." |
| 318 | (let ((hup (shadow-parse-fullname fullname))) | 321 | (concat (file-remote-p fullname) newname)) |
| 319 | (shadow-make-fullname (nth 0 hup) (nth 1 hup) newname))) | ||
| 320 | 322 | ||
| 321 | (defun shadow-local-file (file) | 323 | (defun shadow-local-file (file) |
| 322 | "If FILE is at this site, remove /user@host part. | 324 | "If FILE is not remote, return it. |
| 323 | If refers to a different system or a different user on this system, | 325 | If it refers to a different system, return nil." |
| 324 | return nil." | 326 | (cond |
| 325 | (let ((hup (shadow-parse-fullname file))) | 327 | ((null file) nil) |
| 326 | (cond ((null hup) file) | 328 | ;; `tramp-file-name' structure. |
| 327 | ((and (shadow-site-match (nth 0 hup) shadow-system-name) | 329 | ((and (tramp-file-name-p file) (null (tramp-file-name-method file))) |
| 328 | (string-equal (nth 1 hup) (user-login-name))) | 330 | (tramp-file-name-localname file)) |
| 329 | (nth 2 hup)) | 331 | ((tramp-file-name-p file) nil) |
| 330 | (t nil)))) | 332 | ;; Local host name. |
| 333 | ((string-match | ||
| 334 | (format "^%s\\([^:]*\\)$" (regexp-quote shadow-system-name)) file) | ||
| 335 | (match-string 1 file)) | ||
| 336 | ;; Cluster name. | ||
| 337 | ((and (string-match "^/\\([^:/]+\\):\\([^:]*\\)$" file) | ||
| 338 | (shadow-get-cluster (match-string 1 file))) | ||
| 339 | (let ((file (match-string 2 file)) | ||
| 340 | (primary | ||
| 341 | (shadow-cluster-primary | ||
| 342 | (shadow-get-cluster (match-string 1 file))))) | ||
| 343 | (when (string-equal primary shadow-system-name) (setq primary nil)) | ||
| 344 | (shadow-local-file (concat primary file)))) | ||
| 345 | ;; Local name. | ||
| 346 | ((null (file-remote-p file)) file))) | ||
| 331 | 347 | ||
| 332 | (defun shadow-expand-cluster-in-file-name (file) | 348 | (defun shadow-expand-cluster-in-file-name (file) |
| 333 | "If hostname part of FILE is a cluster, expand it to cluster's primary hostname. | 349 | "If hostname part of FILE is a cluster, expand it to cluster's primary hostname. |
| 334 | Will return the name bare if it is a local file." | 350 | Will return the name bare if it is a local file." |
| 335 | (let ((hup (shadow-parse-name file))) | 351 | (when (stringp file) |
| 336 | (cond ((null hup) file) | 352 | (cond |
| 337 | ((shadow-local-file hup)) | 353 | ;; Local file. |
| 338 | ((shadow-make-fullname (shadow-site-primary (nth 0 hup)) | 354 | ((shadow-local-file file)) |
| 339 | (nth 1 hup) | 355 | ;; Cluster name. |
| 340 | (nth 2 hup)))))) | 356 | ((string-match "^\\(/[^:/]+:\\)[^:]*$" file) |
| 357 | (let ((primary | ||
| 358 | (save-match-data | ||
| 359 | (shadow-cluster-primary | ||
| 360 | (shadow-get-cluster | ||
| 361 | (shadow-site-name (match-string 1 file))))))) | ||
| 362 | (if (not primary) | ||
| 363 | file | ||
| 364 | (setq file (replace-match primary nil nil file 1)) | ||
| 365 | (or (shadow-local-file file) file)))) | ||
| 366 | (t file)))) | ||
| 341 | 367 | ||
| 342 | (defun shadow-expand-file-name (file &optional default) | 368 | (defun shadow-expand-file-name (file &optional default) |
| 343 | "Expand file name and get FILE's true name." | 369 | "Expand file name and get FILE's true name." |
| @@ -352,46 +378,50 @@ true." | |||
| 352 | (homedir (if (shadow-local-file hup) | 378 | (homedir (if (shadow-local-file hup) |
| 353 | shadow-homedir | 379 | shadow-homedir |
| 354 | (file-name-as-directory | 380 | (file-name-as-directory |
| 355 | (nth 2 (shadow-parse-fullname | 381 | (file-local-name |
| 356 | (expand-file-name | 382 | (expand-file-name (shadow-make-fullname hup nil "~")))))) |
| 357 | (shadow-make-fullname | 383 | (suffix (shadow-suffix homedir (tramp-file-name-localname hup))) |
| 358 | (nth 0 hup) (nth 1 hup) "~"))))))) | 384 | (cluster (shadow-site-cluster (shadow-make-fullname hup nil "")))) |
| 359 | (suffix (shadow-suffix homedir (nth 2 hup))) | 385 | (when cluster |
| 360 | (cluster (shadow-site-cluster (nth 0 hup)))) | 386 | (setf (tramp-file-name-method hup) nil |
| 387 | (tramp-file-name-host hup) (shadow-cluster-name cluster))) | ||
| 361 | (shadow-make-fullname | 388 | (shadow-make-fullname |
| 362 | (if cluster | 389 | hup nil |
| 363 | (shadow-cluster-name cluster) | ||
| 364 | (nth 0 hup)) | ||
| 365 | (nth 1 hup) | ||
| 366 | (if suffix | 390 | (if suffix |
| 367 | (concat "~/" suffix) | 391 | (concat "~/" suffix) |
| 368 | (nth 2 hup))))) | 392 | (tramp-file-name-localname hup))))) |
| 369 | 393 | ||
| 370 | (defun shadow-same-site (pattern file) | 394 | (defun shadow-same-site (pattern file) |
| 371 | "True if the site of PATTERN and of FILE are on the same site. | 395 | "True if the site of PATTERN and of FILE are on the same site. |
| 372 | If usernames are supplied, they must also match exactly. PATTERN and FILE may | 396 | PATTERN and FILE may be Tramp vectors, or remote file names. |
| 373 | be lists of host, user, name, or ange-ftp file names. FILE may also be just a | 397 | FILE may also be just a local filename." |
| 374 | local filename." | 398 | (let ((pattern-sup (shadow-parse-name pattern)) |
| 375 | (let ((pattern-sup (shadow-parse-fullname pattern)) | ||
| 376 | (file-sup (shadow-parse-name file))) | 399 | (file-sup (shadow-parse-name file))) |
| 377 | (and | 400 | (and |
| 378 | (shadow-site-match (nth 0 pattern-sup) (nth 0 file-sup)) | 401 | (shadow-site-match |
| 379 | (or (null (nth 1 pattern-sup)) | 402 | (tramp-file-name-host pattern-sup) (tramp-file-name-host file-sup)) |
| 380 | (string-equal (nth 1 pattern-sup) (nth 1 file-sup)))))) | 403 | (or (null (tramp-file-name-user pattern-sup)) |
| 404 | (string-equal | ||
| 405 | (tramp-file-name-user pattern-sup) | ||
| 406 | (tramp-file-name-user file-sup)))))) | ||
| 381 | 407 | ||
| 382 | (defun shadow-file-match (pattern file &optional regexp) | 408 | (defun shadow-file-match (pattern file &optional regexp) |
| 383 | "Return t if PATTERN matches FILE. | 409 | "Return t if PATTERN matches FILE. |
| 384 | If REGEXP is supplied and non-nil, the file part of the pattern is a regular | 410 | If REGEXP is supplied and non-nil, the file part of the pattern is a regular |
| 385 | expression, otherwise it must match exactly. The sites and usernames must | 411 | expression, otherwise it must match exactly. The sites must |
| 386 | match---see `shadow-same-site'. The pattern must be in full ange-ftp format, | 412 | match---see `shadow-same-site'. The pattern must be in full Tramp format, |
| 387 | but the file can be any valid filename. This function does not do any | 413 | but the file can be any valid filename. This function does not do any |
| 388 | filename expansion or contraction, you must do that yourself first." | 414 | filename expansion or contraction, you must do that yourself first." |
| 389 | (let* ((pattern-sup (shadow-parse-fullname pattern)) | 415 | (let* ((pattern-sup (shadow-parse-name pattern)) |
| 390 | (file-sup (shadow-parse-name file))) | 416 | (file-sup (shadow-parse-name file))) |
| 391 | (and (shadow-same-site pattern-sup file-sup) | 417 | (and (shadow-same-site pattern-sup file-sup) |
| 392 | (if regexp | 418 | (if regexp |
| 393 | (string-match (nth 2 pattern-sup) (nth 2 file-sup)) | 419 | (string-match |
| 394 | (string-equal (nth 2 pattern-sup) (nth 2 file-sup)))))) | 420 | (tramp-file-name-localname pattern-sup) |
| 421 | (tramp-file-name-localname file-sup)) | ||
| 422 | (string-equal | ||
| 423 | (tramp-file-name-localname pattern-sup) | ||
| 424 | (tramp-file-name-localname file-sup)))))) | ||
| 395 | 425 | ||
| 396 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | 426 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; |
| 397 | ;;; User-level Commands | 427 | ;;; User-level Commands |
| @@ -405,30 +435,34 @@ one of them is sufficient to update the file on all of them. Clusters are | |||
| 405 | defined by a name, the network address of a primary host (the one we copy | 435 | defined by a name, the network address of a primary host (the one we copy |
| 406 | files to), and a regular expression that matches the hostnames of all the | 436 | files to), and a regular expression that matches the hostnames of all the |
| 407 | sites in the cluster." | 437 | sites in the cluster." |
| 408 | (interactive (list (completing-read "Cluster name: " shadow-clusters () ()))) | 438 | (interactive (list (completing-read "Cluster name: " shadow-clusters))) |
| 409 | (let* ((old (shadow-get-cluster name)) | 439 | (let* ((old (shadow-get-cluster name)) |
| 410 | (primary (read-string "Primary host: " | 440 | (primary (let (try-primary) |
| 411 | (if old (shadow-cluster-primary old) | 441 | (while (not |
| 412 | name))) | 442 | (or |
| 413 | (regexp (let (try-regexp) | 443 | (string-equal |
| 414 | (while (not | 444 | (setq try-primary |
| 415 | (string-match | ||
| 416 | (setq try-regexp | ||
| 417 | (read-string | 445 | (read-string |
| 418 | "Regexp matching all host names: " | 446 | "Primary host: " |
| 419 | (if old (shadow-cluster-regexp old) | 447 | (if old (shadow-cluster-primary old) |
| 420 | (shadow-regexp-superquote primary)))) | 448 | name))) |
| 421 | primary)) | 449 | shadow-system-name) |
| 422 | (message "Regexp doesn't include the primary host!") | 450 | (file-remote-p try-primary))) |
| 423 | (sit-for 2)) | 451 | (message "Not a valid primary!") |
| 424 | try-regexp)) | 452 | (sit-for 2)) |
| 425 | ; (username (read-no-blanks-input | 453 | try-primary)) |
| 426 | ; (format "Username (default %s): " | 454 | (regexp (let (try-regexp) |
| 427 | ; (shadow-get-user primary)) | 455 | (while (not |
| 428 | ; (if old (or (shadow-cluster-username old) "") | 456 | (string-match |
| 429 | ; (user-login-name)))) | 457 | (setq try-regexp |
| 430 | ) | 458 | (read-string |
| 431 | ; (if (string-equal "" username) (setq username nil)) | 459 | "Regexp matching all host names: " |
| 460 | (if old (shadow-cluster-regexp old) | ||
| 461 | (shadow-regexp-superquote primary)))) | ||
| 462 | primary)) | ||
| 463 | (message "Regexp doesn't include the primary host!") | ||
| 464 | (sit-for 2)) | ||
| 465 | try-regexp))) | ||
| 432 | (shadow-set-cluster name primary regexp))) | 466 | (shadow-set-cluster name primary regexp))) |
| 433 | 467 | ||
| 434 | ;;;###autoload | 468 | ;;;###autoload |
| @@ -438,20 +472,14 @@ It may have different filenames on each site. When this file is edited, the | |||
| 438 | new version will be copied to each of the other locations. Sites can be | 472 | new version will be copied to each of the other locations. Sites can be |
| 439 | specific hostnames, or names of clusters (see `shadow-define-cluster')." | 473 | specific hostnames, or names of clusters (see `shadow-define-cluster')." |
| 440 | (interactive) | 474 | (interactive) |
| 441 | (let* ((hup (shadow-parse-fullname | 475 | (let* ((hup (shadow-parse-name |
| 442 | (shadow-contract-file-name (buffer-file-name)))) | 476 | (shadow-contract-file-name (buffer-file-name)))) |
| 443 | (name (nth 2 hup)) | 477 | (name (tramp-file-name-localname hup)) |
| 444 | user site group) | 478 | site group) |
| 445 | (while (setq site (shadow-read-site)) | 479 | (while (setq site (shadow-read-site)) |
| 446 | (setq user (read-string (format "Username (default %s): " | 480 | (setq name (read-string "Filename: " name) |
| 447 | (shadow-get-user site))) | 481 | hup (shadow-parse-name (shadow-contract-file-name name)) |
| 448 | name (read-string "Filename: " name)) | 482 | group (cons (shadow-make-fullname hup site) group))) |
| 449 | (setq group (cons (shadow-make-fullname site | ||
| 450 | (if (string-equal "" user) | ||
| 451 | (shadow-get-user site) | ||
| 452 | user) | ||
| 453 | name) | ||
| 454 | group))) | ||
| 455 | (setq shadow-literal-groups (cons group shadow-literal-groups))) | 483 | (setq shadow-literal-groups (cons group shadow-literal-groups))) |
| 456 | (shadow-write-info-file)) | 484 | (shadow-write-info-file)) |
| 457 | 485 | ||
| @@ -468,19 +496,12 @@ function). Each site can be either a hostname or the name of a cluster (see | |||
| 468 | "Filename regexp: " | 496 | "Filename regexp: " |
| 469 | (if (buffer-file-name) | 497 | (if (buffer-file-name) |
| 470 | (shadow-regexp-superquote | 498 | (shadow-regexp-superquote |
| 471 | (nth 2 | 499 | (file-local-name (buffer-file-name)))))) |
| 472 | (shadow-parse-name | 500 | site sites) |
| 473 | (shadow-contract-file-name | ||
| 474 | (buffer-file-name)))))))) | ||
| 475 | site sites usernames) | ||
| 476 | (while (setq site (shadow-read-site)) | 501 | (while (setq site (shadow-read-site)) |
| 477 | (setq sites (cons site sites)) | 502 | (setq sites (cons site sites))) |
| 478 | (setq usernames | ||
| 479 | (cons (read-string (format "Username for %s: " site) | ||
| 480 | (shadow-get-user site)) | ||
| 481 | usernames))) | ||
| 482 | (setq shadow-regexp-groups | 503 | (setq shadow-regexp-groups |
| 483 | (cons (shadow-make-group regexp sites usernames) | 504 | (cons (shadow-make-group regexp sites) |
| 484 | shadow-regexp-groups)) | 505 | shadow-regexp-groups)) |
| 485 | (shadow-write-info-file))) | 506 | (shadow-write-info-file))) |
| 486 | 507 | ||
| @@ -537,14 +558,14 @@ permanently, remove the group from `shadow-literal-groups' or | |||
| 537 | ;;; Internal functions | 558 | ;;; Internal functions |
| 538 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | 559 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; |
| 539 | 560 | ||
| 540 | (defun shadow-make-group (regexp sites usernames) | 561 | (defun shadow-make-group (regexp sites) |
| 541 | "Make a description of a file group--- | 562 | "Make a description of a file group--- |
| 542 | actually a list of regexp ange-ftp file names---from REGEXP (name of file to | 563 | actually a list of regexp Tramp file names---from REGEXP (name of file to |
| 543 | be shadowed), list of SITES, and corresponding list of USERNAMES for each | 564 | be shadowed), and list of SITES" |
| 544 | site." | ||
| 545 | (if sites | 565 | (if sites |
| 546 | (cons (shadow-make-fullname (car sites) (car usernames) regexp) | 566 | (cons (shadow-make-fullname |
| 547 | (shadow-make-group regexp (cdr sites) (cdr usernames))) | 567 | (shadow-parse-name (shadow-site-primary (car sites))) nil regexp) |
| 568 | (shadow-make-group regexp (cdr sites))) | ||
| 548 | nil)) | 569 | nil)) |
| 549 | 570 | ||
| 550 | (defun shadow-copy-file (s) | 571 | (defun shadow-copy-file (s) |
| @@ -601,7 +622,9 @@ Consider them as regular expressions if third arg REGEXP is true." | |||
| 601 | (car groups)))) | 622 | (car groups)))) |
| 602 | (append (cond ((equal nonmatching (car groups)) nil) | 623 | (append (cond ((equal nonmatching (car groups)) nil) |
| 603 | (regexp | 624 | (regexp |
| 604 | (let ((realname (nth 2 (shadow-parse-fullname file)))) | 625 | (let ((realname |
| 626 | (tramp-file-name-localname | ||
| 627 | (shadow-parse-name file)))) | ||
| 605 | (mapcar | 628 | (mapcar |
| 606 | (function | 629 | (function |
| 607 | (lambda (x) | 630 | (lambda (x) |
| @@ -636,9 +659,8 @@ PAIR must be `eq' to one of the elements of that list." | |||
| 636 | Thus restores shadowfile's state from your last Emacs session. | 659 | Thus restores shadowfile's state from your last Emacs session. |
| 637 | Return t unless files were locked; then return nil." | 660 | Return t unless files were locked; then return nil." |
| 638 | (interactive) | 661 | (interactive) |
| 639 | (if (and (fboundp 'file-locked-p) | 662 | (if (or (stringp (file-locked-p shadow-info-file)) |
| 640 | (or (stringp (file-locked-p shadow-info-file)) | 663 | (stringp (file-locked-p shadow-todo-file))) |
| 641 | (stringp (file-locked-p shadow-todo-file)))) | ||
| 642 | (progn | 664 | (progn |
| 643 | (message "Shadowfile is running in another Emacs; can't have two.") | 665 | (message "Shadowfile is running in another Emacs; can't have two.") |
| 644 | (beep) | 666 | (beep) |
| @@ -647,7 +669,7 @@ Return t unless files were locked; then return nil." | |||
| 647 | (save-current-buffer | 669 | (save-current-buffer |
| 648 | (when shadow-info-file | 670 | (when shadow-info-file |
| 649 | (set-buffer (setq shadow-info-buffer | 671 | (set-buffer (setq shadow-info-buffer |
| 650 | (find-file-noselect shadow-info-file))) | 672 | (find-file-noselect shadow-info-file 'nowarn))) |
| 651 | (when (and (not (buffer-modified-p)) | 673 | (when (and (not (buffer-modified-p)) |
| 652 | (file-newer-than-file-p (make-auto-save-file-name) | 674 | (file-newer-than-file-p (make-auto-save-file-name) |
| 653 | shadow-info-file)) | 675 | shadow-info-file)) |
| @@ -680,6 +702,7 @@ defined, the old hashtable info is invalid." | |||
| 680 | (if (not shadow-info-buffer) | 702 | (if (not shadow-info-buffer) |
| 681 | (setq shadow-info-buffer (find-file-noselect shadow-info-file))) | 703 | (setq shadow-info-buffer (find-file-noselect shadow-info-file))) |
| 682 | (set-buffer shadow-info-buffer) | 704 | (set-buffer shadow-info-buffer) |
| 705 | (setq buffer-read-only nil) | ||
| 683 | (delete-region (point-min) (point-max)) | 706 | (delete-region (point-min) (point-max)) |
| 684 | (shadow-insert-var 'shadow-clusters) | 707 | (shadow-insert-var 'shadow-clusters) |
| 685 | (shadow-insert-var 'shadow-literal-groups) | 708 | (shadow-insert-var 'shadow-literal-groups) |
| @@ -692,6 +715,7 @@ With non-nil argument also saves the buffer." | |||
| 692 | (if (not shadow-todo-buffer) | 715 | (if (not shadow-todo-buffer) |
| 693 | (setq shadow-todo-buffer (find-file-noselect shadow-todo-file))) | 716 | (setq shadow-todo-buffer (find-file-noselect shadow-todo-file))) |
| 694 | (set-buffer shadow-todo-buffer) | 717 | (set-buffer shadow-todo-buffer) |
| 718 | (setq buffer-read-only nil) | ||
| 695 | (delete-region (point-min) (point-max)) | 719 | (delete-region (point-min) (point-max)) |
| 696 | (shadow-insert-var 'shadow-files-to-copy) | 720 | (shadow-insert-var 'shadow-files-to-copy) |
| 697 | (if save (shadow-save-todo-file)))) | 721 | (if save (shadow-save-todo-file)))) |
| @@ -765,24 +789,6 @@ look for files that have been changed and need to be copied to other systems." | |||
| 765 | (kill-emacs))) | 789 | (kill-emacs))) |
| 766 | 790 | ||
| 767 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | 791 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; |
| 768 | ;;; Lucid Emacs compatibility | ||
| 769 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||
| 770 | |||
| 771 | ;; This is on hold until someone tells me about a working version of | ||
| 772 | ;; map-ynp for Lucid Emacs. | ||
| 773 | |||
| 774 | ;(when (string-match "Lucid" emacs-version) | ||
| 775 | ; (require 'symlink-fix) | ||
| 776 | ; (require 'ange-ftp) | ||
| 777 | ; (require 'map-ynp) | ||
| 778 | ; (if (not (fboundp 'file-truename)) | ||
| 779 | ; (fset 'shadow-expand-file-name | ||
| 780 | ; (symbol-function 'symlink-expand-file-name))) | ||
| 781 | ; (if (not (fboundp 'ange-ftp-ftp-name)) | ||
| 782 | ; (fset 'ange-ftp-ftp-name | ||
| 783 | ; (symbol-function 'ange-ftp-ftp-name)))) | ||
| 784 | |||
| 785 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||
| 786 | ;;; Hook us up | 792 | ;;; Hook us up |
| 787 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | 793 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; |
| 788 | 794 | ||
| @@ -790,18 +796,10 @@ look for files that have been changed and need to be copied to other systems." | |||
| 790 | (defun shadow-initialize () | 796 | (defun shadow-initialize () |
| 791 | "Set up file shadowing." | 797 | "Set up file shadowing." |
| 792 | (interactive) | 798 | (interactive) |
| 793 | (if (null shadow-homedir) | 799 | (setq shadow-homedir |
| 794 | (setq shadow-homedir | 800 | (file-name-as-directory (shadow-expand-file-name shadow-homedir)) |
| 795 | (file-name-as-directory (shadow-expand-file-name "~")))) | 801 | shadow-info-file (shadow-expand-file-name shadow-info-file) |
| 796 | (if (null shadow-info-file) | 802 | shadow-todo-file (shadow-expand-file-name shadow-todo-file)) |
| 797 | (setq shadow-info-file | ||
| 798 | ;; FIXME: Move defaults to their defcustom. | ||
| 799 | (shadow-expand-file-name | ||
| 800 | (locate-user-emacs-file "shadows" ".shadows")))) | ||
| 801 | (if (null shadow-todo-file) | ||
| 802 | (setq shadow-todo-file | ||
| 803 | (shadow-expand-file-name | ||
| 804 | (locate-user-emacs-file "shadow_todo" ".shadow_todo")))) | ||
| 805 | (if (not (shadow-read-files)) | 803 | (if (not (shadow-read-files)) |
| 806 | (progn | 804 | (progn |
| 807 | (message "Shadowfile information files not found - aborting") | 805 | (message "Shadowfile information files not found - aborting") |