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