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