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