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