Merge from emacs-24; up to 2013-01-03T02:37:57Z!rgm@gnu.org
[bpt/emacs.git] / lisp / shadowfile.el
CommitLineData
36fd8e17 1;;; shadowfile.el --- automatic file copying
bb5d4e1a 2
ab422c4d 3;; Copyright (C) 1993-1994, 2001-2013 Free Software Foundation, Inc.
bb5d4e1a 4
5762abec 5;; Author: Boris Goldowsky <boris@gnu.org>
36fd8e17 6;; Keywords: comm files
be010748
RS
7
8;; This file is part of GNU Emacs.
9
eb3fa2cf 10;; GNU Emacs is free software: you can redistribute it and/or modify
be010748 11;; it under the terms of the GNU General Public License as published by
eb3fa2cf
GM
12;; the Free Software Foundation, either version 3 of the License, or
13;; (at your option) any later version.
be010748
RS
14
15;; GNU Emacs is distributed in the hope that it will be useful,
16;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18;; GNU General Public License for more details.
19
20;; You should have received a copy of the GNU General Public License
eb3fa2cf 21;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
bb5d4e1a 22
36fd8e17 23;;; Commentary:
bb5d4e1a 24
b578f267
EN
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
27;; whether it is on the list of files with "shadows", and if so, it tries to
32f389a4 28;; copy it when you exit Emacs (or use the shadow-copy-files command).
bb5d4e1a 29
b578f267
EN
30;; Installation & Use:
31
36fd8e17 32;; Add clusters (if necessary) and file groups with shadow-define-cluster,
b578f267 33;; shadow-define-literal-group, and shadow-define-regexp-group (see the
36fd8e17
DL
34;; documentation for these functions for information on how and when to use
35;; them). After doing this once, everything should be automatic.
b578f267
EN
36
37;; The lists of clusters and shadows are saved in a file called .shadows,
32f389a4
JB
38;; so that they can be remembered from one Emacs session to another, even
39;; (as much as possible) if the Emacs session terminates abnormally. The
b578f267
EN
40;; files needing to be copied are stored in .shadow_todo; if a file cannot
41;; be copied for any reason, it will stay on the list to be tried again
42;; next time. The .shadows file should itself have shadows on all your
43;; accounts so that the information in it is consistent everywhere, but
44;; .shadow_todo is local information and should have no shadows.
45
46;; If you do not want to copy a particular file, you can answer "no" and
32f389a4 47;; be asked again next time you hit C-x 4 s or exit Emacs. If you do not
b578f267
EN
48;; want to be asked again, use shadow-cancel, and you will not be asked
49;; until you change the file and save it again. If you do not want to
50;; shadow that file ever again, you can edit it out of the .shadows
51;; buffer. Anytime you edit the .shadows buffer, you must type M-x
52;; shadow-read-files to load in the new information, or your changes will
53;; be overwritten!
54
55;; Bugs & Warnings:
56;;
57;; - It is bad to have two emacses both running shadowfile at the same
58;; time. It tries to detect this condition, but is not always successful.
59;;
60;; - You have to be careful not to edit a file in two locations
61;; before shadowfile has had a chance to copy it; otherwise
62;; "updating shadows" will overwrite one of the changed versions.
63;;
64;; - It ought to check modification times of both files to make sure
65;; it is doing the right thing. This will have to wait until
66;; file-newer-than-file-p works between machines.
67;;
68;; - It will not make directories for you, it just fails to copy files
69;; that belong in non-existent directories.
70;;
5762abec 71;; Please report any bugs to me (boris@gnu.org). Also let me know
b578f267 72;; if you have suggestions or would like to be informed of updates.
bb5d4e1a 73
36fd8e17 74
bb5d4e1a
RS
75;;; Code:
76
539a920c 77(require 'cl-lib)
bb5d4e1a
RS
78(require 'ange-ftp)
79
bb5d4e1a
RS
80;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
81;;; Variables
82;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
83
4bef9110
SE
84(defgroup shadow nil
85 "Automatic file copying when saving a file."
86 :prefix "shadow-"
36fd8e17 87 :link '(emacs-commentary-link "shadowfile")
4bef9110
SE
88 :group 'files)
89
90(defcustom shadow-noquery nil
9201cc28 91 "If t, always copy shadow files without asking.
06b60517 92If nil (the default), always ask. If not nil and not t, ask only if there
4bef9110 93is no buffer currently visiting the file."
a62e9680 94 :type '(choice (const t) (const nil) (other :tag "Ask if no buffer" maybe))
4bef9110 95 :group 'shadow)
bb5d4e1a 96
4bef9110 97(defcustom shadow-inhibit-message nil
9201cc28 98 "If non-nil, do not display a message when a file needs copying."
4bef9110
SE
99 :type 'boolean
100 :group 'shadow)
bb5d4e1a 101
4bef9110 102(defcustom shadow-inhibit-overload nil
7ad8d84e 103 "If non-nil, shadowfile won't redefine \\[save-buffers-kill-emacs].
2fc88dcc
JB
104Normally it overloads the function `save-buffers-kill-emacs' to check for
105files that have been changed and need to be copied to other systems."
4bef9110
SE
106 :type 'boolean
107 :group 'shadow)
bb5d4e1a 108
4bef9110 109(defcustom shadow-info-file nil
36fd8e17
DL
110 "File to keep shadow information in.
111The `shadow-info-file' should be shadowed to all your accounts to
4bef9110
SE
112ensure consistency. Default: ~/.shadows"
113 :type '(choice (const nil) file)
114 :group 'shadow)
bb5d4e1a 115
4bef9110 116(defcustom shadow-todo-file nil
bb5d4e1a
RS
117 "File to store the list of uncopied shadows in.
118This means that if a remote system is down, or for any reason you cannot or
36fd8e17
DL
119decide not to copy your shadow files at the end of one Emacs session, it will
120remember and ask you again in your next Emacs session.
bb5d4e1a 121This file must NOT be shadowed to any other system, it is host-specific.
4bef9110
SE
122Default: ~/.shadow_todo"
123 :type '(choice (const nil) file)
124 :group 'shadow)
125
bb5d4e1a
RS
126
127;;; The following two variables should in most cases initialize themselves
128;;; correctly. They are provided as variables in case the defaults are wrong
06b60517 129;;; on your machine (and for efficiency).
bb5d4e1a
RS
130
131(defvar shadow-system-name (system-name)
132 "The complete hostname of this machine.")
133
134(defvar shadow-homedir nil
135 "Your home directory on this machine.")
136
137;;;
138;;; Internal variables whose values are stored in the info and todo files:
139;;;
140
141(defvar shadow-clusters nil
06b60517 142 "List of host clusters (see `shadow-define-cluster').")
bb5d4e1a
RS
143
144(defvar shadow-literal-groups nil
145 "List of files that are shared between hosts.
146This list contains shadow structures with literal filenames, created by
7ad8d84e 147`shadow-define-literal-group'.")
bb5d4e1a
RS
148
149(defvar shadow-regexp-groups nil
150 "List of file types that are shared between hosts.
36fd8e17
DL
151This list contains shadow structures with regexps matching filenames,
152created by `shadow-define-regexp-group'.")
bb5d4e1a
RS
153
154;;;
155;;; Other internal variables:
156;;;
157
158(defvar shadow-files-to-copy nil) ; List of files that need to
159 ; be copied to remote hosts.
160
161(defvar shadow-hashtable nil) ; for speed
162
163(defvar shadow-info-buffer nil) ; buf visiting shadow-info-file
164(defvar shadow-todo-buffer nil) ; buf visiting shadow-todo-file
165
166;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
167;;; Syntactic sugar; General list and string manipulation
168;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
169
bb5d4e1a 170(defun shadow-union (a b)
36fd8e17 171 "Add members of list A to list B if not equal to items already in B."
bb5d4e1a
RS
172 (if (null a)
173 b
174 (if (member (car a) b)
175 (shadow-union (cdr a) b)
176 (shadow-union (cdr a) (cons (car a) b)))))
177
178(defun shadow-find (func list)
7ad8d84e 179 "If FUNC applied to some element of LIST is non-nil, return first such element."
bb5d4e1a
RS
180 (while (and list (not (funcall func (car list))))
181 (setq list (cdr list)))
182 (car list))
183
bb5d4e1a 184(defun shadow-regexp-superquote (string)
36fd8e17
DL
185 "Like `regexp-quote', but includes the ^ and $.
186This makes sure regexp matches nothing but STRING."
bb5d4e1a
RS
187 (concat "^" (regexp-quote string) "$"))
188
189(defun shadow-suffix (prefix string)
190 "If PREFIX begins STRING, return the rest.
7ad8d84e 191Return value is non-nil if PREFIX and STRING are `string=' up to the length of
bb5d4e1a
RS
192PREFIX."
193 (let ((lp (length prefix))
194 (ls (length string)))
195 (if (and (>= ls lp)
196 (string= prefix (substring string 0 lp)))
197 (substring string lp))))
198
199;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
200;;; Clusters and sites
201;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
202
203;;; I use the term `site' to refer to a string which may be the name of a
204;;; cluster or a literal hostname. All user-level commands should accept
205;;; either.
206
207(defun shadow-make-cluster (name primary regexp)
36fd8e17
DL
208 "Create a shadow cluster.
209It is called NAME, uses the PRIMARY hostname and REGEXP matching all
210hosts in the cluster. The variable `shadow-clusters' associates the
211names of clusters to these structures. This function is for program
212use: to create clusters interactively, use `shadow-define-cluster'
213instead."
bb5d4e1a
RS
214 (list name primary regexp))
215
216(defmacro shadow-cluster-name (cluster)
217 "Return the name of the CLUSTER."
218 (list 'elt cluster 0))
219
220(defmacro shadow-cluster-primary (cluster)
221 "Return the primary hostname of a CLUSTER."
222 (list 'elt cluster 1))
223
224(defmacro shadow-cluster-regexp (cluster)
225 "Return the regexp matching hosts in a CLUSTER."
226 (list 'elt cluster 2))
227
228(defun shadow-set-cluster (name primary regexp)
36fd8e17
DL
229 "Put cluster NAME on the list of clusters.
230Replace old definition, if any. PRIMARY and REGEXP are the
bb5d4e1a 231information defining the cluster. For interactive use, call
36fd8e17 232`shadow-define-cluster' instead."
539a920c
GM
233 (let ((rest (cl-remove-if (lambda (x) (equal name (car x)))
234 shadow-clusters)))
36fd8e17 235 (setq shadow-clusters
bb5d4e1a
RS
236 (cons (shadow-make-cluster name primary regexp)
237 rest))))
238
239(defmacro shadow-get-cluster (name)
240 "Return cluster named NAME, or nil."
241 (list 'assoc name 'shadow-clusters))
242
243(defun shadow-site-primary (site)
244 "If SITE is a cluster, return primary host, otherwise return SITE."
245 (let ((c (shadow-get-cluster site)))
246 (if c
247 (shadow-cluster-primary c)
248 site)))
249
250;;; SITES
251
252(defun shadow-site-cluster (site)
06b60517 253 "Given a SITE (hostname or cluster name), return cluster it is in, or nil."
bb5d4e1a
RS
254 (or (assoc site shadow-clusters)
255 (shadow-find
256 (function (lambda (x)
257 (string-match (shadow-cluster-regexp x)
258 site)))
259 shadow-clusters)))
260
261(defun shadow-read-site ()
262 "Read a cluster name or hostname from the minibuffer."
263 (let ((ans (completing-read "Host or cluster name [RET when done]: "
264 shadow-clusters)))
265 (if (equal "" ans)
266 nil
267 ans)))
268
269(defun shadow-site-match (site1 site2)
4837b516 270 "Non-nil if SITE1 is or includes SITE2.
7ad8d84e
JB
271Each may be a host or cluster name; if they are clusters, regexp of SITE1 will
272be matched against the primary of SITE2."
bb5d4e1a
RS
273 (or (string-equal site1 site2) ; quick check
274 (let* ((cluster1 (shadow-get-cluster site1))
275 (primary2 (shadow-site-primary site2)))
276 (if cluster1
277 (string-match (shadow-cluster-regexp cluster1) primary2)
278 (string-equal site1 primary2)))))
279
280(defun shadow-get-user (site)
36fd8e17 281 "Return the default username for a SITE."
bb5d4e1a
RS
282 (ange-ftp-get-user (shadow-site-primary site)))
283
284;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
285;;; Filename manipulation
286;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
287
5354cb6d 288(defun shadow-parse-fullname (fullname)
06b60517 289 "Parse FULLNAME into (site user path) list.
2fc88dcc 290Leave it alone if it already is one. Return nil if the argument is
36fd8e17 291not a full ange-ftp pathname."
5354cb6d
RS
292 (if (listp fullname)
293 fullname
294 (ange-ftp-ftp-name fullname)))
295
296(defun shadow-parse-name (name)
06b60517 297 "Parse any NAME into (site user name) list.
5354cb6d
RS
298Argument can be a simple name, full ange-ftp name, or already a hup list."
299 (or (shadow-parse-fullname name)
bb5d4e1a
RS
300 (list shadow-system-name
301 (user-login-name)
5354cb6d 302 name)))
bb5d4e1a 303
5354cb6d
RS
304(defsubst shadow-make-fullname (host user name)
305 "Make an ange-ftp style fullname out of HOST, USER (optional), and NAME.
bb5d4e1a 306This is probably not as general as it ought to be."
36fd8e17 307 (concat "/"
bb5d4e1a
RS
308 (if user (concat user "@"))
309 host ":"
5354cb6d 310 name))
bb5d4e1a 311
5354cb6d
RS
312(defun shadow-replace-name-component (fullname newname)
313 "Return FULLNAME with the name component changed to NEWNAME."
314 (let ((hup (shadow-parse-fullname fullname)))
315 (shadow-make-fullname (nth 0 hup) (nth 1 hup) newname)))
bb5d4e1a
RS
316
317(defun shadow-local-file (file)
36fd8e17
DL
318 "If FILE is at this site, remove /user@host part.
319If refers to a different system or a different user on this system,
320return nil."
5354cb6d 321 (let ((hup (shadow-parse-fullname file)))
bb5d4e1a
RS
322 (cond ((null hup) file)
323 ((and (shadow-site-match (nth 0 hup) shadow-system-name)
324 (string-equal (nth 1 hup) (user-login-name)))
325 (nth 2 hup))
326 (t nil))))
327
328(defun shadow-expand-cluster-in-file-name (file)
36fd8e17 329 "If hostname part of FILE is a cluster, expand it to cluster's primary hostname.
5354cb6d 330Will return the name bare if it is a local file."
06b60517 331 (let ((hup (shadow-parse-name file)))
bb5d4e1a
RS
332 (cond ((null hup) file)
333 ((shadow-local-file hup))
5354cb6d 334 ((shadow-make-fullname (shadow-site-primary (nth 0 hup))
bb5d4e1a
RS
335 (nth 1 hup)
336 (nth 2 hup))))))
337
338(defun shadow-expand-file-name (file &optional default)
7ad8d84e 339 "Expand file name and get FILE's true name."
bb5d4e1a
RS
340 (file-truename (expand-file-name file default)))
341
342(defun shadow-contract-file-name (file)
36fd8e17
DL
343 "Simplify FILE.
344Do so by replacing (when possible) home directory with ~, and hostname
345with cluster name that includes it. Filename should be absolute and
346true."
5354cb6d 347 (let* ((hup (shadow-parse-name file))
bb5d4e1a
RS
348 (homedir (if (shadow-local-file hup)
349 shadow-homedir
350 (file-name-as-directory
5354cb6d 351 (nth 2 (shadow-parse-fullname
bb5d4e1a 352 (expand-file-name
5354cb6d 353 (shadow-make-fullname
bb5d4e1a
RS
354 (nth 0 hup) (nth 1 hup) "~")))))))
355 (suffix (shadow-suffix homedir (nth 2 hup)))
356 (cluster (shadow-site-cluster (nth 0 hup))))
5354cb6d 357 (shadow-make-fullname
bb5d4e1a
RS
358 (if cluster
359 (shadow-cluster-name cluster)
360 (nth 0 hup))
361 (nth 1 hup)
36fd8e17 362 (if suffix
bb5d4e1a
RS
363 (concat "~/" suffix)
364 (nth 2 hup)))))
365
366(defun shadow-same-site (pattern file)
367 "True if the site of PATTERN and of FILE are on the same site.
368If usernames are supplied, they must also match exactly. PATTERN and FILE may
5354cb6d 369be lists of host, user, name, or ange-ftp file names. FILE may also be just a
bb5d4e1a 370local filename."
5354cb6d
RS
371 (let ((pattern-sup (shadow-parse-fullname pattern))
372 (file-sup (shadow-parse-name file)))
bb5d4e1a
RS
373 (and
374 (shadow-site-match (nth 0 pattern-sup) (nth 0 file-sup))
375 (or (null (nth 1 pattern-sup))
376 (string-equal (nth 1 pattern-sup) (nth 1 file-sup))))))
377
378(defun shadow-file-match (pattern file &optional regexp)
36fd8e17 379 "Return t if PATTERN matches FILE.
5354cb6d 380If REGEXP is supplied and non-nil, the file part of the pattern is a regular
bb5d4e1a 381expression, otherwise it must match exactly. The sites and usernames must
2fc88dcc
JB
382match---see `shadow-same-site'. The pattern must be in full ange-ftp format,
383but the file can be any valid filename. This function does not do any
384filename expansion or contraction, you must do that yourself first."
5354cb6d
RS
385 (let* ((pattern-sup (shadow-parse-fullname pattern))
386 (file-sup (shadow-parse-name file)))
bb5d4e1a 387 (and (shadow-same-site pattern-sup file-sup)
36fd8e17 388 (if regexp
bb5d4e1a
RS
389 (string-match (nth 2 pattern-sup) (nth 2 file-sup))
390 (string-equal (nth 2 pattern-sup) (nth 2 file-sup))))))
f1180544 391
bb5d4e1a
RS
392;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
393;;; User-level Commands
394;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
395
36fd8e17 396;;;###autoload
bb5d4e1a 397(defun shadow-define-cluster (name)
06b60517 398 "Edit (or create) the definition of a cluster NAME.
bb5d4e1a
RS
399This is a group of hosts that share directories, so that copying to or from
400one of them is sufficient to update the file on all of them. Clusters are
06b60517 401defined by a name, the network address of a primary host (the one we copy
2fc88dcc
JB
402files to), and a regular expression that matches the hostnames of all the
403sites in the cluster."
bb5d4e1a
RS
404 (interactive (list (completing-read "Cluster name: " shadow-clusters () ())))
405 (let* ((old (shadow-get-cluster name))
406 (primary (read-string "Primary host: "
36fd8e17 407 (if old (shadow-cluster-primary old)
bb5d4e1a
RS
408 name)))
409 (regexp (let (try-regexp)
410 (while (not
36fd8e17 411 (string-match
bb5d4e1a 412 (setq try-regexp
36fd8e17 413 (read-string
bb5d4e1a
RS
414 "Regexp matching all host names: "
415 (if old (shadow-cluster-regexp old)
416 (shadow-regexp-superquote primary))))
417 primary))
418 (message "Regexp doesn't include the primary host!")
419 (sit-for 2))
420 try-regexp))
36fd8e17 421; (username (read-no-blanks-input
5b76833f 422; (format "Username (default %s): "
bb5d4e1a
RS
423; (shadow-get-user primary))
424; (if old (or (shadow-cluster-username old) "")
425; (user-login-name))))
426 )
427; (if (string-equal "" username) (setq username nil))
428 (shadow-set-cluster name primary regexp)))
429
36fd8e17 430;;;###autoload
bb5d4e1a
RS
431(defun shadow-define-literal-group ()
432 "Declare a single file to be shared between sites.
433It may have different filenames on each site. When this file is edited, the
434new version will be copied to each of the other locations. Sites can be
06b60517 435specific hostnames, or names of clusters (see `shadow-define-cluster')."
bb5d4e1a 436 (interactive)
5354cb6d 437 (let* ((hup (shadow-parse-fullname
bb5d4e1a 438 (shadow-contract-file-name (buffer-file-name))))
5354cb6d 439 (name (nth 2 hup))
bb5d4e1a
RS
440 user site group)
441 (while (setq site (shadow-read-site))
5b76833f 442 (setq user (read-string (format "Username (default %s): "
bb5d4e1a 443 (shadow-get-user site)))
5354cb6d
RS
444 name (read-string "Filename: " name))
445 (setq group (cons (shadow-make-fullname site
bb5d4e1a
RS
446 (if (string-equal "" user)
447 (shadow-get-user site)
448 user)
5354cb6d 449 name)
bb5d4e1a
RS
450 group)))
451 (setq shadow-literal-groups (cons group shadow-literal-groups)))
452 (shadow-write-info-file))
453
36fd8e17 454;;;###autoload
bb5d4e1a
RS
455(defun shadow-define-regexp-group ()
456 "Make each of a group of files be shared between hosts.
457Prompts for regular expression; files matching this are shared between a list
36fd8e17 458of sites, which are also prompted for. The filenames must be identical on all
06b60517
JB
459hosts (if they aren't, use `shadow-define-literal-group' instead of this
460function). Each site can be either a hostname or the name of a cluster (see
36fd8e17 461`shadow-define-cluster')."
bb5d4e1a 462 (interactive)
36fd8e17
DL
463 (let ((regexp (read-string
464 "Filename regexp: "
bb5d4e1a
RS
465 (if (buffer-file-name)
466 (shadow-regexp-superquote
467 (nth 2
5354cb6d 468 (shadow-parse-name
bb5d4e1a
RS
469 (shadow-contract-file-name
470 (buffer-file-name))))))))
471 site sites usernames)
472 (while (setq site (shadow-read-site))
473 (setq sites (cons site sites))
36fd8e17 474 (setq usernames
bb5d4e1a
RS
475 (cons (read-string (format "Username for %s: " site)
476 (shadow-get-user site))
477 usernames)))
36fd8e17 478 (setq shadow-regexp-groups
bb5d4e1a
RS
479 (cons (shadow-make-group regexp sites usernames)
480 shadow-regexp-groups))
481 (shadow-write-info-file)))
f1180544 482
bb5d4e1a
RS
483(defun shadow-shadows ()
484 ;; Mostly for debugging.
485 "Interactive function to display shadows of a buffer."
486 (interactive)
32f389a4 487 (let ((msg (mapconcat #'cdr (shadow-shadows-of (buffer-file-name)) " ")))
673dac20 488 (message "%s"
36fd8e17 489 (if (zerop (length msg))
bb5d4e1a
RS
490 "No shadows."
491 msg))))
492
493(defun shadow-copy-files (&optional arg)
494 "Copy all pending shadow files.
495With prefix argument, copy all pending files without query.
36fd8e17
DL
496Pending copies are stored in variable `shadow-files-to-copy', and in
497`shadow-todo-file' if necessary. This function is invoked by
498`shadow-save-buffers-kill-emacs', so it is not usually necessary to
bb5d4e1a
RS
499call it manually."
500 (interactive "P")
4f8e58ec 501 (if (not shadow-files-to-copy)
32226619 502 (if (called-interactively-p 'interactive)
4f8e58ec 503 (message "No files need to be shadowed."))
bb5d4e1a
RS
504 (save-excursion
505 (map-y-or-n-p (function
506 (lambda (pair)
191b14ba 507 (or arg shadow-noquery
bb5d4e1a
RS
508 (format "Copy shadow file %s? " (cdr pair)))))
509 (function shadow-copy-file)
510 shadow-files-to-copy
511 '("shadow" "shadows" "copy"))
512 (shadow-write-todo-file t))))
513
514(defun shadow-cancel ()
515 "Cancel the instruction to copy some files.
516Prompts for which copy operations to cancel. You will not be asked to copy
517them again, unless you make more changes to the files. To cancel a shadow
36fd8e17
DL
518permanently, remove the group from `shadow-literal-groups' or
519`shadow-regexp-groups'."
bb5d4e1a
RS
520 (interactive)
521 (map-y-or-n-p (function (lambda (pair)
36fd8e17 522 (format "Cancel copying %s to %s? "
bb5d4e1a 523 (car pair) (cdr pair))))
36fd8e17 524 (function (lambda (pair)
bb5d4e1a
RS
525 (shadow-remove-from-todo pair)))
526 shadow-files-to-copy
527 '("shadow" "shadows" "cancel copy"))
36fd8e17 528 (message "There are %d shadows to be updated."
673dac20 529 (length shadow-files-to-copy))
bb5d4e1a
RS
530 (shadow-write-todo-file))
531
532;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
533;;; Internal functions
534;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
535
536(defun shadow-make-group (regexp sites usernames)
36fd8e17 537 "Make a description of a file group---
06b60517 538actually a list of regexp ange-ftp file names---from REGEXP (name of file to
bb5d4e1a
RS
539be shadowed), list of SITES, and corresponding list of USERNAMES for each
540site."
541 (if sites
5354cb6d 542 (cons (shadow-make-fullname (car sites) (car usernames) regexp)
bb5d4e1a
RS
543 (shadow-make-group regexp (cdr sites) (cdr usernames)))
544 nil))
545
546(defun shadow-copy-file (s)
547 "Copy one shadow file."
36fd8e17
DL
548 (let* ((buffer
549 (cond ((get-file-buffer
191b14ba 550 (abbreviate-file-name (shadow-expand-file-name (car s)))))
bb5d4e1a
RS
551 ((not (file-readable-p (car s)))
552 (if (y-or-n-p
36fd8e17 553 (format "Cannot find file %s--cancel copy request? "
bb5d4e1a
RS
554 (car s)))
555 (shadow-remove-from-todo s))
556 nil)
191b14ba 557 ((or (eq t shadow-noquery)
36fd8e17
DL
558 (y-or-n-p
559 (format "No buffer for %s -- update shadow anyway? "
191b14ba 560 (car s))))
bb5d4e1a
RS
561 (find-file-noselect (car s)))))
562 (to (shadow-expand-cluster-in-file-name (cdr s))))
5a7b9024 563 (when buffer
bb5d4e1a 564 (set-buffer buffer)
06b60517 565 (condition-case nil
348a60a8 566 (progn
737ef682 567 (write-region nil nil to)
ab1d3835
SM
568 (shadow-remove-from-todo s))
569 (error (message "Shadow %s not updated!" (cdr s)))))))
bb5d4e1a
RS
570
571(defun shadow-shadows-of (file)
36fd8e17
DL
572 "Return copy operations needed to update FILE.
573Filename should have clusters expanded, but otherwise can have any format.
06b60517 574Return value is a list of dotted pairs like (from . to), where from
bb5d4e1a
RS
575and to are absolute file names."
576 (or (symbol-value (intern-soft file shadow-hashtable))
577 (let* ((absolute-file (shadow-expand-file-name
578 (or (shadow-local-file file) file)
579 shadow-homedir))
580 (canonical-file (shadow-contract-file-name absolute-file))
36fd8e17 581 (shadows
bb5d4e1a
RS
582 (mapcar (function (lambda (shadow)
583 (cons absolute-file shadow)))
584 (append
585 (shadow-shadows-of-1
586 canonical-file shadow-literal-groups nil)
587 (shadow-shadows-of-1
588 canonical-file shadow-regexp-groups t)))))
589 (set (intern file shadow-hashtable) shadows))))
590
591(defun shadow-shadows-of-1 (file groups regexp)
36fd8e17
DL
592 "Return list of FILE's shadows in GROUPS.
593Consider them as regular expressions if third arg REGEXP is true."
bb5d4e1a
RS
594 (if groups
595 (let ((nonmatching
539a920c
GM
596 (cl-remove-if (lambda (x) (shadow-file-match x file regexp))
597 (car groups))))
bb5d4e1a 598 (append (cond ((equal nonmatching (car groups)) nil)
36fd8e17 599 (regexp
5354cb6d 600 (let ((realname (nth 2 (shadow-parse-fullname file))))
36fd8e17
DL
601 (mapcar
602 (function
603 (lambda (x)
5354cb6d 604 (shadow-replace-name-component x realname)))
bb5d4e1a
RS
605 nonmatching)))
606 (t nonmatching))
607 (shadow-shadows-of-1 file (cdr groups) regexp)))))
608
609(defun shadow-add-to-todo ()
36fd8e17
DL
610 "If current buffer has shadows, add them to the list needing to be copied."
611 (let ((shadows (shadow-shadows-of
612 (shadow-expand-file-name
bb5d4e1a 613 (buffer-file-name (current-buffer))))))
5a7b9024 614 (when shadows
bb5d4e1a
RS
615 (setq shadow-files-to-copy
616 (shadow-union shadows shadow-files-to-copy))
5a7b9024 617 (when (not shadow-inhibit-message)
673dac20
KH
618 (message "%s" (substitute-command-keys
619 "Use \\[shadow-copy-files] to update shadows."))
bb5d4e1a
RS
620 (sit-for 1))
621 (shadow-write-todo-file)))
32f389a4 622 nil) ; Return nil for write-file-functions
bb5d4e1a
RS
623
624(defun shadow-remove-from-todo (pair)
36fd8e17 625 "Remove PAIR from `shadow-files-to-copy'.
2fc88dcc 626PAIR must be `eq' to one of the elements of that list."
36fd8e17 627 (setq shadow-files-to-copy
539a920c 628 (cl-remove-if (lambda (s) (eq s pair)) shadow-files-to-copy)))
bb5d4e1a
RS
629
630(defun shadow-read-files ()
36fd8e17
DL
631 "Visit and load `shadow-info-file' and `shadow-todo-file'.
632Thus restores shadowfile's state from your last Emacs session.
2fc88dcc 633Return t unless files were locked; then return nil."
bb5d4e1a 634 (interactive)
191b14ba
RS
635 (if (and (fboundp 'file-locked-p)
636 (or (stringp (file-locked-p shadow-info-file))
637 (stringp (file-locked-p shadow-todo-file))))
bb5d4e1a 638 (progn
7ad8d84e 639 (message "Shadowfile is running in another Emacs; can't have two.")
bb5d4e1a
RS
640 (beep)
641 (sit-for 3)
642 nil)
940e5099 643 (save-current-buffer
5a7b9024 644 (when shadow-info-file
bb5d4e1a
RS
645 (set-buffer (setq shadow-info-buffer
646 (find-file-noselect shadow-info-file)))
5a7b9024
GM
647 (when (and (not (buffer-modified-p))
648 (file-newer-than-file-p (make-auto-save-file-name)
649 shadow-info-file))
bb5d4e1a 650 (erase-buffer)
36fd8e17 651 (message "Data recovered from %s."
bb5d4e1a
RS
652 (car (insert-file-contents (make-auto-save-file-name))))
653 (sit-for 1))
887195ac 654 (eval-buffer))
5a7b9024 655 (when shadow-todo-file
36fd8e17 656 (set-buffer (setq shadow-todo-buffer
bb5d4e1a 657 (find-file-noselect shadow-todo-file)))
5a7b9024
GM
658 (when (and (not (buffer-modified-p))
659 (file-newer-than-file-p (make-auto-save-file-name)
660 shadow-todo-file))
bb5d4e1a 661 (erase-buffer)
36fd8e17 662 (message "Data recovered from %s."
bb5d4e1a
RS
663 (car (insert-file-contents (make-auto-save-file-name))))
664 (sit-for 1))
887195ac 665 (eval-buffer nil))
bb5d4e1a
RS
666 (shadow-invalidate-hashtable))
667 t))
668
669(defun shadow-write-info-file ()
36fd8e17
DL
670 "Write out information to `shadow-info-file'.
671Also clear `shadow-hashtable', since when there are new shadows
672defined, the old hashtable info is invalid."
bb5d4e1a
RS
673 (shadow-invalidate-hashtable)
674 (if shadow-info-file
940e5099 675 (save-current-buffer
bb5d4e1a
RS
676 (if (not shadow-info-buffer)
677 (setq shadow-info-buffer (find-file-noselect shadow-info-file)))
678 (set-buffer shadow-info-buffer)
679 (delete-region (point-min) (point-max))
680 (shadow-insert-var 'shadow-clusters)
681 (shadow-insert-var 'shadow-literal-groups)
682 (shadow-insert-var 'shadow-regexp-groups))))
683
684(defun shadow-write-todo-file (&optional save)
7ad8d84e
JB
685 "Write out information to `shadow-todo-file'.
686With non-nil argument also saves the buffer."
bb5d4e1a
RS
687 (save-excursion
688 (if (not shadow-todo-buffer)
689 (setq shadow-todo-buffer (find-file-noselect shadow-todo-file)))
690 (set-buffer shadow-todo-buffer)
691 (delete-region (point-min) (point-max))
692 (shadow-insert-var 'shadow-files-to-copy)
693 (if save (shadow-save-todo-file))))
694
695(defun shadow-save-todo-file ()
696 (if (and shadow-todo-buffer (buffer-modified-p shadow-todo-buffer))
7fdbcd83 697 (with-current-buffer shadow-todo-buffer
36fd8e17 698 (condition-case nil ; have to continue even in case of
bb5d4e1a
RS
699 (basic-save-buffer) ; error, otherwise kill-emacs might
700 (error ; not work!
701 (message "WARNING: Can't save shadow todo file; it is locked!")
702 (sit-for 1))))))
703
704(defun shadow-invalidate-hashtable ()
705 (setq shadow-hashtable (make-vector 37 0)))
706
707(defun shadow-insert-var (variable)
2fc88dcc
JB
708 "Build a `setq' to restore VARIABLE.
709Prettily insert a `setq' command which, when later evaluated,
710will restore VARIABLE to its current setting.
7ad8d84e 711VARIABLE must be the name of a variable whose value is a list."
bb5d4e1a
RS
712 (let ((standard-output (current-buffer)))
713 (insert (format "(setq %s" variable))
714 (cond ((consp (eval variable))
36fd8e17 715 (insert "\n '(")
bb5d4e1a
RS
716 (prin1 (car (eval variable)))
717 (let ((rest (cdr (eval variable))))
718 (while rest
719 (insert "\n ")
720 (prin1 (car rest))
721 (setq rest (cdr rest)))
722 (insert "))\n\n")))
723 (t (insert " ")
724 (prin1 (eval variable))
725 (insert ")\n\n")))))
726
727(defun shadow-save-buffers-kill-emacs (&optional arg)
728 "Offer to save each buffer and copy shadows, then kill this Emacs process.
729With prefix arg, silently save all file-visiting buffers, then kill.
730
731Extended by shadowfile to automatically save `shadow-todo-file' and
732look for files that have been changed and need to be copied to other systems."
733 ;; This function is necessary because we need to get control and save
734 ;; the todo file /after/ saving other files, but /before/ the warning
735 ;; message about unsaved buffers (because it can get modified by the
736 ;; action of saving other buffers). `kill-emacs-hook' is no good
737 ;; because it is not called at the correct time, and also because it is
738 ;; called when the terminal is disconnected and we cannot ask whether
739 ;; to copy files.
740 (interactive "P")
741 (shadow-save-todo-file)
742 (save-some-buffers arg t)
743 (shadow-copy-files)
744 (shadow-save-todo-file)
745 (and (or (not (memq t (mapcar (function
746 (lambda (buf) (and (buffer-file-name buf)
747 (buffer-modified-p buf))))
748 (buffer-list))))
749 (yes-or-no-p "Modified buffers exist; exit anyway? "))
750 (or (not (fboundp 'process-list))
7c2fb837 751 ;; process-list is not defined on MSDOS.
bb5d4e1a
RS
752 (let ((processes (process-list))
753 active)
754 (while processes
41c4dfe1
KS
755 (and (memq (process-status (car processes)) '(run stop open listen))
756 (process-query-on-exit-flag (car processes))
bb5d4e1a
RS
757 (setq active t))
758 (setq processes (cdr processes)))
759 (or (not active)
760 (yes-or-no-p "Active processes exist; kill them and exit anyway? "))))
761 (kill-emacs)))
762
763;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
36fd8e17 764;;; Lucid Emacs compatibility
bb5d4e1a
RS
765;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
766
191b14ba
RS
767;; This is on hold until someone tells me about a working version of
768;; map-ynp for Lucid Emacs.
769
5a7b9024 770;(when (string-match "Lucid" emacs-version)
191b14ba
RS
771; (require 'symlink-fix)
772; (require 'ange-ftp)
773; (require 'map-ynp)
774; (if (not (fboundp 'file-truename))
36fd8e17 775; (fset 'shadow-expand-file-name
191b14ba
RS
776; (symbol-function 'symlink-expand-file-name)))
777; (if (not (fboundp 'ange-ftp-ftp-name))
778; (fset 'ange-ftp-ftp-name
5354cb6d 779; (symbol-function 'ange-ftp-ftp-name))))
bb5d4e1a
RS
780
781;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
782;;; Hook us up
783;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
784
36fd8e17 785;;;###autoload
bb5d4e1a 786(defun shadow-initialize ()
36fd8e17
DL
787 "Set up file shadowing."
788 (interactive)
bb5d4e1a
RS
789 (if (null shadow-homedir)
790 (setq shadow-homedir
791 (file-name-as-directory (shadow-expand-file-name "~"))))
792 (if (null shadow-info-file)
36fd8e17 793 (setq shadow-info-file
940e5099
SM
794 ;; FIXME: Move defaults to their defcustom.
795 (shadow-expand-file-name
796 (locate-user-emacs-file "shadows" ".shadows"))))
bb5d4e1a 797 (if (null shadow-todo-file)
36fd8e17 798 (setq shadow-todo-file
b6489a43 799 (shadow-expand-file-name
940e5099 800 (locate-user-emacs-file "shadow_todo" ".shadow_todo"))))
bb5d4e1a
RS
801 (if (not (shadow-read-files))
802 (progn
803 (message "Shadowfile information files not found - aborting")
804 (beep)
805 (sit-for 3))
5a7b9024
GM
806 (when (and (not shadow-inhibit-overload)
807 (not (fboundp 'shadow-orig-save-buffers-kill-emacs)))
36fd8e17
DL
808 (defalias 'shadow-orig-save-buffers-kill-emacs
809 (symbol-function 'save-buffers-kill-emacs))
810 (defalias 'save-buffers-kill-emacs 'shadow-save-buffers-kill-emacs))
32f389a4 811 (add-hook 'write-file-functions 'shadow-add-to-todo)
bb5d4e1a
RS
812 (define-key ctl-x-4-map "s" 'shadow-copy-files)))
813
32f389a4
JB
814(defun shadowfile-unload-function ()
815 (substitute-key-definition 'shadow-copy-files nil ctl-x-4-map)
816 (when (fboundp 'shadow-orig-save-buffers-kill-emacs)
817 (fset 'save-buffers-kill-emacs
818 (symbol-function 'shadow-orig-save-buffers-kill-emacs)))
819 ;; continue standard unloading
820 nil)
a6d23706 821
36fd8e17
DL
822(provide 'shadowfile)
823
bb5d4e1a 824;;; shadowfile.el ends here