Add 2012 to FSF copyright years for Emacs files
[bpt/emacs.git] / lisp / shadowfile.el
CommitLineData
36fd8e17 1;;; shadowfile.el --- automatic file copying
bb5d4e1a 2
acaf905b 3;; Copyright (C) 1993-1994, 2001-2012 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.
06b60517 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
06b60517 128;;; on your machine (and for efficiency).
bb5d4e1a
RS
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
06b60517 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)
06b60517 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 297(defun shadow-parse-fullname (fullname)
06b60517 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)
06b60517 306 "Parse any NAME into (site user name) list.
5354cb6d
RS
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 339Will return the name bare if it is a local file."
06b60517 340 (let ((hup (shadow-parse-name file)))
bb5d4e1a
RS
341 (cond ((null hup) file)
342 ((shadow-local-file hup))
5354cb6d 343 ((shadow-make-fullname (shadow-site-primary (nth 0 hup))
bb5d4e1a
RS
344 (nth 1 hup)
345 (nth 2 hup))))))
346
347(defun shadow-expand-file-name (file &optional default)
7ad8d84e 348 "Expand file name and get FILE's true name."
bb5d4e1a
RS
349 (file-truename (expand-file-name file default)))
350
351(defun shadow-contract-file-name (file)
36fd8e17
DL
352 "Simplify FILE.
353Do so by replacing (when possible) home directory with ~, and hostname
354with cluster name that includes it. Filename should be absolute and
355true."
5354cb6d 356 (let* ((hup (shadow-parse-name file))
bb5d4e1a
RS
357 (homedir (if (shadow-local-file hup)
358 shadow-homedir
359 (file-name-as-directory
5354cb6d 360 (nth 2 (shadow-parse-fullname
bb5d4e1a 361 (expand-file-name
5354cb6d 362 (shadow-make-fullname
bb5d4e1a
RS
363 (nth 0 hup) (nth 1 hup) "~")))))))
364 (suffix (shadow-suffix homedir (nth 2 hup)))
365 (cluster (shadow-site-cluster (nth 0 hup))))
5354cb6d 366 (shadow-make-fullname
bb5d4e1a
RS
367 (if cluster
368 (shadow-cluster-name cluster)
369 (nth 0 hup))
370 (nth 1 hup)
36fd8e17 371 (if suffix
bb5d4e1a
RS
372 (concat "~/" suffix)
373 (nth 2 hup)))))
374
375(defun shadow-same-site (pattern file)
376 "True if the site of PATTERN and of FILE are on the same site.
377If usernames are supplied, they must also match exactly. PATTERN and FILE may
5354cb6d 378be lists of host, user, name, or ange-ftp file names. FILE may also be just a
bb5d4e1a 379local filename."
5354cb6d
RS
380 (let ((pattern-sup (shadow-parse-fullname pattern))
381 (file-sup (shadow-parse-name file)))
bb5d4e1a
RS
382 (and
383 (shadow-site-match (nth 0 pattern-sup) (nth 0 file-sup))
384 (or (null (nth 1 pattern-sup))
385 (string-equal (nth 1 pattern-sup) (nth 1 file-sup))))))
386
387(defun shadow-file-match (pattern file &optional regexp)
36fd8e17 388 "Return t if PATTERN matches FILE.
5354cb6d 389If REGEXP is supplied and non-nil, the file part of the pattern is a regular
bb5d4e1a 390expression, otherwise it must match exactly. The sites and usernames must
2fc88dcc
JB
391match---see `shadow-same-site'. The pattern must be in full ange-ftp format,
392but the file can be any valid filename. This function does not do any
393filename expansion or contraction, you must do that yourself first."
5354cb6d
RS
394 (let* ((pattern-sup (shadow-parse-fullname pattern))
395 (file-sup (shadow-parse-name file)))
bb5d4e1a 396 (and (shadow-same-site pattern-sup file-sup)
36fd8e17 397 (if regexp
bb5d4e1a
RS
398 (string-match (nth 2 pattern-sup) (nth 2 file-sup))
399 (string-equal (nth 2 pattern-sup) (nth 2 file-sup))))))
f1180544 400
bb5d4e1a
RS
401;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
402;;; User-level Commands
403;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
404
36fd8e17 405;;;###autoload
bb5d4e1a 406(defun shadow-define-cluster (name)
06b60517 407 "Edit (or create) the definition of a cluster NAME.
bb5d4e1a
RS
408This is a group of hosts that share directories, so that copying to or from
409one of them is sufficient to update the file on all of them. Clusters are
06b60517 410defined by a name, the network address of a primary host (the one we copy
2fc88dcc
JB
411files to), and a regular expression that matches the hostnames of all the
412sites in the cluster."
bb5d4e1a
RS
413 (interactive (list (completing-read "Cluster name: " shadow-clusters () ())))
414 (let* ((old (shadow-get-cluster name))
415 (primary (read-string "Primary host: "
36fd8e17 416 (if old (shadow-cluster-primary old)
bb5d4e1a
RS
417 name)))
418 (regexp (let (try-regexp)
419 (while (not
36fd8e17 420 (string-match
bb5d4e1a 421 (setq try-regexp
36fd8e17 422 (read-string
bb5d4e1a
RS
423 "Regexp matching all host names: "
424 (if old (shadow-cluster-regexp old)
425 (shadow-regexp-superquote primary))))
426 primary))
427 (message "Regexp doesn't include the primary host!")
428 (sit-for 2))
429 try-regexp))
36fd8e17 430; (username (read-no-blanks-input
5b76833f 431; (format "Username (default %s): "
bb5d4e1a
RS
432; (shadow-get-user primary))
433; (if old (or (shadow-cluster-username old) "")
434; (user-login-name))))
435 )
436; (if (string-equal "" username) (setq username nil))
437 (shadow-set-cluster name primary regexp)))
438
36fd8e17 439;;;###autoload
bb5d4e1a
RS
440(defun shadow-define-literal-group ()
441 "Declare a single file to be shared between sites.
442It may have different filenames on each site. When this file is edited, the
443new version will be copied to each of the other locations. Sites can be
06b60517 444specific hostnames, or names of clusters (see `shadow-define-cluster')."
bb5d4e1a 445 (interactive)
5354cb6d 446 (let* ((hup (shadow-parse-fullname
bb5d4e1a 447 (shadow-contract-file-name (buffer-file-name))))
5354cb6d 448 (name (nth 2 hup))
bb5d4e1a
RS
449 user site group)
450 (while (setq site (shadow-read-site))
5b76833f 451 (setq user (read-string (format "Username (default %s): "
bb5d4e1a 452 (shadow-get-user site)))
5354cb6d
RS
453 name (read-string "Filename: " name))
454 (setq group (cons (shadow-make-fullname site
bb5d4e1a
RS
455 (if (string-equal "" user)
456 (shadow-get-user site)
457 user)
5354cb6d 458 name)
bb5d4e1a
RS
459 group)))
460 (setq shadow-literal-groups (cons group shadow-literal-groups)))
461 (shadow-write-info-file))
462
36fd8e17 463;;;###autoload
bb5d4e1a
RS
464(defun shadow-define-regexp-group ()
465 "Make each of a group of files be shared between hosts.
466Prompts for regular expression; files matching this are shared between a list
36fd8e17 467of sites, which are also prompted for. The filenames must be identical on all
06b60517
JB
468hosts (if they aren't, use `shadow-define-literal-group' instead of this
469function). Each site can be either a hostname or the name of a cluster (see
36fd8e17 470`shadow-define-cluster')."
bb5d4e1a 471 (interactive)
36fd8e17
DL
472 (let ((regexp (read-string
473 "Filename regexp: "
bb5d4e1a
RS
474 (if (buffer-file-name)
475 (shadow-regexp-superquote
476 (nth 2
5354cb6d 477 (shadow-parse-name
bb5d4e1a
RS
478 (shadow-contract-file-name
479 (buffer-file-name))))))))
480 site sites usernames)
481 (while (setq site (shadow-read-site))
482 (setq sites (cons site sites))
36fd8e17 483 (setq usernames
bb5d4e1a
RS
484 (cons (read-string (format "Username for %s: " site)
485 (shadow-get-user site))
486 usernames)))
36fd8e17 487 (setq shadow-regexp-groups
bb5d4e1a
RS
488 (cons (shadow-make-group regexp sites usernames)
489 shadow-regexp-groups))
490 (shadow-write-info-file)))
f1180544 491
bb5d4e1a
RS
492(defun shadow-shadows ()
493 ;; Mostly for debugging.
494 "Interactive function to display shadows of a buffer."
495 (interactive)
32f389a4 496 (let ((msg (mapconcat #'cdr (shadow-shadows-of (buffer-file-name)) " ")))
673dac20 497 (message "%s"
36fd8e17 498 (if (zerop (length msg))
bb5d4e1a
RS
499 "No shadows."
500 msg))))
501
502(defun shadow-copy-files (&optional arg)
503 "Copy all pending shadow files.
504With prefix argument, copy all pending files without query.
36fd8e17
DL
505Pending copies are stored in variable `shadow-files-to-copy', and in
506`shadow-todo-file' if necessary. This function is invoked by
507`shadow-save-buffers-kill-emacs', so it is not usually necessary to
bb5d4e1a
RS
508call it manually."
509 (interactive "P")
4f8e58ec 510 (if (not shadow-files-to-copy)
32226619 511 (if (called-interactively-p 'interactive)
4f8e58ec 512 (message "No files need to be shadowed."))
bb5d4e1a
RS
513 (save-excursion
514 (map-y-or-n-p (function
515 (lambda (pair)
191b14ba 516 (or arg shadow-noquery
bb5d4e1a
RS
517 (format "Copy shadow file %s? " (cdr pair)))))
518 (function shadow-copy-file)
519 shadow-files-to-copy
520 '("shadow" "shadows" "copy"))
521 (shadow-write-todo-file t))))
522
523(defun shadow-cancel ()
524 "Cancel the instruction to copy some files.
525Prompts for which copy operations to cancel. You will not be asked to copy
526them again, unless you make more changes to the files. To cancel a shadow
36fd8e17
DL
527permanently, remove the group from `shadow-literal-groups' or
528`shadow-regexp-groups'."
bb5d4e1a
RS
529 (interactive)
530 (map-y-or-n-p (function (lambda (pair)
36fd8e17 531 (format "Cancel copying %s to %s? "
bb5d4e1a 532 (car pair) (cdr pair))))
36fd8e17 533 (function (lambda (pair)
bb5d4e1a
RS
534 (shadow-remove-from-todo pair)))
535 shadow-files-to-copy
536 '("shadow" "shadows" "cancel copy"))
36fd8e17 537 (message "There are %d shadows to be updated."
673dac20 538 (length shadow-files-to-copy))
bb5d4e1a
RS
539 (shadow-write-todo-file))
540
541;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
542;;; Internal functions
543;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
544
545(defun shadow-make-group (regexp sites usernames)
36fd8e17 546 "Make a description of a file group---
06b60517 547actually a list of regexp ange-ftp file names---from REGEXP (name of file to
bb5d4e1a
RS
548be shadowed), list of SITES, and corresponding list of USERNAMES for each
549site."
550 (if sites
5354cb6d 551 (cons (shadow-make-fullname (car sites) (car usernames) regexp)
bb5d4e1a
RS
552 (shadow-make-group regexp (cdr sites) (cdr usernames)))
553 nil))
554
555(defun shadow-copy-file (s)
556 "Copy one shadow file."
36fd8e17
DL
557 (let* ((buffer
558 (cond ((get-file-buffer
191b14ba 559 (abbreviate-file-name (shadow-expand-file-name (car s)))))
bb5d4e1a
RS
560 ((not (file-readable-p (car s)))
561 (if (y-or-n-p
36fd8e17 562 (format "Cannot find file %s--cancel copy request? "
bb5d4e1a
RS
563 (car s)))
564 (shadow-remove-from-todo s))
565 nil)
191b14ba 566 ((or (eq t shadow-noquery)
36fd8e17
DL
567 (y-or-n-p
568 (format "No buffer for %s -- update shadow anyway? "
191b14ba 569 (car s))))
bb5d4e1a
RS
570 (find-file-noselect (car s)))))
571 (to (shadow-expand-cluster-in-file-name (cdr s))))
5a7b9024 572 (when buffer
bb5d4e1a 573 (set-buffer buffer)
06b60517 574 (condition-case nil
348a60a8 575 (progn
737ef682 576 (write-region nil nil to)
ab1d3835
SM
577 (shadow-remove-from-todo s))
578 (error (message "Shadow %s not updated!" (cdr s)))))))
bb5d4e1a
RS
579
580(defun shadow-shadows-of (file)
36fd8e17
DL
581 "Return copy operations needed to update FILE.
582Filename should have clusters expanded, but otherwise can have any format.
06b60517 583Return value is a list of dotted pairs like (from . to), where from
bb5d4e1a
RS
584and to are absolute file names."
585 (or (symbol-value (intern-soft file shadow-hashtable))
586 (let* ((absolute-file (shadow-expand-file-name
587 (or (shadow-local-file file) file)
588 shadow-homedir))
589 (canonical-file (shadow-contract-file-name absolute-file))
36fd8e17 590 (shadows
bb5d4e1a
RS
591 (mapcar (function (lambda (shadow)
592 (cons absolute-file shadow)))
593 (append
594 (shadow-shadows-of-1
595 canonical-file shadow-literal-groups nil)
596 (shadow-shadows-of-1
597 canonical-file shadow-regexp-groups t)))))
598 (set (intern file shadow-hashtable) shadows))))
599
600(defun shadow-shadows-of-1 (file groups regexp)
36fd8e17
DL
601 "Return list of FILE's shadows in GROUPS.
602Consider them as regular expressions if third arg REGEXP is true."
bb5d4e1a
RS
603 (if groups
604 (let ((nonmatching
36fd8e17 605 (shadow-remove-if
bb5d4e1a
RS
606 (function (lambda (x) (shadow-file-match x file regexp)))
607 (car groups))))
608 (append (cond ((equal nonmatching (car groups)) nil)
36fd8e17 609 (regexp
5354cb6d 610 (let ((realname (nth 2 (shadow-parse-fullname file))))
36fd8e17
DL
611 (mapcar
612 (function
613 (lambda (x)
5354cb6d 614 (shadow-replace-name-component x realname)))
bb5d4e1a
RS
615 nonmatching)))
616 (t nonmatching))
617 (shadow-shadows-of-1 file (cdr groups) regexp)))))
618
619(defun shadow-add-to-todo ()
36fd8e17
DL
620 "If current buffer has shadows, add them to the list needing to be copied."
621 (let ((shadows (shadow-shadows-of
622 (shadow-expand-file-name
bb5d4e1a 623 (buffer-file-name (current-buffer))))))
5a7b9024 624 (when shadows
bb5d4e1a
RS
625 (setq shadow-files-to-copy
626 (shadow-union shadows shadow-files-to-copy))
5a7b9024 627 (when (not shadow-inhibit-message)
673dac20
KH
628 (message "%s" (substitute-command-keys
629 "Use \\[shadow-copy-files] to update shadows."))
bb5d4e1a
RS
630 (sit-for 1))
631 (shadow-write-todo-file)))
32f389a4 632 nil) ; Return nil for write-file-functions
bb5d4e1a
RS
633
634(defun shadow-remove-from-todo (pair)
36fd8e17 635 "Remove PAIR from `shadow-files-to-copy'.
2fc88dcc 636PAIR must be `eq' to one of the elements of that list."
36fd8e17 637 (setq shadow-files-to-copy
bb5d4e1a
RS
638 (shadow-remove-if (function (lambda (s) (eq s pair)))
639 shadow-files-to-copy)))
640
641(defun shadow-read-files ()
36fd8e17
DL
642 "Visit and load `shadow-info-file' and `shadow-todo-file'.
643Thus restores shadowfile's state from your last Emacs session.
2fc88dcc 644Return t unless files were locked; then return nil."
bb5d4e1a 645 (interactive)
191b14ba
RS
646 (if (and (fboundp 'file-locked-p)
647 (or (stringp (file-locked-p shadow-info-file))
648 (stringp (file-locked-p shadow-todo-file))))
bb5d4e1a 649 (progn
7ad8d84e 650 (message "Shadowfile is running in another Emacs; can't have two.")
bb5d4e1a
RS
651 (beep)
652 (sit-for 3)
653 nil)
654 (save-excursion
5a7b9024 655 (when shadow-info-file
bb5d4e1a
RS
656 (set-buffer (setq shadow-info-buffer
657 (find-file-noselect shadow-info-file)))
5a7b9024
GM
658 (when (and (not (buffer-modified-p))
659 (file-newer-than-file-p (make-auto-save-file-name)
660 shadow-info-file))
bb5d4e1a 661 (erase-buffer)
36fd8e17 662 (message "Data recovered from %s."
bb5d4e1a
RS
663 (car (insert-file-contents (make-auto-save-file-name))))
664 (sit-for 1))
887195ac 665 (eval-buffer))
5a7b9024 666 (when shadow-todo-file
36fd8e17 667 (set-buffer (setq shadow-todo-buffer
bb5d4e1a 668 (find-file-noselect shadow-todo-file)))
5a7b9024
GM
669 (when (and (not (buffer-modified-p))
670 (file-newer-than-file-p (make-auto-save-file-name)
671 shadow-todo-file))
bb5d4e1a 672 (erase-buffer)
36fd8e17 673 (message "Data recovered from %s."
bb5d4e1a
RS
674 (car (insert-file-contents (make-auto-save-file-name))))
675 (sit-for 1))
887195ac 676 (eval-buffer nil))
bb5d4e1a
RS
677 (shadow-invalidate-hashtable))
678 t))
679
680(defun shadow-write-info-file ()
36fd8e17
DL
681 "Write out information to `shadow-info-file'.
682Also clear `shadow-hashtable', since when there are new shadows
683defined, the old hashtable info is invalid."
bb5d4e1a
RS
684 (shadow-invalidate-hashtable)
685 (if shadow-info-file
686 (save-excursion
687 (if (not shadow-info-buffer)
688 (setq shadow-info-buffer (find-file-noselect shadow-info-file)))
689 (set-buffer shadow-info-buffer)
690 (delete-region (point-min) (point-max))
691 (shadow-insert-var 'shadow-clusters)
692 (shadow-insert-var 'shadow-literal-groups)
693 (shadow-insert-var 'shadow-regexp-groups))))
694
695(defun shadow-write-todo-file (&optional save)
7ad8d84e
JB
696 "Write out information to `shadow-todo-file'.
697With non-nil argument also saves the buffer."
bb5d4e1a
RS
698 (save-excursion
699 (if (not shadow-todo-buffer)
700 (setq shadow-todo-buffer (find-file-noselect shadow-todo-file)))
701 (set-buffer shadow-todo-buffer)
702 (delete-region (point-min) (point-max))
703 (shadow-insert-var 'shadow-files-to-copy)
704 (if save (shadow-save-todo-file))))
705
706(defun shadow-save-todo-file ()
707 (if (and shadow-todo-buffer (buffer-modified-p shadow-todo-buffer))
7fdbcd83 708 (with-current-buffer shadow-todo-buffer
36fd8e17 709 (condition-case nil ; have to continue even in case of
bb5d4e1a
RS
710 (basic-save-buffer) ; error, otherwise kill-emacs might
711 (error ; not work!
712 (message "WARNING: Can't save shadow todo file; it is locked!")
713 (sit-for 1))))))
714
715(defun shadow-invalidate-hashtable ()
716 (setq shadow-hashtable (make-vector 37 0)))
717
718(defun shadow-insert-var (variable)
2fc88dcc
JB
719 "Build a `setq' to restore VARIABLE.
720Prettily insert a `setq' command which, when later evaluated,
721will restore VARIABLE to its current setting.
7ad8d84e 722VARIABLE must be the name of a variable whose value is a list."
bb5d4e1a
RS
723 (let ((standard-output (current-buffer)))
724 (insert (format "(setq %s" variable))
725 (cond ((consp (eval variable))
36fd8e17 726 (insert "\n '(")
bb5d4e1a
RS
727 (prin1 (car (eval variable)))
728 (let ((rest (cdr (eval variable))))
729 (while rest
730 (insert "\n ")
731 (prin1 (car rest))
732 (setq rest (cdr rest)))
733 (insert "))\n\n")))
734 (t (insert " ")
735 (prin1 (eval variable))
736 (insert ")\n\n")))))
737
738(defun shadow-save-buffers-kill-emacs (&optional arg)
739 "Offer to save each buffer and copy shadows, then kill this Emacs process.
740With prefix arg, silently save all file-visiting buffers, then kill.
741
742Extended by shadowfile to automatically save `shadow-todo-file' and
743look for files that have been changed and need to be copied to other systems."
744 ;; This function is necessary because we need to get control and save
745 ;; the todo file /after/ saving other files, but /before/ the warning
746 ;; message about unsaved buffers (because it can get modified by the
747 ;; action of saving other buffers). `kill-emacs-hook' is no good
748 ;; because it is not called at the correct time, and also because it is
749 ;; called when the terminal is disconnected and we cannot ask whether
750 ;; to copy files.
751 (interactive "P")
752 (shadow-save-todo-file)
753 (save-some-buffers arg t)
754 (shadow-copy-files)
755 (shadow-save-todo-file)
756 (and (or (not (memq t (mapcar (function
757 (lambda (buf) (and (buffer-file-name buf)
758 (buffer-modified-p buf))))
759 (buffer-list))))
760 (yes-or-no-p "Modified buffers exist; exit anyway? "))
761 (or (not (fboundp 'process-list))
7c2fb837 762 ;; process-list is not defined on MSDOS.
bb5d4e1a
RS
763 (let ((processes (process-list))
764 active)
765 (while processes
41c4dfe1
KS
766 (and (memq (process-status (car processes)) '(run stop open listen))
767 (process-query-on-exit-flag (car processes))
bb5d4e1a
RS
768 (setq active t))
769 (setq processes (cdr processes)))
770 (or (not active)
771 (yes-or-no-p "Active processes exist; kill them and exit anyway? "))))
772 (kill-emacs)))
773
774;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
36fd8e17 775;;; Lucid Emacs compatibility
bb5d4e1a
RS
776;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
777
191b14ba
RS
778;; This is on hold until someone tells me about a working version of
779;; map-ynp for Lucid Emacs.
780
5a7b9024 781;(when (string-match "Lucid" emacs-version)
191b14ba
RS
782; (require 'symlink-fix)
783; (require 'ange-ftp)
784; (require 'map-ynp)
785; (if (not (fboundp 'file-truename))
36fd8e17 786; (fset 'shadow-expand-file-name
191b14ba
RS
787; (symbol-function 'symlink-expand-file-name)))
788; (if (not (fboundp 'ange-ftp-ftp-name))
789; (fset 'ange-ftp-ftp-name
5354cb6d 790; (symbol-function 'ange-ftp-ftp-name))))
bb5d4e1a
RS
791
792;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
793;;; Hook us up
794;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
795
36fd8e17 796;;;###autoload
bb5d4e1a 797(defun shadow-initialize ()
36fd8e17
DL
798 "Set up file shadowing."
799 (interactive)
bb5d4e1a
RS
800 (if (null shadow-homedir)
801 (setq shadow-homedir
802 (file-name-as-directory (shadow-expand-file-name "~"))))
803 (if (null shadow-info-file)
36fd8e17 804 (setq shadow-info-file
b6489a43 805 (shadow-expand-file-name (convert-standard-filename "~/.shadows"))))
bb5d4e1a 806 (if (null shadow-todo-file)
36fd8e17 807 (setq shadow-todo-file
b6489a43
EZ
808 (shadow-expand-file-name
809 (convert-standard-filename "~/.shadow_todo"))))
bb5d4e1a
RS
810 (if (not (shadow-read-files))
811 (progn
812 (message "Shadowfile information files not found - aborting")
813 (beep)
814 (sit-for 3))
5a7b9024
GM
815 (when (and (not shadow-inhibit-overload)
816 (not (fboundp 'shadow-orig-save-buffers-kill-emacs)))
36fd8e17
DL
817 (defalias 'shadow-orig-save-buffers-kill-emacs
818 (symbol-function 'save-buffers-kill-emacs))
819 (defalias 'save-buffers-kill-emacs 'shadow-save-buffers-kill-emacs))
32f389a4 820 (add-hook 'write-file-functions 'shadow-add-to-todo)
bb5d4e1a
RS
821 (define-key ctl-x-4-map "s" 'shadow-copy-files)))
822
32f389a4
JB
823(defun shadowfile-unload-function ()
824 (substitute-key-definition 'shadow-copy-files nil ctl-x-4-map)
825 (when (fboundp 'shadow-orig-save-buffers-kill-emacs)
826 (fset 'save-buffers-kill-emacs
827 (symbol-function 'shadow-orig-save-buffers-kill-emacs)))
828 ;; continue standard unloading
829 nil)
a6d23706 830
36fd8e17
DL
831(provide 'shadowfile)
832
bb5d4e1a 833;;; shadowfile.el ends here