Initial Commit
[packages] / xemacs-packages / xemacs-devel / patcher.el
1 ;;; patcher.el --- Utility for mailing patch information
2
3 ;; Copyright (C) 2008, 2009, 2010 Didier Verna
4 ;; Copyright (C) 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2007 Didier Verna
5
6 ;; Author:        Didier Verna <didier@xemacs.org>
7 ;; Maintainer:    Didier Verna <didier@xemacs.org>
8 ;; Created:       Tue Sep 28 18:12:43 1999
9 ;; Last Revision: Wed Feb 10 18:01:21 2010
10 ;; Keywords:      maint
11
12
13 ;; This file is part of Patcher.
14
15 ;; Patcher is free software; you can redistribute it and/or modify
16 ;; it under the terms of the GNU General Public License version 2,
17 ;; as published by the Free Software Foundation.
18
19 ;; Patcher is distributed in the hope that it will be useful,
20 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
21 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22 ;; GNU General Public License for more details.
23
24 ;; You should have received a copy of the GNU General Public License
25 ;; along with this program; if not, write to the Free Software
26 ;; Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
27
28
29 ;;; Commentary:
30
31 ;; Contents management by FCM version 0.1.
32
33 ;; Patcher is an XEmacs package designed to automate and ease the
34 ;; maintenance of archive-based projects.  It provides assistance in
35 ;; building, reporting and committing patches, as well as in handling the
36 ;; corresponding ChangeLog entries, for example by creating skeletons.
37
38 ;; Patcher is fully documented.  Please refer to the documentation for
39 ;; information on how to use it.  All user options can be found in the
40 ;; "Patcher" Custom group.
41
42
43 ;; Suggestions for further improvements:
44
45 ;; #### Check the exit code of processes.
46
47 ;; #### Add a patcher-aux-buffer minor mode in every auxiliary buffer with
48 ;; convenient keystrokes, for example one for returning to the main (mail)
49 ;; buffer.
50
51 ;; #### When the commit command is displayed, it would be nive to also see a
52 ;; list of the files involved, for information.
53
54 ;; #### Replace the ChangeLogs "loaded" flag by a proper refcount, so that
55 ;; several Patcher instances can work in parallel.
56 ;; kill-change-logs-after-sending and removal of after-save hooks can then
57 ;; play on this refcount.
58
59 ;; #### Add support for interactive RCSes (hmpf !). For instance, it would be
60 ;; neat to answer darcs questions interactively, and then construct the patch
61 ;; and the changelogs only for the selected patches.
62
63 ;; #### Add an "identity" project option (more than just the
64 ;; change-logs-user-[name|mail]. Some RCSes like hg may use it for commit
65 ;; information.
66
67 ;; #### Investigate on the notion of adding new files (it's different across
68 ;; RCSes).
69
70 ;; #### If the user answers `no' to the confirm commit question, it should be
71 ;; possible to edit manually the computed commit command.
72
73 ;; #### The subject-related strings could benefit from almost all %
74 ;; constructs.
75
76 ;; #### Provide a way to attach patches instead of inserting them as plain
77 ;; text.
78
79 ;; #### Provide a way to filter out some files from the mailed patch.  For
80 ;; instance, when you commit yourself, you want to have `configure' in your
81 ;; files, but you don't want to display a patch against it.
82
83 ;; #### Before sending the message, we could check that the contents is ok
84 ;; (like, there's no more diff errors and stuff).
85
86 ;; #### Implement a real error / warning mechanism.
87
88 ;; #### When a project is found to be out of date, we could implement
89 ;; something to update it and re-run patcher again.
90
91 ;; #### For the 'gnus mail method, add the possibility to temporarily use a
92 ;; different one if the user don't want to start Gnus.
93 ;;
94 ;; #### Consider doing a better job of handling overlapping patcher
95 ;; instances. e.g. use a different extent property for each instance,
96 ;; and keep a usage count for the ChangeLog files so that they're only
97 ;; killed when no instance uses them any more.
98 ;;
99 ;; #### Also add an option to kill, not just bury, the mail message when
100 ;; it's sent.
101 ;;
102 ;; #### When the message is sent, the cvs commit results window should be
103 ;; removed and the corresponding buffer buried.
104 ;;
105 ;; #### Have a control window to clarify the progress of everything.
106 ;; Make it perhaps a small buffer, above the mail message/ChangeLog
107 ;; buffer.  It shows what steps have been completed, what haven't, and
108 ;; what to do.  It should have buttons in it for the various actions.
109 ;; One button is for include-changelogs, one for commit, one for send
110 ;; the mail, and one for execute the commit.  These should be enabled
111 ;; or grayed out appropriately.  It should also have buttons that show
112 ;; the associated changelogs; clicking on a button puts that changelog
113 ;; buffer in the main window below the control window.  By each
114 ;; changelog button should be an indication of whether the changelog
115 ;; has been modified so far.  The control window should stay around as
116 ;; much as it can during the whole process (e.g. use
117 ;; set-buffer-dedicated-p or something), so that it's always there to
118 ;; drive the whole process.  One corollary is that you don't actually
119 ;; have to switch to the mail buffer to (e.g.) execute
120 ;; include-changelogs -- you just click on the control window, and it
121 ;; does it automatically.  also, when you execute include-changelogs,
122 ;; it can issue a warning if not all changelogs have been visited, and
123 ;; prompt you to make sure you want to continue.  Similarly, you can
124 ;; run include-changelogs more than once even if it succeeded the
125 ;; first time (e.g. for some reason you didn't like the result and
126 ;; deleted it), but it will prompt "are you sure?".  There could also
127 ;; be an "undo include-changelogs", if you make a mistake after doing
128 ;; include-changelogs and realize you want to go back and fix the
129 ;; problem and do include-changelogs again.
130
131
132
133 ;; Internal notes:
134
135 ;; - See why the ChangeLogs are represented in absolute path instead of
136 ;; relative to the project's directory. I can't remember, but this looks
137 ;; weird.
138
139
140
141 ;; Thanks to these people for their suggestions, testing and contributions:
142 ;; Adrian Aichner, Ben Wing, Karl Pflasterer, Malcolm Purvis, Norbert Koch,
143 ;; Raphael Poss, Stephen J. Turnbull, Steve Youngs.
144
145
146 ;;; Code:
147
148 ;; Require 'sendmail for getting `mail-header-separator'.
149 ;; #### Now that a fake mail sending function exists, sendmail shouldn't be
150 ;; #### systematically required like this.  However, since most users will
151 ;; #### really want do send real messages, it probably doesn't hurt to keep
152 ;; #### things as-is.
153 (require 'sendmail)
154
155
156 ;; These macros are copied from bytecomp-runtime.el because they're only
157 ;; available in XEmacs 21.5.
158
159 (defmacro patcher-globally-declare-fboundp (symbol)
160   (when (cl-compiling-file)
161     (setq symbol (eval symbol))
162     (if (not (consp symbol))
163         (setq symbol (list symbol)))
164     ;; Another hack.  This works because the autoload environment is
165     ;; currently used ONLY to suppress warnings, and the actual
166     ;; autoload definition is not used.  (NOTE: With this definition,
167     ;; we will get spurious "multiple autoloads for %s" warnings if we
168     ;; have an autoload later in the file for any functions in SYMBOL.
169     ;; This is not something that code should ever do, though.)
170     (setq byte-compile-autoload-environment
171           (append (mapcar #'(lambda (sym) (cons sym nil)) symbol)
172                   byte-compile-autoload-environment)))
173   nil)
174
175 (defmacro patcher-globally-declare-boundp (symbol)
176   (setq symbol (eval symbol))
177   (if (not (consp symbol))
178       (setq symbol (list symbol)))
179   `(progn
180      ;; (defvar FOO) has no side effects.
181      ,@(mapcar #'(lambda (sym) `(defvar ,sym)) symbol)))
182
183
184 (patcher-globally-declare-boundp 'font-lock-always-fontify-immediately)
185
186
187
188 ;; ===========================================================================
189 ;; Version management
190 ;; ===========================================================================
191
192 (defconst patcher-version "3.11"
193   "Current version of Patcher.")
194
195 ;;;###autoload
196 (defun patcher-version ()
197   "Show the current version of Patcher."
198   (interactive)
199   (message "Patcher version %s" patcher-version))
200
201
202
203 ;; ===========================================================================
204 ;; General utilities
205 ;; ===========================================================================
206
207 (defsubst patcher-message (msg &rest args)
208   ;; Print a message, letting XEmacs time to display it.  Also, handle command
209   ;; substitution.
210   (message (substitute-command-keys (apply 'format msg args)))
211   (save-current-buffer
212     ;; sit-for may change the current buffer and we don't want that.
213     (sit-for 0)))
214
215 (defsubst patcher-warning (msg &rest args)
216   ;; Like `patcher-message, but triggers a Patcher warning instead.
217   (warn (substitute-command-keys (apply 'format msg args))))
218
219 (defsubst patcher-error (msg &rest args)
220   ;; Like `patcher-message, but triggers a Patcher error instead.
221   (error (substitute-command-keys (apply 'format msg args))))
222
223 (defmacro patcher-with-progression (msg &rest body)
224   ;; wrap BODY in "msg..." / "msg...done" messages.
225   ;; Return the value of BODY execution.
226   `(prog2
227        (patcher-message (concat ,msg "... please wait."))
228        (progn ,@body)
229      (patcher-message (concat ,msg "... done."))))
230 (put 'patcher-with-progression 'lisp-indent-function 1)
231
232
233 (defsubst patcher-file-relative-name (file &optional dir raw)
234   ;; Construct a filename from FILE relative to DIR (default directory if not
235   ;; given).  Unless RAW is given, force unix syntax
236   ;; #### NOTE: FILE may be either a string (a file name), or a list such as
237   ;; #### patcher-change-logs elements, whose car must then be the file name
238   ;; #### in question.
239   (and (listp file) (setq file (car file)))
240   (let ((directory-sep-char (if raw directory-sep-char ?/)))
241     (or dir (setq dir default-directory))
242     (file-relative-name (expand-file-name file (expand-file-name dir))
243                         (expand-file-name dir))))
244
245 (defsubst patcher-files-string (files)
246   ;; Convert FILES to a string of relative file names.  Unless RAW is given,
247   ;; force unix syntax.
248   ;; #### NOTE: FILES may be either a list of strings (file names), or a list
249   ;; #### such as the one in patcher-change-logs.
250   (mapconcat 'patcher-file-relative-name  files " "))
251
252 (defun patcher-files-buffers (files &optional find)
253   ;; Find a buffer visiting each file in FILES, and return a list of
254   ;; corresponding buffers.  Skip files that are not visited, unless optional
255   ;; argument FIND is non nil.  In that case, visit the file.
256   ;; #### NOTE: FILES may be either a list of strings (file names), or a list
257   ;; #### such as the one in patcher-change-logs.
258   (let (buffer buffers)
259     (dolist (file files buffers)
260       (and (listp file) (setq file (car file)))
261       (setq buffer (or (get-file-buffer file)
262                        (and find (find-file-noselect file))))
263       (when buffer (push buffer buffers)))))
264
265 (defun patcher-save-buffers (buffers)
266   ;; Offer to save some buffers.
267   ;; #### FIXME: this should be a standard function somewhere.
268   (map-y-or-n-p
269    (lambda (buffer)
270      (and (buffer-modified-p buffer)
271           (not (buffer-base-buffer buffer))
272           (buffer-file-name buffer)
273           (format "Save file %s? "
274                   (buffer-file-name buffer))))
275    (lambda (buffer)
276      (save-excursion
277        (set-buffer buffer)
278        (condition-case ()
279            (save-buffer)
280          (error nil))))
281    buffers
282    '("buffer" "buffers" "save")))
283
284 (defun patcher-goto-subject ()
285   ;; Move point to the beginning of the Subject: header's contents, if any.
286   ;; Return that position, or nil.
287   ;; #### FIXME: maybe we should issue a warning if no subject line is found ?
288   (let (pos)
289     (save-excursion
290       (goto-char (point-min))
291       (setq pos (re-search-forward "^Subject: " nil t)))
292     (and pos (goto-char pos))))
293
294 (patcher-globally-declare-boundp
295  '(message-signature-separator))
296
297 (defun patcher-goto-signature ()
298   ;; Move point to the beginning of the mail signature (actually, in front of
299   ;; the signature separator), if any.  Otherwise, move point to the end of
300   ;; the message. Return that position.
301   (goto-char (point-min))
302   (if (re-search-forward
303        (cond ((eq major-mode 'mail-mode)
304               ;; this is hard-wired in sendmail.el
305               "\n\n-- \n")
306              ((eq major-mode 'message-mode)
307               message-signature-separator)
308              (t
309               (patcher-warning "\
310 Major mode: %s.
311 Your mailing method is not fully supported by Patcher.
312 This is not critical though: Patcher may not find the message signature
313 correctly.
314
315 Please report to <didier@xemacs.org>."
316                                major-mode)
317               ;; Use the standard one by default.
318               "\n\n-- \n"))
319        nil t)
320       (goto-char (match-beginning 0))
321     ;; else: no signature
322     (goto-char (point-max))))
323
324
325
326 ;; ===========================================================================
327 ;; Projects description
328 ;; ===========================================================================
329
330 (defgroup patcher nil
331   "Automatic archive-base project maintenance.")
332
333 (defgroup patcher-default nil
334   "Default settings for Patcher project options."
335   :group 'patcher)
336
337 (defcustom patcher-default-name nil
338   "*Default name for Patcher projects.
339
340 This project option (a string) exists to let you define different Patcher
341 projects (hence with different names) sharing a common name for the
342 underlying diff and commit commands.  If set, it will be used rather than
343 the real project's name."
344   :group 'patcher-default
345   :type '(choice (const :tag "Patcher name" nil)
346                  (string :tag "Other name")))
347
348 (defcustom patcher-default-mail-method 'compose-mail
349   "*Default method used by Patcher to prepare a mail.
350
351 Currently, there are four built-in methods: 'compose-mail \(the default),
352 'sendmail, 'message, 'gnus and 'fake.  Please refer to the corresponding
353 `patcher-mail-*' function for a description of each method.
354
355 You can also define your own method, say `foo'.  In that case, you *must*
356 provide a function named `patcher-mail-foo' which takes two arguments: a
357 project descriptor and a string containing the subject of the message.
358 This function must prepare a mail buffer.  If you want to do this, please
359 see how it's done for the built-in methods."
360   :group 'patcher-default
361   :type '(radio (const compose-mail)
362                 (const sendmail)
363                 (const message)
364                 (const gnus)
365                 (const fake)
366                 (symbol :tag "other")))
367
368 (defcustom patcher-default-user-name nil
369   "*Default user full name to use when sending a Patcher mail.
370
371 If nil, `user-full-name' is used."
372   :group 'patcher-default
373   :type  '(choice (const :tag "user-full-name" nil)
374                   string))
375
376 (defcustom patcher-default-user-mail nil
377   "*Default user mail address to use when sending a Patcher mail.
378
379 If nil, `user-mail-address' is used."
380   :group 'patcher-default
381   :type '(choice (const :tag "user-mail-address" nil)
382                   string))
383
384 (defcustom patcher-default-to-address nil
385   "*Default To: header value to use when sending a Patcher mail.
386
387 This variable is used by all mail methods except the 'gnus one \(see
388 `patcher-default-mail-method').  If nil, it is prompted for."
389   :group 'patcher-default
390   :type '(choice (const :tag "Ask" nil)
391                  string))
392
393 (defcustom patcher-default-gnus-group nil
394   "*Default Gnus group to use when sending a Patcher mail.
395
396 This variable is used only in the 'gnus mail method \(see
397 `patcher-default-mail-method').  The mail sending process will behave as if
398 you had typed `C-u a' in the group buffer on that Gnus group.  If nil,
399 it is prompted for."
400   :group 'patcher-default
401   :type  '(choice (const :tag "Ask" nil)
402                   string))
403
404 (defcustom patcher-default-subject-prefix "[PATCH]"
405   "*Default prefix for the subject of Patcher mails.
406
407 The following string transformations are performed:
408 - %n: the value of the :name project option if set, or the project's name
409       in the Patcher sense.
410 - %N: the project's name in the Patcher sense.
411
412 A space will be inserted between the prefix and the rest of the subject,
413 as appropriate.  This part of the subject is never prompted for.  See
414 also `patcher-default-subject' and
415 `patcher-default-subject-committed-prefix'."
416   :group 'patcher-default
417   :type '(choice (const :tag "None" nil)
418           string))
419
420 (defcustom patcher-default-subject-committed-prefix "[COMMIT]"
421   "*Default prefix for the subject of Patcher mails.
422
423 Same as `patcher-default-subject-prefix', but for committed patches.
424 If nil, keep the normal subject prefix."
425   :group 'patcher-default
426   :type '(choice (const :tag "Don't change" nil)
427                  string))
428
429 (defcustom patcher-default-subject nil
430   "*Default subject for Patcher mails.
431
432 The following string transformations are performed:
433 - %n: the value of the :name project option if set, or the project's name
434       in the Patcher sense.
435 - %N: the project's name in the Patcher sense.
436
437 Please note that this is used *only* to provide a default value for prompted
438 subjects.  Subjects are *always* prompted for.
439
440 See also `patcher-default-subject-prefix' and
441 `patcher-default-subject-committed-prefix', which are not subject to
442 prompting."
443   :group 'patcher-default
444   :type '(choice (const :tag "None" nil)
445           string))
446
447 (defcustom patcher-default-mail-prologue nil
448   "*Default prologue for every Patcher mail."
449   :group 'patcher-default
450   :type '(choice (const :tag "None" nil)
451           string))
452
453 (defcustom patcher-default-change-logs-updating 'automatic
454   "*Default ChangeLog updating mode.
455
456 Possible values and their meaning are:
457 - 'automatic: \(the default) Patcher generates ChangeLog skeletons
458    automatically based on the created diff (you then have to fill up the
459    entries as needed).
460 - 'manual: you are supposed to have updated the ChangeLog files by hand,
461    prior to calling Patcher.
462 -  nil: you don't do ChangeLogs at all."
463   :group 'patcher-default
464   :type '(radio (const :tag "Automatic" automatic)
465                 (const :tag "Manual" manual)
466                 (const :tag "None" nil)))
467
468 (defcustom patcher-default-change-logs-user-name nil
469   "*Default user full name for generated ChangeLog entries.
470
471 If nil, let `patch-to-change-log' decide what to use.
472 Otherwise, it should be a string."
473   :group 'patcher-default
474   :type '(choice (const :tag "Default" nil)
475                  (string :tag "Other name")))
476
477 (defcustom patcher-default-change-logs-user-mail nil
478   "*Default user mail address for generated ChangeLog entries.
479
480 If nil, let `patch-to-change-log' decide what to use.
481 Otherwise, it should be a string."
482   :group 'patcher-default
483   :type '(choice (const :tag "Default" nil)
484                  (string :tag "Other mail")))
485
486 (defcustom patcher-default-change-logs-appearance 'verbatim
487   "*Default appearance of ChangeLog entries in Patcher mails.
488
489 The values currently supported are:
490 - 'verbatim \(the default): ChangeLog entries appear simply as text above
491    the patch.  A short line mentioning the ChangeLog file they belong to
492    is added when necessary.
493 - 'packed: ChangeLog files are diff'ed, but the output is packed above the
494    rest of the patch.
495 - 'patch: ChangeLog files are diff'ed, and the output appears as part of
496    the patch itself.
497 -  nil: ChangeLog entries don't appear at all.
498
499 See also the `patcher-default-change-logs-diff-command' user option."
500   :group 'patcher-default
501   :type '(radio (const :tag "Verbatim" verbatim)
502                 (const :tag "Diff, packed together" packed)
503                 (const :tag "Diff, part of the patch" patch)
504                 (const :tag "Don't appear in message" nil)))
505
506 (defcustom patcher-default-change-logs-prologue "%f addition:"
507   "*Default ChangeLogs prologue for every Patcher mail.
508
509 This applies when ChangeLog additions appear verbatim in the message.
510 A %f occurring in this string will be replaced with the ChangeLog file name
511 \(relative to the project's directory)."
512   :group 'patcher-default
513   :type '(choice (const :tag "None" nil)
514                  string))
515
516 (defcustom patcher-default-diff-prologue-function
517   'patcher-default-diff-prologue
518   "*Default function used to insert a prologue before each diff output.
519
520 Insertion must occur at current point in current buffer.
521 This function should take one argument indicating the kind of diff:
522 - a value of 'sources indicates a source diff only,
523 - a value of 'change-logs indicates a ChangeLog diff only,
524 - a value of 'mixed indicates a diff on both source and ChangeLog files.
525
526 The following variables are bound (when appropriate) when this function
527 is executed:
528 - `name': the name of the current Patcher project,
529 - `source-diff': the command used to create a source diff,
530 - `change-log-diff': the command used to create a ChangeLog diff,
531 - `source-files': sources files affected by the current patch,
532 - `change-log-files': ChangeLog files affected by the current patch.
533
534 In the case of a 'mixed diff, a nil value for `change-log-diff' indicates
535 that the same command was used for both the source and ChangeLog files."
536   :group 'patcher-default
537   :type '(choice (const :tag "Default" patcher-default-diff-prologue)
538                  (const :tag "None" nil)
539                  (symbol :tag "Other")))
540
541 (defcustom patcher-default-command-directory nil
542   "*Default command directory for Patcher projects.
543
544 This directory (a string) can be relative to the project's directory.
545 All diff and commit commands are executed from this directory if set.
546 Otherwise, the project's directory is used."
547   :group 'patcher-default
548   :type  '(choice (const :tag "Same directory" nil)
549                   (string :tag "Other directory")))
550
551
552 (defcustom patcher-default-pre-command ""
553   "*Default string to prefix patcher commands with.
554
555 This is where you would put things like \"runsocks\"."
556   :group 'patcher-default
557   :type '(choice (const :tag "None" nil)
558                  string))
559
560 (defcustom patcher-default-diff-command nil
561   "*Default method used by Patcher to generate a patch.
562
563 The following string transformations are performed:
564 - %n: the value of the :name project option if set, or the project's name
565       in the Patcher sense.
566 - %N: the project's name in the Patcher sense.
567 - %f: the files affected by the patch.  These files can be specified by
568       using `patcher-mail-subproject' instead of `patcher-mail' to prepare
569       the patch.  Otherwise, the %f will simply be removed."
570   :group 'patcher-default
571   :type '(choice (const :tag "None" nil)
572                  string))
573
574 (defcustom patcher-default-diff-cleaner 'patcher-generic-diff-cleaner
575   "*Default function used for cleaning up a diff.
576
577 This function is used to transform RCS-specific diff outputs into
578 something more standard, that `patch-to-change-log' can handle."
579   :group 'patcher-default
580   :type '(choice (const :tag "None" nil)
581           function))
582
583 (defcustom patcher-default-diff-header nil
584   "*Default diff header used by Patcher to determine the diff'ed file.
585
586 This variable is of the form (REGEXP . NUMBER). REGEXP is used to match
587 the beginning of a diff output, and NUMBER is the parenthesized level in
588 which to find the file name.
589
590 The default value is suitable for a Unix unified diff command output,
591 although file names with spaces are not supported."
592   :group 'patcher-default
593   :type '(choice (const :tag "None" nil)
594                  (list regexp
595                        (integer :tag "Old file match number")
596                        (integer :tag "New file match number"))))
597
598 (defcustom patcher-default-after-diff-hook nil
599   "*Default hook run on the output of a Patcher diff comand.
600
601 The functions in this hook should operate on the current buffer and take
602 two optional arguments limiting the processing to a buffer region
603 \(in the absence of arguments, the whole buffer should be processed).
604
605 When each function in this hook is run, the point is placed at the
606 beginning of the region, and the buffer excursion is saved for you."
607   :group 'patcher-default
608   :type 'hook)
609
610 (defcustom patcher-default-notice-change-log-hook nil
611   "*Default hook run every time Patcher notices a new ChangeLog file.
612
613 Noticing a ChangeLog file in this context means figuring out that it is
614 involved in the current patch. Every function in this hook hook will be
615 given the ChangeLog file name, relative to the project's directory, as
616 argument."
617   :group 'patcher-default
618   :type 'hook)
619
620 (defcustom patcher-default-after-save-change-log-hook nil
621   "*Default hook run after a ChangeLog file is saved.
622
623 The functions in this hook are executed in the ChangeLog's buffer."
624   :group 'patcher-default
625   :type 'hook)
626
627 (defcustom patcher-default-diff-line-filter nil
628   "*Default line filter to pass Patcher diffs through.
629
630 When inserting a diff in Patcher mails, lines matching this regexp will
631 be excluded.
632
633 Note: the regexp must match the whole line.  Don't add beginning and end
634 of line markers to it, Patcher will do this for you.
635
636 A value of nil (the default) means no line filter."
637   :group 'patcher-default
638   :type '(choice (const :tag "None" nil)
639                  regexp))
640
641 (defcustom patcher-default-change-logs-diff-command 'diff
642   "*Default command to use to generate ChangeLog diffs.
643
644 This value is used when the ChangeLog appearance is either 'packed or
645 'patch (see the variable `patcher-default-change-logs-appearance').
646
647 If set to 'diff (the default), use the same command as for the rest of the
648 patch. Otherwise, it should be a string.
649
650 The following string transformations are performed:
651 - %n: the value of the :name project option if set, or the project's name
652       in the Patcher sense.
653 - %N: the project's name in the Patcher sense.
654 - %f: the ChangeLog filenames.
655
656 Note: it is highly recommended to remove the context from ChangeLog diffs
657 because they often fail to apply correctly."
658   :group 'patcher-default
659   :type '(choice (const  :tag "Project diff command" diff)
660                  (string :tag "Other diff command" "cvs -q diff -U 0 %f")))
661
662 (defcustom patcher-default-commit-privilege nil
663   "*Default value for Patcher commit privilege status.
664
665 If you have the privilege to commit patches yourself, you should set
666 this option to t."
667   :group 'patcher-default
668   :type 'boolean)
669
670 (defcustom patcher-default-commit-command nil
671   "*Default method used by Patcher to commit a patch.
672
673 The following string transformations are performed:
674 - %n: the value of the :name project option if set, or the project's name
675       in the Patcher sense.
676 - %N: the project's name in the Patcher sense.
677 - %s: the name of a file containing the commit log message.
678 - %S: the commit log message itself (quoted to prevent shell expansion).
679 - %f: the files affected by the patch.  These files can be specified by using
680      `patcher-mail-subproject' instead of `patcher-mail' to prepare the patch.
681       Otherwise, the %f will simply be removed.
682
683 - %?f{xxx}: this construct is an \"if %f\" form: if %f expands to something,
684             this construct expands to `xxx'. Otherwise, its value is
685             discarded. See the `git' built-in themes for an example of use
686             (in `patcher-built-in-themes').
687 - %!f{xxx}: this construct is an \"if not %f\" form: if %f expands to nothing,
688             this construct expands to `xxx'. Otherwise, its value is
689             discarded. See the `git' built-in themes for an example of use
690             (in `patcher-built-in-themes')."
691   :group 'patcher-default
692   :type '(choice (const :tag "None" nil)
693                  string))
694
695 (defcustom patcher-default-confirm-commits t
696   "*Whether Patcher asks for a confirmation before doing a commit by default."
697   :group 'patcher-default
698   :type 'boolean)
699
700 (defcustom patcher-default-committed-notice
701   "NOTE: This patch has been committed."
702   "*Default notice added to a mail after a commit."
703   :group 'patcher-default
704   :type '(choice (const :tag "None" nil)
705                  string))
706
707 (defcustom patcher-default-failed-command-regexp nil
708   "*Default regular expression for matching the result of a failed command.
709
710 Commands in question are the diff and the commit one."
711   :group 'patcher-default
712   :type '(choice (const :tag "None" nil)
713                  regexp))
714
715 (defcustom patcher-default-log-message-items '(subject)
716   "*Default elements used to initialize a Patcher commit log message.
717
718 This is nil, or a list of the following items:
719 - 'subject: the subject of the corresponding Patcher mail (sans the prefix),
720 - 'compressed-change-logs: the compressed ChangeLog entries for the current
721    patch.
722 - 'change-logs: the ChangeLog entries for the current patch.  If some items
723    appear before the ChangeLog entries, the ChangeLogs separator will
724    automatically be included."
725   :group 'patcher-default
726   :type '(set (const :tag "Subject" subject)
727               (const :tag "Compressed ChangeLogs" compressed-change-logs)
728               (const :tag "ChangeLogs" change-logs)))
729
730 (defcustom patcher-default-change-logs-separator
731   "-------------------- ChangeLog entries follow: --------------------"
732   "*Default ChangeLog entries separator for Patcher commit log messages.
733
734 Either nil, or a string which will be inserted in a line of its own.
735
736 See also the function `patcher-logmsg-insert-change-logs'."
737   :group 'patcher-default
738   :type '(choice (const :tag "None" nil)
739                  string))
740
741 (defcustom patcher-default-edit-log-message t
742   "*Whether Patcher lets you edit the commit log message by default.
743
744 If nil, Patcher will directly use the initialization value \(see
745 `patcher-default-init-log-message')."
746   :group 'patcher-default
747   :type 'boolean)
748
749 (defcustom patcher-default-kill-source-files-after-sending t
750   "*Whether to kill source files after sending the mail be default.
751
752 This is effective only when sources files have not been killed already
753 \(see the variable `patcher-default-kill-source-files-after-diffing').
754
755 That feature is not implemented yet."
756   :group 'patcher-default
757   :type 'boolean)
758
759 (defcustom patcher-default-kill-change-logs-after-sending t
760   "*Whether to kill the ChangeLog files after sending the mail by default."
761   :group 'patcher-default
762   :type 'boolean)
763
764 (defcustom patcher-default-kill-source-files-after-diffing t
765   "*Whether to kill source files after building the ChangeLog skeletons
766 by default.
767
768 These files are loaded temporarily by `patch-to-change-log'.  If this
769 variable is non nil, `patch-to-change-log' will be instructed to remove
770 them when they are not needed anymore.
771
772 See also the variable `patcher-default-kill-source-files-after-sending'."
773   :group 'patcher-default
774   :type 'boolean)
775
776 (defcustom patcher-default-themes nil
777   "*Default themes to use in Patcher projects.
778
779 This is a list of theme names (symbols) that must be defined either
780 in `patcher-themes' or `patcher-built-in-themes'."
781   :group 'patcher-default
782   ;; #### NOTE: ideally, this type should be computed automatically, depending
783   ;; #### on the defined themes.  This arises the interesting question of
784   ;; #### custom dynamic types.  Without them, it's a complex thing to do.
785   :type '(repeat (symbol :tag "Theme name")))
786
787
788 ;; Defining these const avoids coding special cases for the :inheritance,
789 ;; :subdirectory, :files and :command-directory (sub)project option in the
790 ;; accessor functions.
791 (defconst patcher-default-inheritance nil)
792 (defconst patcher-default-subdirectory nil)
793 (defconst patcher-default-files nil)
794
795 (defconst patcher-project-options-custom-type
796   '((list :inline t :tag "Project name"
797           :format "%{%t%}: %v"
798           (const :tag "" :value :name)
799           (choice (const :tag "Patcher name" nil)
800                   (string :tag "Other name")))
801     (list :inline t :tag "Mail method"
802           :format "%{%t%}: %v"
803           (const :tag "" :value :mail-method)
804           (choice (const compose-mail)
805                   (const sendmail)
806                   (const message)
807                   (const gnus)
808                   (const fake)
809                   (symbol :tag "other")))
810     (list :inline t :tag "User name"
811           :format "%{%t%}: %v"
812           (const  :tag "" :value :user-name)
813           (choice (const :tag "user-full-name" nil)
814                   string))
815     (list :inline t :tag "User mail"
816           :format "%{%t%}: %v"
817           (const  :tag "" :value :user-mail)
818           (choice (const :tag "user-mail-address" nil)
819                   string))
820     (list :inline t :tag "To address"
821           :format "%{%t%}: %v"
822           (const  :tag "" :value :to-address)
823           (choice (const :tag "Ask" nil)
824                   string))
825     (list :inline t :tag "Gnus group"
826           :format "%{%t%}: %v"
827           (const  :tag "" :value :gnus-group)
828           (choice (const :tag "Ask" nil)
829                   string))
830     (list :inline t :tag "Subject prefix"
831           :format "%{%t%}: %v"
832           (const  :tag "" :value :subject-prefix)
833           (choice (const :tag "None" nil)
834                   string))
835     (list :inline t :tag "Subject committed prefix"
836           :format "%{%t%}: %v"
837           (const  :tag "" :value :subject-committed-prefix)
838           (choice (const :tag "None" nil)
839                   string))
840     (list :inline t :tag "Subject"
841           :format "%{%t%}: %v"
842           (const  :tag "" :value :subject)
843           (choice (const :tag "None" nil)
844                   string))
845     (list :inline t :tag "Mail prologue"
846           :format "%{%t%}: %v"
847           (const :tag "" :value :mail-prologue)
848           (choice (const :tag "None" nil)
849                   string))
850     (list :inline t :tag "ChangeLogs updating"
851           :format "%{%t%}: %v"
852           (const :tag "" :value :change-logs-updating)
853           (choice (const :tag "Automatic" automatic)
854                   (const :tag "Manual" manual)
855                   (const :tag "None" nil)))
856     (list :inline t :tag "ChangeLogs user name"
857           :format "%{%t%}: %v"
858           (const :tag "" :value :change-logs-user-name)
859           (choice (const :tag "Default" nil)
860                   (string :tag "Other name")))
861     (list :inline t :tag "ChangeLogs user mail"
862           :format "%{%t%}: %v"
863           (const :tag "" :value :change-logs-user-mail)
864           (choice (const :tag "Default" nil)
865                   (string :tag "Other mail")))
866     (list :inline t :tag "ChangeLogs appearance"
867           :format "%{%t%}: %v"
868           (const :tag "" :value :change-logs-appearance)
869           (choice (const :tag "Verbatim" verbatim)
870                   (const :tag "Diff, packed together" packed)
871                   (const :tag "Diff, part of the patch" patch)
872                   (const :tag "Don't appear" nil)))
873     (list :inline t :tag "ChangeLogs prologue"
874           :format "%{%t%}: %v"
875           (const :tag "" :value :change-logs-prologue)
876           (choice (const :tag "None" nil)
877                   string))
878     (list :inline t :tag "Diff prologue function"
879           :format "%{%t%}: %v"
880           (const :tag "" :value :diff-prologue-function)
881           (choice (const :tag "Default" patcher-default-diff-prologue)
882                   (const :tag "None" nil)
883                   (symbol :tag "Other")))
884     (list :inline t :tag "Command directory"
885           :format "%{%t%}: %v"
886           (const :tag "" :value :command-directory)
887           (choice (const :tag "Same directory" nil)
888                   (string :tag "Other directory")))
889     (list :inline t :tag "Pre command"
890           :format "%{%t%}: %v"
891           (const :tag "" :value :pre-command)
892           (choice (const :tag "None" nil)
893                   string))
894     (list :inline t :tag "Diff command"
895           :format "%{%t%}: %v"
896           (const :tag "" :value :diff-command)
897           ;; #### NOTE: nil forbidden.
898           string)
899     (list :inline t :tag "Diff cleaner"
900           :format "%{%t%}: %v"
901           (const :tag "" :value :diff-cleaner)
902           (choice (const :tag "None" nil)
903                   function))
904     (list :inline t :tag "Diff header"
905           :format "%{%t%}: %v"
906           (const :tag "" :value :diff-header)
907           ;; #### NOTE: nil forbidden.
908           (list regexp
909                 (integer :tag "Old file match number")
910                 (integer :tag "New file match number")))
911     ;; #### NOTE: this feature obsolete and not documented anymore.
912     (list :inline t :tag "After diff hook"
913           :format "%{%t%}: %v"
914           (const :tag "" :value :after-diff-hook)
915           hook)
916     (list :inline t :tag "Notice ChangeLog hook"
917           :format "%{%t%}: %v"
918           (const :tag "" :value :notice-change-log-hook)
919           hook)
920     (list :inline t :tag "After save ChangeLog hook"
921           :format "%{%t%}: %v"
922           (const :tag "" :value :after-save-change-log-hook)
923           hook)
924     (list :inline t :tag "Diff line filter"
925           :format "%{%t%}: %v"
926           (const :tag "" :value :diff-line-filter)
927           (choice (const :tag "None" nil)
928                   regexp))
929     (list :inline t :tag "ChangeLogs diff command"
930           :format "%{%t%}: %v"
931           (const :tag "" :value :change-logs-diff-command)
932           (choice (const :tag "Project diff command" diff)
933                   (string :tag "Other diff command" "cvs -q diff -U 0 %f")))
934     (list :inline t :tag "Commit privilege"
935           :format "%{%t%}: %v"
936           (const :tag "" :value :commit-privilege)
937           boolean)
938     (list :inline t :tag "Commit command"
939           :format "%{%t%}: %v"
940           (const :tag "" :value :commit-command)
941           ;; #### NOTE: nil forbidden.
942           string)
943     (list :inline t :tag "Confirm commits"
944           :format "%{%t%}: %v"
945           (const :tag "" :value :confirm-commits)
946           boolean)
947     (list :inline t :tag "Committed notice"
948           :format "%{%t%}: %v"
949           (const :tag "" :value :committed-notice)
950           (choice (const :tag "None" nil)
951                   string))
952     (list :inline t :tag "Failed command regexp"
953           :format "%{%t%}: %v"
954           (const :tag "" :value :failed-command-regexp)
955           (choice (const :tag "None" nil)
956                   regexp))
957     (list :inline t :tag "Log message items"
958           :format "%{%t%}: %v"
959           (const :tag "" :value :log-message-items)
960           (set (const :tag "Subject" subject)
961                (const :tag "Compressed ChangeLogs" compressed-change-logs)
962                (const :tag "ChangeLogs" change-logs)))
963     (list :inline t :tag "ChangeLogs separator"
964           :format "%{%t%}: %v"
965           (const :tag "" :value :change-logs-separator)
966           (choice (const :tag "None" nil)
967                   string))
968     (list :inline t :tag "Edit log message"
969           :format "%{%t%}: %v"
970           (const :tag "" :value :edit-log-message)
971           boolean)
972     (list :inline t
973           :tag "Kill source files after sending"
974           :format "%{%t%}: %v"
975           (const :tag "" :value :kill-source-files-after-sending)
976           boolean)
977     (list :inline t
978           :tag "Kill changeLogs after sending"
979           :format "%{%t%}: %v"
980           (const :tag "" :value :kill-change-logs-after-sending)
981           boolean)
982     (list :inline t
983           :tag "Kill source files after diffing"
984           :format "%{%t%}: %v"
985           (const :tag "" :value :kill-source-files-after-diffing)
986           boolean)
987     (list :inline t :tag "Themes"
988           :format "%{%t%}: %v"
989           (const :tag "" :value :themes)
990           ;; #### NOTE: ideally, this type should be computed automatically,
991           ;; #### depending on the defined themes.  This arises the
992           ;; #### interesting question of custom dynamic types.  Without them,
993           ;; #### it's a complex thing to do.
994           (repeat (symbol :tag "Theme name"))))
995   ;; This is currently useless, and would cause problems in the custom type:
996   ;; it will match the inheritance field in patcher-projects before the
997   ;; corresponding custom type definition.
998   ;;    (list :inline t :tag "Other"
999   ;;      symbol
1000   ;;      sexp))
1001   ;; Custom type elements for Patcher project options common to
1002   ;; `patcher-projects' and `patcher-subprojects'.
1003   )
1004
1005
1006 (defgroup patcher-themes nil
1007   "Theme settings for Patcher projects."
1008   :group 'patcher)
1009
1010 (defcustom patcher-themes '()
1011   "*List of themes to use in Patcher projects.
1012
1013 Each element looks like \(NAME :OPTION VALUE ...).  NAME is the theme
1014 name (a symbol).  The remainder of the list is the same as in project
1015 descriptors (see `patcher-projects').
1016
1017 Themes are searched for respectively in this variable and in
1018 `patcher-built-in-themes'.
1019
1020 See also `patcher-max-theme-depth'."
1021   :group 'patcher-themes
1022   :type `(repeat
1023           (group (symbol :tag "Theme name")
1024                  ;; #### NOTE: we could be tempted to add an `inheritance'
1025                  ;; #### mechanism for themes, just like for projects.
1026                  ;; #### However, don't forget that a theme can contain other
1027                  ;; #### themes because themes belong to
1028                  ;; #### `patcher-project-options-custom-type'.
1029                  (repeat :inline t :tag "Options"
1030                          (choice :inline t :value (:mail-method compose-mail)
1031                                  ,@patcher-project-options-custom-type)))))
1032
1033 (defconst patcher-built-in-themes
1034   '((git-index-ws
1035      :diff-command "git diff -b -w -M --no-prefix --cached%?f{ -- }%f"
1036      :change-logs-diff-command
1037      "git diff -b -w -U0 --no-prefix --cached%?f{ -- }%f"
1038      :themes (git-index ws))
1039     (git-index
1040      :notice-change-log-hook (patcher-git-intent-to-add)
1041      :after-save-change-log-hook (patcher-git-add)
1042      :diff-command "git diff -M --no-prefix --cached%?f{ -- }%f"
1043      :change-logs-diff-command "git diff -U0 --no-prefix --cached%?f{ -- }%f"
1044      :commit-command "git commit -F %s"
1045      :themes (git))
1046     (git-ws
1047      :diff-command "git diff -b -w --no-prefix HEAD%?f{ -- }%f"
1048      :change-logs-diff-command "git diff -b -w -U0 --no-prefix HEAD%?f{ -- }%f"
1049      :themes (git ws))
1050     (git
1051      :diff-command "git diff --no-prefix HEAD%?f{ -- }%f"
1052      :diff-header ("\
1053 ^diff .*\n\
1054 \\(\\(deleted file\\|new file\\).*\n\\)?\
1055 \\(similarity \\)?index .*\n\
1056 \\(--- \\|rename from \\)\\(\\S-+\\).*\n\
1057 \\(\\+\\+\\+ \\|rename to \\)\\(\\S-+\\).*"
1058                    5 7)
1059      :change-logs-diff-command "git diff -U0 --no-prefix HEAD%?f{ -- }%f"
1060      :commit-command "git commit %!f{-a }-F %s%?f{ -- }%f")
1061     (mercurial-ws
1062      :diff-command "hg diff --git -wbB %f"
1063      :change-logs-diff-command "hg extdiff -o -wbBtU0 %f"
1064      :themes (mercurial ws))
1065     (mercurial
1066      :diff-command "hg diff --git %f"
1067      :diff-header ("\
1068 ^diff .*\n\
1069 \\(\\(deleted file\\|new file\\).*\n\\)?\
1070 \\(--- \\(a/\\)?\\|rename from \\|copy from \\)\\(\\S-+\\).*\n\
1071 \\(\\+\\+\\+ \\(b/\\)?\\|rename to \\|copy to \\)\\(\\S-+\\).*"
1072                    5 8)
1073      :change-logs-diff-command "hg extdiff -o -U0 %f"
1074      :commit-command "hg commit --logfile %s %f")
1075     (darcs-ws
1076      :diff-command "darcs diff --diff-opts -uwbBt %f"
1077      :change-logs-diff-command "darcs diff --diff-opts -wbBtU0 %f"
1078      :themes (darcs ws))
1079     (darcs
1080      :diff-command "darcs diff -u %f"
1081      :diff-header ("\
1082 ^diff .*\n\
1083 --- old-.*?/\\(.*?\\)\t.*\n\
1084 \\+\\+\\+ new-.*?/\\(.*?\\)\t.*"
1085                    1 2)
1086      :change-logs-diff-command "darcs diff --diff-opts -U0 %f"
1087      :commit-command "darcs record -a --logfile %s --delete-logfile %f"
1088      :failed-command-regexp "^darcs failed:")
1089     (svn-ws
1090      :diff-command "svn diff -x -uwb %f"
1091      :change-logs-diff-command
1092      "svn diff --diff-cmd /usr/bin/diff -x -wbBtU0 %f"
1093      :themes (svn ws))
1094     (svn
1095      :diff-command "svn diff -x -u %f"
1096      :diff-header ("\
1097 ^Index: \\(.\\|\n\\)+?\n\
1098 --- \\(.*?\\)\t.*\n\
1099 \\+\\+\\+ \\(.*?\\)\t.*"
1100                    2 3)
1101      :change-logs-diff-command "svn diff --diff-cmd /usr/bin/diff -x -U0 %f"
1102      :commit-command "svn commit -F %s %f")
1103     (cvs-ws
1104      :diff-command "cvs -q diff -uwbBt %f"
1105      :change-logs-diff-command "cvs -q diff -wbBtU0 %f"
1106      :themes (cvs ws))
1107     (cvs
1108      :diff-command "cvs -q diff -u %f"
1109      :diff-header ("\
1110 ^Index: \\(.\\|\n\\)+?\n\
1111 --- \\(.*?\\)\t.*\n\
1112 \\+\\+\\+ \\(.*?\\)\t.*"
1113                    2 3)
1114      :diff-line-filter "\\? .*"
1115      :change-logs-diff-command "cvs -q diff -U0 %f"
1116      :commit-command "cvs commit -F %s %f"
1117      :failed-command-regexp "^cvs \\[[^]]* aborted\\]")
1118     (prcs-ws
1119      :diff-command "prcs diff -f -P %n %f -- -uwbBt"
1120      :change-logs-diff-command "prcs diff -f -P %n %f -- -wbBtU0"
1121      :themes (prcs ws))
1122     (prcs
1123      :diff-command "prcs diff -f -P %n %f -- -u"
1124      :diff-header ("\
1125 ^Index: .*\n\
1126 --- \\(.*?\\)/\\(\\S-+\\) .*\n\
1127 \\+\\+\\+ \\(.*?\\)/\\(\\S-+\\) .*"
1128                    2 4)
1129      :change-logs-diff-command "prcs diff -f -P %n %f -- -U 0"
1130      :commit-command "prcs checkin -f --version-log=%S %n %f"
1131      :failed-command-regexp "^prcs: Command failed.")
1132     (ws
1133      :committed-notice "\
1134 NOTE: This patch has been committed. The version below is informational only.
1135 In particular, whitespace difference have been removed."))
1136   "List of predefined themes.
1137
1138 You can add new ones or override these ones in `patcher-themes'.")
1139
1140 (defsubst patcher-themes ()
1141   ;; Return the concatenation of user defined and built-in themes.
1142   (append patcher-themes patcher-built-in-themes))
1143
1144
1145
1146 (defgroup patcher-projects nil
1147   "Project settings for Patcher."
1148   :group 'patcher)
1149
1150 (defcustom patcher-projects '()
1151   "*List of project descriptors.
1152
1153 Each project descriptor looks like \(NAME DIR :OPTION VALUE ...).
1154 - NAME is the project's name \(a string).
1155 - DIR is the project's root directory (a string).
1156
1157 The remainder of the project descriptor is composed of \"project options\"
1158 \(keyword / value pairs).  When Patcher needs a project option, it tries
1159 to find it at different places:
1160 - First, it looks for it in the project descriptor itself.
1161 - If that fails, it tries to find it in the project themes, if any.
1162 - If that fails, it tries to find it in the inherited projects, if any.
1163 - If that fails, it falls back to the corresponding `patcher-default-*'
1164   user option."
1165   :group 'patcher-projects
1166   :type `(repeat
1167           (group (string :tag "Project")
1168                  (directory :tag "Project directory")
1169                  (repeat :inline t :tag "Options"
1170                          (choice :inline t :value (:mail-method compose-mail)
1171                                  ,@patcher-project-options-custom-type
1172                                  (list :inline t :tag "Inheritance"
1173                                        :format "%{%t%}: %v"
1174                                        (const :tag "" :value :inheritance)
1175                                        (repeat :tag "From"
1176                                                (string :tag "Project"))))))))
1177
1178 (defcustom patcher-subprojects '()
1179   "*List of Patcher subproject descriptors.
1180
1181 Subproject descriptors are similar to project descriptors \(see the
1182 variable `patcher-projects') with a few exceptions:
1183
1184 - Instead of the project directory field DIR, you specify the name of the
1185   project this subproject is based on.
1186 - Two project options are available in addition to the standard ones:
1187   - :subdirectory lets you specify a subdirectory \(of the parent
1188      project's directory) in which the whole subproject resides.  There is
1189      no corresponding `patcher-default-subdirectory' fallback..
1190   - :files lets you specify a list of files or directories composing the
1191      subproject.  Each file specification can contain wildcards.  If a
1192      :subdirectory option is given, these files or directories should be
1193      relative to this subdirectory.  Otherwise, they should be relative to
1194      the base project's directory.  There is no corresponding
1195      `patcher-default-files' variable.
1196   Note that a subproject with neither a :subdirectory nor a :files option
1197   behaves exactly like the corresponding base project.
1198 - Subprojects don't have an :inheritance mechanism.  Instead, they
1199   implicitly inherit from their base project \(which in turn can inherit
1200   from other projects).
1201
1202 Note: the normal way to use predefined Patcher subprojects is to call
1203 `patcher-mail', *not* `patcher-mail-subproject'.  Using the former will
1204 directly use the set of files and/or directory you have specified.  Using
1205 the latter will also let you modify this set."
1206   :group 'patcher-projects
1207   :type `(repeat
1208           (group (string :tag "Subproject")
1209                  (string :tag "Of project")
1210                  (repeat :inline t :tag "Options"
1211                          (choice :inline t :value (:subdirectory "")
1212                                  ;; #### Look inside the widget library to see
1213                                  ;; #### how we can modify the completion
1214                                  ;; #### behavior
1215                                  (list :inline t :tag "Subdirectory"
1216                                        :format "%{%t%}: %v"
1217                                        (const :tag "" :value :subdirectory)
1218                                        directory)
1219                                  (list :inline t :tag "Files"
1220                                        :format "%{%t%}: %v"
1221                                        (const :tag "" :value :files)
1222                                        (repeat :format "\n%v%i\n" file))
1223                                  ,@patcher-project-options-custom-type)))))
1224
1225
1226 ;; Project descriptors Accessors =============================================
1227
1228 ;; #### NOTE: the accessors routines don't handle the case where the same
1229 ;; #### option is given several times.  Only the first one is used.  This
1230 ;; #### currently would have any sensible meaning anyway.
1231
1232 (defsubst patcher-project-patcher-name (project)
1233   (nth 0 project))
1234
1235 (defsubst patcher-subproject-p (project)
1236   ;; Return non nil if PROJECT is defined in `patcher-subprojects'.
1237   (member project patcher-subprojects))
1238
1239 (defcustom patcher-max-theme-depth 8
1240   "*Maximum nesting level in Patcher themes.
1241
1242 This option is a guard against infinite loops that might occur for wrong
1243 settings of Patcher themes (as themes can contain themes)."
1244   :group 'patcher-themes
1245   :type 'integer)
1246
1247 (defun patcher-themes-option (theme-names option level)
1248   ;; Look for an option in a list of themes.  Note that themes can have the
1249   ;; :themes option set.  The themes tree (it shouldn't be a graph) is
1250   ;; traversed in depth first.
1251   (let (theme-name theme value)
1252     (while (and (not value) (setq theme-name (pop theme-names)))
1253       (setq theme (assoc theme-name (patcher-themes)))
1254       (or theme (patcher-error "`%s': no such theme" theme-name))
1255       (let ((theme-options (cdr theme)))
1256         (setq value (member option theme-options))
1257         (unless value
1258           (let ((subthemes (member :themes theme-options)))
1259             (when (> level patcher-max-theme-depth)
1260               (patcher-error "\
1261 Theme `%s': maximum nesting level of themes exceeded.
1262 Either you have an infinite loop in your theme's :themes option, or you should
1263 increase the value of `patcher-max-theme-depth'"
1264                (car theme)))
1265             (setq value
1266                   (patcher-themes-option
1267                    (cadr subthemes) option (1+ level)))))))
1268     value))
1269
1270 (defcustom patcher-max-inheritance-depth 8
1271   "*Maximum nesting level in Patcher projects.
1272
1273 This option is a guard against infinite loops that might occur for wrong
1274 settings of Patcher projects (as projects can inherit projects)."
1275   :group 'patcher-projects
1276   :type 'integer)
1277
1278 (defun patcher-project-option-1 (project option level)
1279   ;; Try to find an option in the project descriptor, otherwise, try in each
1280   ;; project from the project's inheritance list.
1281   ;; The whole option form is returned: '(:stuff value)
1282   (when (> level patcher-max-inheritance-depth)
1283     (patcher-error "\
1284 Project `%s': maximum nesting level of projects exceeded.
1285 Either you have an infinite loop in your project's inheritance, or you should
1286 increase the value of `patcher-max-inheritance-depth'"
1287                    (patcher-project-patcher-name project)))
1288   (let* ((is-subproject (patcher-subproject-p project))
1289          (options (cddr project))
1290          (value (member option options)))
1291     ;; Try to find the option in themes.
1292     (unless value
1293       (let ((themes (member :themes options)))
1294         (when themes
1295           (setq value (patcher-themes-option (cadr themes) option 0)))))
1296     ;; Try to find the option in inherited projects.  Note that inherited
1297     ;; projects can have their :inherit option set in turn.  The inheritance
1298     ;; tree (it shouldn't be a graph) is traverse in depth first.
1299     (unless value
1300       (let ((projs (if is-subproject
1301                        (list (nth 1 project))
1302                      (cadr (member :inheritance options))))
1303             proj)
1304         (when projs
1305           (while (and (not value) (setq proj (pop projs)))
1306             ;; #### FIXME: what happens if we inherit from something like a
1307             ;; #### subproject which is unrelated to the current project ?
1308             (setq value (patcher-project-option-1 (assoc proj patcher-projects)
1309                                                   option (1+ level)))))))
1310     ;; Now some checkings.
1311     (when (and (eq option :files) value)
1312       (if is-subproject
1313           ;; Return the files as a string, not as the original list.
1314           (setq value (list :files (mapconcat #'identity (cadr value) " ")))
1315         ;; #### NOTE: we don't normally check other user-level errors (like,
1316         ;; #### only projects can have an :inheritance option above).
1317         ;; #### However, that case is special: we have some blind calls to
1318         ;; #### `patcher-project-option' that could get an illegal :files
1319         ;; #### options  from illegal projects.  These calls are supposed to
1320         ;; #### return `nil' as a  result, so we perform the checking.
1321         (patcher-error "Project `%s': only subprojects can have a :file option"
1322                        (patcher-project-patcher-name project))
1323         (setq value nil)))
1324     value))
1325
1326 (defun patcher-project-option (project option &optional non-nil)
1327   ;; Returns either a project's option, or the patcher-default-* value.
1328   ;; If NON-NIL, barf on null value.
1329   (let* ((opt (patcher-project-option-1 project option 0))
1330          (val (if opt
1331                   (cadr opt)
1332                 (symbol-value
1333                  (intern-soft
1334                   (concat "patcher-default-"
1335                           (substring (symbol-name option) 1)))))))
1336     (if val
1337         val
1338       (when non-nil
1339         (patcher-error "Project %s: option %s is null"
1340                        (patcher-project-patcher-name project)
1341                        option)))))
1342 (put 'patcher-project-option 'lisp-indent-function 1)
1343
1344 (defsubst patcher-project-name (project)
1345   (let ((name (patcher-project-option project :name)))
1346     (or name (patcher-project-patcher-name project))))
1347
1348 (defun patcher-project-directory (project)
1349   ;; Returns the project directory of PROJECT, possibly expanded as a project
1350   ;; subdir if PROJECT is a subproject.
1351   (if (patcher-subproject-p project)
1352       (let ((prj (assoc (nth 1 project) patcher-projects)))
1353         (unless prj
1354           (patcher-error "Can't find base project for subproject `%s'"
1355                          (patcher-project-patcher-name project)))
1356         (let ((subdir (patcher-project-option project :subdirectory)))
1357           (if subdir
1358               (expand-file-name subdir (patcher-project-directory prj))
1359             (patcher-project-directory prj))))
1360     ;; else: (member project patcher-projects)
1361     (nth 1 project)))
1362
1363
1364 ;; ===========================================================================
1365 ;; Internal utilities
1366 ;; ===========================================================================
1367
1368 ;; #### NOTE: this is currently useless.
1369 (defvar patcher-instances nil
1370   ;; A list of all alive instances of Patcher (an instance is dead after the
1371   ;; mail has been sent.  Each element is of the form '(BUFFER_NAME . BUFFER).
1372   )
1373
1374 (defconst patcher-change-log-entry-start-regexp
1375   "^[0-9]\\{4,4\\}-[0-9]\\{2,2\\}-[0-9]\\{2,2\\} "
1376   ;; Regexp matching the beginning of a ChangeLog entry
1377   )
1378
1379 ;; Buffer local variables ====================================================
1380
1381 ;; The following variables get local values in various Patcher buffers (mail
1382 ;; buffer, process output buffer etc).
1383
1384 (make-variable-buffer-local
1385  (defvar patcher-project nil
1386    ;; Patcher project related to the current patch.  This is also set in
1387    ;; auxiliary buffers.
1388    ))
1389
1390 (make-variable-buffer-local
1391  (defvar patcher-mail-buffer nil
1392    ;; Mail buffer corresponding to Patcher auxiliary buffers.
1393    ))
1394
1395
1396 ;; Utility functions =========================================================
1397
1398 ;;(defun patcher-keyword-value (keyword values)
1399 ;;  ;; Return the value of KEYWORD from a (KEY VAL ...) list.  VAL may be omitted
1400 ;;  ;; in the list, in which case t is returned.
1401 ;;  (let ((vals values)
1402 ;;      key val)
1403 ;;    (catch 'found
1404 ;;      (while (setq key (pop vals))
1405 ;;      (setq val (or (not (car vals))
1406 ;;                    (if (keywordp (car vals)) t (pop vals))))
1407 ;;      (and (eq keyword key)
1408 ;;           (throw 'found val))))
1409 ;;    ))
1410
1411
1412 (defsubst patcher-substitute-name (project str)
1413   ;; Replace a %N in string STR with the current project's name.
1414   ;; Replace a %n in string STR with the value of :name, if set, and with the
1415   ;; current project's name otherwise.
1416   (let ((name (patcher-project-name project))
1417         (patcher-name (patcher-project-patcher-name project))
1418         case-fold-search)
1419     (replace-in-string (replace-in-string str "%N" patcher-name)
1420                        "%n" name)))
1421
1422 (defun patcher-command (project command &optional files)
1423   ;; Build a Patcher command from COMMAND that applies to FILES.
1424   ;; This involves %n, %f %?f and %?f substitutions, as well as :pre-command
1425   ;; handling.
1426
1427   ;; 1/ %n substitution:
1428   (setq command (patcher-substitute-name project command))
1429
1430   ;; 2/ %?f and %!f substitution:
1431   (setq command (replace-in-string command "%!f{\\(.*?\\)}"
1432                                    (if files "" "\\1")))
1433   (setq command (replace-in-string command "%\\?f{\\(.*?\\)}"
1434                                    (if files "\\1" "")))
1435
1436   ;; 3/ %f substitution (force Unix syntax):
1437   (setq command (replace-in-string command "%f"
1438                                    (if files
1439                                        (replace-in-string
1440                                         (mapconcat #'identity files " ")
1441                                         "\\\\" "/")
1442                                      "") t))
1443
1444   ;; 4/ Prepend the :pre-command option to COMMAND, if any:
1445   (let ((precmd (patcher-project-option project :pre-command)))
1446     (when (> (length precmd) 0)
1447       (setq command (concat precmd " " command))))
1448
1449   ;; 5/ Final cosmetic cleanup:
1450   (setq command (replace-in-string command "[ \t]+" " " t))
1451
1452   ;; 6/ You got it.
1453   command)
1454
1455 ;; #### FIXME: we should check the exist status.
1456 (defsubst patcher-call-process (command &optional buffer)
1457   ;; Call a shell process which executes COMMAND (a string) with output to
1458   ;; BUFFER (the current buffer by default).
1459   (apply 'call-process shell-file-name nil (or buffer t) nil
1460          shell-command-switch (list command)))
1461
1462 (defsubst patcher-extent (property &optional value buffer)
1463   ;; Get the extent that has PROPERTY set to VALUE (t if not given) in BUFFER
1464   ;; (current buffer if nil).
1465   (car (mapcar-extents #'identity nil buffer nil nil nil
1466                        property (or value t))))
1467
1468 (defun patcher-process-output-buffer (&optional mail-buffer)
1469   ;; Get a process output buffer for the current Patcher MAIL-BUFFER (current
1470   ;; buffer by default), and prepare it.  We can reuse an already existing one
1471   ;; because auxiliary buffers are currently used only in one Lisp shot, so
1472   ;; there's no risk of Patcher instances overlapping.
1473   (let ((project patcher-project)
1474         (directory default-directory)
1475         (buffer (get-buffer-create " *Patcher Process Output*")))
1476     (or mail-buffer (setq mail-buffer (current-buffer)))
1477     (with-current-buffer buffer
1478       (cd directory)
1479       (setq patcher-project project)
1480       (setq patcher-mail-buffer mail-buffer)
1481       (erase-buffer))
1482     buffer))
1483
1484
1485
1486 ;; ==========================================================================
1487 ;; ChangeLog buffers
1488 ;; ==========================================================================
1489
1490 (defun patcher-read-natnum (prompt &optional default-value)
1491   ;; Hacked from read-number
1492   ;; Read a natural number from the minibuffer, prompting with PROMPT.
1493   ;; If optional second argument DEFAULT-VALUE is non-nil, return that if user
1494   ;; enters an empty line.
1495   (let ((pred (lambda (val) (and (integerp val) (> val 0))))
1496         num)
1497     (while (not (funcall pred num))
1498       (setq num (condition-case ()
1499                     (let ((minibuffer-completion-table nil))
1500                       (read-from-minibuffer
1501                        prompt (if num (prin1-to-string num)) nil t
1502                        nil nil (and default-value
1503                                     (prin1-to-string default-value))))
1504                   (input-error nil)
1505                   (invalid-read-syntax nil)
1506                   (end-of-file nil)))
1507       (or (funcall pred num) (beep)))
1508     num))
1509
1510 (defun patcher-change-log-extent (change-log mail)
1511   ;; Return (maybe after creating it) the extent in buffer CHANGE-LOG which
1512   ;; has the 'patcher property set to the buffer MAIL.
1513   (let ((extent (patcher-extent 'patcher mail change-log)))
1514     (unless extent
1515       (save-window-excursion
1516         (display-buffer change-log t)
1517         (let ((entries (patcher-read-natnum "Number of entries (1): " 1))
1518               beg end)
1519           (save-excursion
1520             (set-buffer change-log)
1521             (save-restriction
1522               (widen)
1523               (goto-char (point-min))
1524               (skip-chars-forward " \n\t")
1525               (unless (looking-at patcher-change-log-entry-start-regexp)
1526                 (patcher-error "\
1527 Beginning of buffer doesn't look like a ChangeLog entry."))
1528               (setq beg (point))
1529               (condition-case nil
1530                   (while (> entries 0)
1531                     (re-search-forward patcher-change-log-entry-start-regexp)
1532                     (setq entries (1- entries)))
1533                 (t
1534                  (patcher-error "\
1535 Buffer is missing %s ChangeLog entr%s to do the count."
1536                                 entries (if (= entries 1) "y" "ies"))))
1537               (setq end
1538                     (or (and (re-search-forward
1539                               patcher-change-log-entry-start-regexp nil t)
1540                              (progn (beginning-of-line) (point)))
1541                         (point-max)))
1542               (set-extent-properties (setq extent (make-extent beg end))
1543                 `(patcher ,mail)))))))
1544     extent))
1545
1546
1547
1548 ;; ==========================================================================
1549 ;; The LogMsg buffer
1550 ;; ==========================================================================
1551
1552 (make-variable-buffer-local
1553  (defvar patcher-logmsg-file-name nil
1554    ;; Name of the temporary file where the log message is stored.
1555    ))
1556
1557 (make-variable-buffer-local
1558  (defvar patcher-logmsg-commit-command
1559    ;; Commit command used for the current Patcher LogMsg buffer.  This variable
1560    ;; is needed because the user has the ability to override the command with
1561    ;; a prefix argument.
1562    ))
1563
1564 (defun patcher-logmsg-compress-change-logs ()
1565   ;; Compress ChangeLog entries appearing in the current buffer between FROM
1566   ;; and TO.  This function compresses the output into something that conveys
1567   ;; the essence of what has been changed, but much more compactly.
1568   (save-excursion
1569     (goto-char (point-min))
1570     (let ((prologue (patcher-project-option patcher-project
1571                       :change-logs-prologue)))
1572       (when (> (length prologue) 0)
1573         (setq prologue (concat
1574                         "^"
1575                         (replace-in-string
1576                          (regexp-quote prologue) "%f" ".+")
1577                         "$"))
1578         (delete-matching-lines prologue)))
1579     (delete-matching-lines patcher-change-log-entry-start-regexp)
1580     ;; Now compress the change log specs into just files, so that mostly just
1581     ;; the annotations are left.
1582     (let ((change-log-change-line
1583            "^\\([ \t]+\\)\\* \\(\\S-+\\)\\( (.*)\\)?:\\( New\\.\\)?"))
1584       (while (re-search-forward change-log-change-line nil t)
1585         (let ((beg (match-beginning 1));; Change to match-end if you want the
1586               ;; indentation.
1587               (end (match-end 0))
1588               files)
1589           (push (match-string 2) files)
1590           (forward-line 1)
1591           (while (looking-at change-log-change-line)
1592             (setq end (match-end 0))
1593             (unless (member (match-string 2) files)
1594               (push (match-string 2) files))
1595             (forward-line 1))
1596           (goto-char beg)
1597           (delete-region beg end)
1598           (insert (mapconcat 'identity (nreverse files) ", ") ":")
1599           (when (looking-at "\\s-+")
1600             (let ((p (point))
1601                   (end (match-end 0)))
1602               ;; If there's no annotation at all for this change, make sure we
1603               ;; don't treat the next change as an annotation for this one!
1604               (if (save-excursion
1605                     (goto-char end)
1606                     (beginning-of-line)
1607                     (looking-at change-log-change-line))
1608                   (progn
1609                     (if (looking-at "[ \t]+")
1610                         (delete-region p (match-end 0))))
1611                 (delete-region p end)
1612                 (insert " ")))))))
1613     ;; Shrink extra blank lines.
1614     (let ((blank-line "^\\s-*$"))
1615       (goto-char (point-min))
1616       (while (and (not (eobp))
1617                   (progn (forward-line 1)
1618                          (re-search-forward blank-line nil t)))
1619         (delete-blank-lines))
1620       (goto-char (point-min))
1621       (if (looking-at blank-line)
1622           (delete-blank-lines)))))
1623
1624
1625 ;; Patcher LogMsg mode ======================================================
1626
1627 (defun patcher-logmsg-insert-subject ()
1628   "Insert the Patcher mail subject into the current LogMsg buffer at point."
1629   (interactive)
1630   (let ((subject "(none)"))
1631     (with-current-buffer patcher-mail-buffer
1632       (save-excursion
1633         (let ((extent (patcher-extent 'patcher-subject-prefix)))
1634           (if extent
1635               (progn
1636                 (goto-char (extent-end-position extent))
1637                 (skip-chars-forward " \t\f\r")
1638                 (unless (eq (point) (point-at-eol))
1639                   (setq subject
1640                         (buffer-substring (point) (point-at-eol)))))
1641             (goto-char (point-min))
1642             (when (patcher-goto-subject)
1643               (skip-chars-forward " \t\f\r")
1644               (unless (eq (point) (point-at-eol))
1645                 (setq subject
1646                       (buffer-substring (point) (point-at-eol)))))))))
1647     (let ((doit (> (length subject) 0)))
1648       (when doit (insert subject))
1649       doit)))
1650
1651 (defun patcher-logmsg-insert-change-logs (&optional separator)
1652   "Insert ChangeLog entries in the current Patcher LogMsg buffer at point.
1653 When called interactively, use a prefix argument to also insert the
1654 ChangeLogs separator string defined by the :change-logs-separator project
1655 option."
1656   (interactive "P")
1657   (unless (point-at-bol)
1658     (insert "\n"))
1659   (when separator
1660     (setq separator (patcher-project-option patcher-project
1661                       :change-logs-separator))
1662     (when (> (length separator) 0)
1663       (insert separator "\n\n")))
1664   (let ((prologue (patcher-project-option patcher-project
1665                     :change-logs-prologue)))
1666     (dolist (change-log (patcher-files-buffers
1667                          (symbol-value-in-buffer 'patcher-change-logs
1668                                                  patcher-mail-buffer)
1669                          'find))
1670       (when (> (length prologue) 0)
1671         (insert (replace-in-string prologue "%f"
1672                                    (patcher-file-relative-name
1673                                     (buffer-file-name change-log)))
1674                     "\n\n"))
1675       (insert (extent-string
1676                ;; #### NOTE: there is an empty line at the end of this extent.
1677                (patcher-change-log-extent change-log patcher-mail-buffer))))))
1678
1679 (defun patcher-logmsg-insert-compressed-change-logs ()
1680   "Insert compressed ChangeLog entries in the current Patcher LogMsg buffer."
1681   (interactive)
1682   (let ((beg (point)))
1683     (patcher-logmsg-insert-change-logs)
1684     (narrow-to-region beg (point))
1685     (patcher-logmsg-compress-change-logs)
1686     (widen)))
1687
1688
1689 ;; #### NOTE: This should be defined in the Mail Buffer section, but defining
1690 ;; #### it here avoids a compiler warning.
1691 (make-variable-buffer-local
1692  (defvar patcher-change-committed nil
1693    ;; Boolean indicating whether the change has been committed already.
1694    ))
1695
1696 (defun patcher-logmsg-commit (&optional arg)
1697   "Commit the change described in the current Patcher LogMsg buffer.
1698 When called interactively, use a prefix to override the commit command."
1699   (interactive "P")
1700   (let ((output-buffer (patcher-process-output-buffer patcher-mail-buffer))
1701         (log-buffer (current-buffer))
1702         runbuf
1703         (change-logs (symbol-value-in-buffer 'patcher-change-logs
1704                                              patcher-mail-buffer))
1705         (sources (symbol-value-in-buffer 'patcher-sources
1706                                          patcher-mail-buffer))
1707         (pre-commit-window-config (symbol-value-in-buffer
1708                                    'patcher-pre-commit-window-config
1709                                    patcher-mail-buffer))
1710         (confirm-commits (patcher-project-option patcher-project
1711                            :confirm-commits)))
1712     (patcher-save-buffers (patcher-files-buffers change-logs))
1713     (and arg (setq patcher-logmsg-commit-command
1714                    (read-shell-command "Commit command: "
1715                                        patcher-logmsg-commit-command)))
1716     ;; Make sure that the log buffer ends with one and only one newline
1717     ;; character. Empty lines are useless, and I've noticed a bug in Darcs
1718     ;; which makes it append the interactive ***END OF DESCRIPTION*** stuff to
1719     ;; the log file contents, when it doesn't end with a newline character.
1720     ;; Also, note that if the log message is put directly on the command line
1721     ;; and not extracted from a file, we take care of removing the final
1722     ;; newline.
1723     (save-excursion
1724       (goto-char (point-max))
1725       (cond ((looking-at "\\'")
1726              (skip-chars-backward "\n")
1727              (delete-region (point) (1- (point-max))))
1728             (t
1729              (insert "\n"))))
1730     (let ((command patcher-logmsg-commit-command))
1731       (setq command
1732             (let (case-fold-search)
1733               (replace-in-string
1734                command
1735                "%S" (shell-quote-argument
1736                      (buffer-substring nil (1- (point-max)))) t)))
1737       (setq command
1738             (replace-in-string command "%s" patcher-logmsg-file-name t))
1739       (setq command
1740             (patcher-command patcher-project command
1741                              (when sources
1742                                (append (mapcar #'patcher-file-relative-name
1743                                                change-logs)
1744                                        sources))))
1745       ;; Maybe display the commit command, and make sure the user agrees.
1746       (when (or (not confirm-commits)
1747                 (save-window-excursion
1748                   (setq runbuf (get-buffer-create
1749                                 "*Patcher Commit Command*"))
1750                   (erase-buffer runbuf)
1751                   (insert-string (format "Command to run:\n\n%s" command)
1752                                  runbuf)
1753                   (display-buffer runbuf)
1754                   (y-or-n-p "Run commit command? ")))
1755         ;; Write out the log message, or "(none)"
1756         (and (= (point-min) (point-max)) (insert "(none)"))
1757         (write-region (point-min) (point-max) patcher-logmsg-file-name
1758                       nil 'silent)
1759         (patcher-with-progression "Committing changes"
1760           (patcher-call-process command output-buffer))
1761         ;; Don't kill the log message buffer.  This will be done after sending
1762         ;; the message -- i.e. when we are done with this project.  We don't
1763         ;; kill the log message buffer now in case the user needs it later --
1764         ;; e.g. if the commit failed and needs to be redone (we try to detect
1765         ;; this, but we might not succeed in all cases.).
1766         ;; Try to see if the commit failed.
1767         ;; #### FIXME: we need to check the exit code !!
1768         (let ((failed-command-regexp
1769                (patcher-project-option patcher-project
1770                  :failed-command-regexp)))
1771           (when failed-command-regexp
1772             (with-current-buffer output-buffer
1773               (goto-char (point-min))
1774               (when (re-search-forward failed-command-regexp nil t)
1775                 (display-buffer output-buffer t)
1776                 (with-current-buffer log-buffer
1777                   ;; make sure substitute-command-keys is run in the right
1778                   ;; buffer
1779                   (patcher-error "\
1780 Error during commit.  Please fix the problem and type \
1781 \\[patcher-logmsg-commit] to try again."))))))
1782         ;; Otherwise, record the successful commit in the mail message.
1783         ;; #### NOTE: it is normal to protect the re-search-forward calls
1784         ;; #### against errors, because when the `fake mail' method is used,
1785         ;; #### neither the Subject line nore the mail-header-separator one
1786         ;; #### exist.
1787         (with-current-buffer patcher-mail-buffer
1788           (setq patcher-change-committed t)
1789           (save-excursion
1790             ;; Possibly change the subject:
1791             (goto-char (point-min))
1792             (when (patcher-goto-subject)
1793               (let ((subject-committed-prefix
1794                      (patcher-project-option patcher-project
1795                        :subject-committed-prefix))
1796                     (extent (patcher-extent 'patcher-subject-prefix)))
1797                 (when subject-committed-prefix
1798                   (setq subject-committed-prefix
1799                         (patcher-substitute-name patcher-project
1800                                                  subject-committed-prefix))
1801                   (when extent
1802                     (goto-char (extent-start-position extent))
1803                     (delete-region (point) (extent-end-position extent)))
1804                   (insert subject-committed-prefix)
1805                   (and (looking-at "\\S-") (insert " ")))))
1806             ;; Insert the `committed' notice:
1807             (goto-char (point-min))
1808             (when (re-search-forward
1809                    (concat "^" (regexp-quote mail-header-separator))
1810                    nil t)
1811               (forward-line 1)
1812               (let ((notice (patcher-project-option patcher-project
1813                               :committed-notice)))
1814                 (when (> (length notice) 0)
1815                   (insert notice "\n"))))))
1816         ;; Bury the log message (see above).  Remove the log message window
1817         ;; and display the output buffer.
1818         (bury-buffer log-buffer)
1819         (set-window-configuration pre-commit-window-config)
1820         (display-buffer output-buffer t))
1821       (and runbuf (bury-buffer runbuf)))))
1822
1823 (defun patcher-logmsg-init-message ()
1824   "(Re)Init the log message in the current Patcher LogMsg buffer.
1825 This is done conforming to the :log-message-items project option."
1826   (interactive)
1827   (erase-buffer)
1828   (let ((items (patcher-project-option patcher-project :log-message-items))
1829         (edit-log-message (patcher-project-option patcher-project
1830                             :edit-log-message))
1831         inserted)
1832     (dolist (item items)
1833       (cond ((eq item 'subject)
1834              (when inserted (insert "\n\n"))
1835              (setq inserted (patcher-logmsg-insert-subject)))
1836             ((eq item 'compressed-change-logs)
1837              (when inserted (insert "\n\n"))
1838              (patcher-logmsg-insert-compressed-change-logs)
1839              (setq inserted t))
1840             ((eq item 'change-logs)
1841              (when inserted (insert "\n\n"))
1842              (patcher-logmsg-insert-change-logs inserted))
1843             (t
1844              (patcher-error "invalid log message item: %s" item))))
1845     (goto-char (point-min))
1846     (if edit-log-message
1847         (patcher-message "\
1848 Edit the log message, and press \\[patcher-logmsg-commit] when done.")
1849       (patcher-logmsg-commit))))
1850
1851 (defcustom patcher-logmsg-mode-hook nil
1852   "*Hook to run after setting up Patcher-Logmsg mode."
1853   :group 'patcher
1854   :type 'hook)
1855
1856 (defvar patcher-logmsg-mode-map
1857   (let ((map (make-sparse-keymap)))
1858     (define-key map [(control c) (control p) s] 'patcher-logmsg-insert-subject)
1859     (define-key map [(control c) (control p) l]
1860       'patcher-logmsg-insert-change-logs)
1861     (define-key map [(control c) (control p) c]
1862       'patcher-logmsg-insert-compressed-change-logs)
1863     (define-key map [(control c) (control p) i] 'patcher-logmsg-init-message)
1864     (define-key map [(control c) (control c)] 'patcher-logmsg-commit)
1865     map))
1866
1867 (defun patcher-logmsg-mode ()
1868   "Major mode for Patcher commit log message management.
1869 You're not supposed to use this mode manually, unless you know what you're
1870 doing.
1871
1872 \\{patcher-logmsg-mode-map}"
1873   (interactive)
1874   (kill-all-local-variables)
1875   (setq major-mode 'patcher-logmsg)
1876   (setq mode-name "Patcher-LogMsg")
1877   (use-local-map patcher-logmsg-mode-map)
1878   (run-hooks 'patcher-logmsg-mode-hook))
1879
1880
1881
1882 ;; ===========================================================================
1883 ;; The Patcher mail buffer
1884 ;; ===========================================================================
1885
1886 (make-variable-buffer-local
1887  (defvar patcher-diff-marker nil
1888    ;; Marker indicating the beginning of the diff.
1889    ))
1890
1891 (make-variable-buffer-local
1892  (defvar patcher-diff-command nil
1893    ;; String containing the diff command to use.  This string is not supposed
1894    ;; to include the files to which the command applies.  Only the command
1895    ;; itself.  This variable is needed because the user has the ability to
1896    ;; override the project's command by giving a prefix to
1897    ;; `patcher-generate-diff'.
1898    ))
1899
1900 (make-variable-buffer-local
1901  (defvar patcher-sources nil
1902    ;; List of files/directories command-line specification for the diff
1903    ;; command.  This variable is needed because the user has the ability to
1904    ;; override the project's files by calling `patcher-mail-subproject'
1905    ;; instead of `patcher-mail'.
1906    ))
1907
1908 (make-variable-buffer-local
1909  (defvar patcher-change-logs-marker nil
1910    ;; Marker indicating the beginning of the ChangeLog entries, when they are
1911    ;; separated from the patch.
1912    ))
1913
1914 (make-variable-buffer-local
1915  (defvar patcher-change-logs nil
1916    ;; List of ChangeLog absolute file names.  This is computed after the
1917    ;; initial diff by `patcher-diff-base'.  Each element is a list of the form
1918    ;; (FILENAME . LOADED). LOADED is a boolean indicating whether we loaded
1919    ;; the file ourselves, and hence can kill it once we're done.
1920    ))
1921
1922 (make-variable-buffer-local
1923  (defvar patcher-pre-commit-window-config nil
1924    ;; Window configuration, just prior to beginning a commit operation, so we
1925    ;; can get back to it at the appropriate time later.
1926    ))
1927
1928 (make-variable-buffer-local
1929  (defvar patcher-logmsg-buffer nil
1930    ;; Buffer containing the commit log message of the current Patcher mail.
1931    ;; This buffer is not killed after the commit operation, but should when
1932    ;; the message is sent.
1933    ))
1934
1935 (defmacro patcher-with-information (information &rest body)
1936   `(save-window-excursion
1937      (save-excursion
1938        (with-output-to-temp-buffer
1939            " *Patcher Information*"
1940          (set-buffer " *Patcher Information*")
1941          (insert ,information))
1942        ,@body)))
1943 (put 'patcher-with-information 'lisp-indent-function 1)
1944
1945 (defsubst patcher-delete-extent-and-region (extent)
1946   ;; Delete EXTENT and the corresponding region.
1947   (when extent
1948     (delete-region (extent-start-position extent) (extent-end-position extent)
1949                    (extent-object extent))
1950     (delete-extent extent)))
1951
1952 (defun patcher-parse-region (&optional min max buffer)
1953   ;; Parse a diff output between MIN and MAX in BUFFER.  Defaults to point min,
1954   ;; point max and current buffer respectively.
1955   ;; For each diffed file, create an extent with the following properties:
1956   ;; 'patcher-change-log = <absolute filename> for ChangeLog files.
1957   ;; 'patcher-source = <absolute filename> for source files.
1958   ;; Return non nil if an error occured.
1959   (with-current-buffer (or buffer (current-buffer))
1960     (let* ((diff-header (patcher-project-option patcher-project
1961                           :diff-header t))
1962            (regexp         (nth 0 diff-header))
1963            (old-file-match (nth 1 diff-header))
1964            (new-file-match (nth 2 diff-header))
1965            (basename-re "\\`\\(.*\\)/\\(.*\\)\\'")
1966            (min (or min (point-min)))
1967            (max (or max (point-max)))
1968            change-log
1969            old-file old-absfile old-dirname
1970            new-file new-absfile new-dirname
1971            beg end)
1972       (save-excursion
1973         (goto-char min)
1974         (while (re-search-forward regexp max t)
1975           (setq beg (match-beginning 0))
1976           (setq old-file (match-string old-file-match))
1977           (setq new-file (match-string new-file-match))
1978           (if (string= old-file "/dev/null")
1979               (setq old-file nil)
1980             (if (string-match basename-re old-file)
1981                 (setq old-dirname  (match-string 1 old-file))
1982               (setq old-dirname ""))
1983             (setq old-absfile (expand-file-name old-file default-directory)))
1984           (if (string= new-file "/dev/null")
1985               (setq new-file nil)
1986             (if (string-match basename-re new-file)
1987                 (setq new-dirname  (match-string 1 new-file))
1988               (setq new-dirname ""))
1989             (setq new-absfile (expand-file-name new-file default-directory)))
1990           (setq end (or (save-excursion
1991                           (and (re-search-forward regexp max t)
1992                                (match-beginning 0)))
1993                         max))
1994           ;; #### NOTE: the extent properties below are set in relation with
1995           ;; NEW-FILE, unless we're facing a deletion, in which case we use
1996           ;; OLD-FILE instead.
1997           (setq change-log
1998                 (with-temp-buffer
1999                   (cd (expand-file-name (or new-dirname old-dirname)
2000                                         default-directory))
2001                   (find-change-log)))
2002           (let ((extent (make-extent beg end)))
2003             (set-extent-properties extent '(start-open t duplicable t))
2004             (set-extent-property extent
2005                                  (if (string= change-log
2006                                               (or new-absfile old-absfile))
2007                                      'patcher-change-log
2008                                    'patcher-source)
2009                                  (or new-absfile old-absfile))))
2010         (goto-char min)
2011         ;; #### FIXME: we need to check the exit code !!
2012         (let ((failed-command-regexp
2013                (patcher-project-option patcher-project
2014                  :failed-command-regexp)))
2015           (when failed-command-regexp
2016             (re-search-forward failed-command-regexp max t)))))))
2017
2018 (defun patcher-generate-change-logs (&optional min max buffer)
2019   ;; Generate ChangeLog skeletons based on the diff between MIN and MAX in
2020   ;; BUFFER.  Defaults to point min, point max and current buffer respectively.
2021   ;; Check `patcher-mail-buffer' first because if that is non nil, we're
2022   ;; in an auxiliary buffer.  Otherwise, we're in a Patcher mail one.
2023   (patcher-with-progression "Generating ChangeLog skeletons"
2024     (let ((mailbuf (or patcher-mail-buffer (current-buffer))))
2025       (with-current-buffer (or buffer (current-buffer))
2026         ;; #### NOTE: before version 3.11, every diff output was cleaned up by
2027         ;; after-diff hooks to remove (some of the) RCS specific syntax. This
2028         ;; isn't the case anymore so in order for patch-to-change-log to keep
2029         ;; on working, we need to do this cleanup here.
2030         (let ((directory default-directory)
2031               (project patcher-project)
2032               (diff-cleaner
2033                (patcher-project-option patcher-project :diff-cleaner))
2034               (diff-header
2035                (patcher-project-option patcher-project :diff-header t))
2036               (string (buffer-substring (or min (point-min))
2037                                         (or max (point-max)))))
2038           (with-string-as-buffer-contents string
2039             (require 'add-log)
2040             (beginning-of-buffer)
2041             (when diff-cleaner
2042               (save-excursion (funcall diff-cleaner diff-header)))
2043             (patch-to-change-log directory
2044                :my-name (or (patcher-project-option project
2045                               :change-logs-user-name)
2046                             (patcher-project-option project
2047                               :user-name))
2048                :my-email (or (patcher-project-option project
2049                                :change-logs-user-mail)
2050                              (patcher-project-option project
2051                                :user-mail))
2052                :keep-source-files
2053                (not (patcher-project-option patcher-project
2054                       :kill-source-files-after-diffing))
2055                :extent-property 'patcher
2056                :extent-property-value mailbuf))))
2057       ;; patch-to-change-log has the unfortunate side effect of burying all
2058       ;; the ChangeLog buffers when it's done.  This is exactly the opposite of
2059       ;; what we want, since once the ChangeLogs have been generated, the next
2060       ;; step is to go visit them.  so put them (in order!) directly below the
2061       ;; current buffer.
2062       (let ((topbuf (car (buffer-list)))
2063             (after-save-change-log-hook
2064              (patcher-project-option patcher-project
2065                :after-save-change-log-hook)))
2066         (dolist (x (patcher-files-buffers
2067                     (symbol-value-in-buffer 'patcher-change-logs mailbuf)))
2068           (with-current-buffer x
2069             (dolist (hook after-save-change-log-hook)
2070               (add-hook 'after-save-hook hook nil t)))
2071           (bury-buffer x topbuf)
2072           ;; window-start ends up past the newly inserted entry, so fix that.
2073           (with-current-buffer x
2074             (let ((ex (patcher-extent 'patcher mailbuf)))
2075               (and ex (goto-char (extent-start-position ex))))))
2076         (bury-buffer topbuf (car (buffer-list)))))))
2077
2078 (defun patcher-ungenerate-change-logs ()
2079   ;; Delete ChangeLog skeletons created by a former call to
2080   ;; `patcher-generate-change-logs', in the current Patcher mail buffer.
2081   (dolist (change-log (patcher-files-buffers patcher-change-logs 'find))
2082     (patcher-delete-extent-and-region
2083      (patcher-change-log-extent change-log (current-buffer)))
2084     (with-current-buffer change-log (save-buffer))))
2085
2086 (defmacro patcher-map-change-log-extents (&optional buffer &rest body)
2087   ;; Map BODY over all extents marking a ChangeLog contents in BUFFER.
2088   `(mapcar-extents
2089     (lambda (extent) ,@body)
2090     nil (or ,buffer (current-buffer)) nil nil nil 'patcher-change-log))
2091 (put 'patcher-map-change-log-extents 'lisp-indent-function 1)
2092
2093 (defmacro patcher-map-source-extents (&optional buffer &rest body)
2094   ;; Map BODY over all extents marking a source contents in BUFFER.
2095   `(mapcar-extents
2096     (lambda (extent) ,@body)
2097     nil (or ,buffer (current-buffer)) nil nil nil 'patcher-source))
2098 (put 'patcher-map-source-extents 'lisp-indent-function 1)
2099
2100 (defun patcher-change-logs (&optional buffer)
2101   ;; Return the list of ChangeLog absolute file names appearing in BUFFER
2102   ;; (current buffer by default).
2103   (let (change-logs)
2104     (patcher-map-change-log-extents buffer
2105       (let ((change-log (extent-property extent 'patcher-change-log)))
2106         (push change-log change-logs)))
2107     change-logs))
2108
2109 (defun patcher-sources (&optional buffer)
2110   ;; Return the list of source absolute file names appearing in BUFFER
2111   ;; (current buffer by default).
2112   (let (sources)
2113     (patcher-map-source-extents buffer
2114       (let ((source (extent-property extent 'patcher-source)))
2115         (push source sources)))
2116     sources))
2117
2118 (defun patcher-remove-change-logs (&optional buffer)
2119   ;; Remove ChangeLog contents from BUFFER (current buffer by default).
2120   (patcher-with-progression "Removing ChangeLog contents"
2121     (patcher-map-change-log-extents buffer
2122       (patcher-delete-extent-and-region extent))))
2123
2124 (patcher-globally-declare-boundp
2125  '(name source-diff source-files change-log-diff change-log-files))
2126
2127 (defun patcher-default-diff-prologue (kind)
2128   ;; Default function for inserting a diff prologue.
2129   (cond ((eq kind 'sources)
2130          (insert name " source patch:\n"
2131                  "Diff command:   " source-diff "\n"
2132                  "Files affected: " source-files "\n"
2133                  "\n"))
2134         ((eq kind 'change-logs)
2135          (insert name " ChangeLog patch:\n"
2136                  "Diff command:   " change-log-diff "\n"
2137                  "Files affected: " change-log-files "\n"
2138                  "\n"))
2139         ((eq kind 'mixed)
2140          (insert name " patch:\n")
2141          (if (not change-log-diff)
2142              (insert "Diff command:             " source-diff "\n"
2143                      "ChangeLog files affected: " change-log-files "\n"
2144                      "Source files affected:    " source-files "\n")
2145            (insert "ChangeLog files diff command: " change-log-diff "\n"
2146                    "Files affected:               " change-log-files "\n"
2147                    "Source files diff command:    " source-diff "\n"
2148                    "Files affected:               " source-files "\n"))
2149          (insert "\n"))))
2150
2151 (defun patcher-insert-diff (buffer)
2152   ;; Insert the diff created in auxiliary BUFFER, and create the patcher-diff
2153   ;; extent.  This function also filters out lines specified by the
2154   ;; diff-line-filter project option.
2155   (save-excursion
2156     (goto-char patcher-diff-marker)
2157     (let ((font-lock-always-fontify-immediately t)
2158           (pos (with-current-buffer buffer (goto-char (point-min))))
2159           (diff-line-filter
2160            (patcher-project-option patcher-project :diff-line-filter)))
2161       (when diff-line-filter
2162         (setq diff-line-filter (concat "^" diff-line-filter "\n"))
2163         (while (re-search-forward diff-line-filter nil t nil buffer)
2164           (insert (buffer-substring pos (match-beginning 0) buffer))
2165           (setq pos (point buffer))))
2166       (insert (buffer-substring pos nil buffer)))
2167     (set-extent-properties (make-extent patcher-diff-marker (point))
2168       '(start-open t patcher-diff t))))
2169
2170
2171 (defun patcher-generic-diff-cleaner (diff-header &optional beg end)
2172   "Patcher default post-processor for diffs.
2173
2174 This function cleans up RCS-specific diff output (as parsed by the
2175 :diff-header project option) to make it look like a standard one."
2176   (unless beg (setq beg (point-min)))
2177   (unless end (setq end (point-max)))
2178   (let ((regexp         (nth 0 diff-header))
2179         (old-file-match (nth 1 diff-header))
2180         (new-file-match (nth 2 diff-header)))
2181     (while (re-search-forward regexp end t)
2182       (replace-match
2183        (concat "--- \\" (number-to-string old-file-match) "\n"
2184                "+++ \\" (number-to-string new-file-match))))))
2185
2186 (defun patcher-git-intent-to-add (file)
2187   "Signal our intention to add FILE to the Git index (add -N)."
2188   (with-temp-buffer
2189     (patcher-call-process (concat "git add -N -- " file))))
2190
2191 (defun patcher-git-add ()
2192   "Add the current buffer's file to the Git index."
2193   (let ((file (patcher-file-relative-name (buffer-file-name))))
2194     (with-temp-buffer
2195       (patcher-call-process (concat "git add -- " file)))))
2196
2197 (defun patcher-run-after-diff-hook (buffer &optional beg end)
2198   ;; If any, call the after-diff hooks on BUFFER (auxiliary or mail
2199   ;; buffer), possibly limiting to the region (BEG END).
2200   ;; #### NOTE: remember that patcher-projects is also set in auxiliary
2201   ;; #### buffers.
2202   (with-current-buffer buffer
2203     (let ((after-diff-hook (patcher-project-option patcher-project
2204                              :after-diff-hook)))
2205       (when after-diff-hook
2206         (patcher-with-progression "Running after diff hooks"
2207           (save-excursion
2208             (mapcar (lambda (func)
2209                       (goto-char (point-min))
2210                       (funcall func beg end))
2211                     after-diff-hook)))))))
2212
2213 (defun patcher-diff-all ()
2214   ;; Create a global diff with both ChangeLogs and given files, insert it in
2215   ;; the current Patcher mail buffer at the patcher-diff-marker position if it
2216   ;; succeeded, and create the patcher-diff extent.
2217   (patcher-save-buffers (patcher-files-buffers patcher-change-logs))
2218   (let ((command
2219          (patcher-command patcher-project patcher-diff-command
2220                           (when patcher-sources
2221                             (append (mapcar #'patcher-file-relative-name
2222                                             patcher-change-logs)
2223                                     patcher-sources))))
2224         (buffer (patcher-process-output-buffer)))
2225     (patcher-with-progression "Generating global diff"
2226       (patcher-call-process command buffer))
2227     (patcher-run-after-diff-hook buffer)
2228     (when (patcher-parse-region nil nil buffer)
2229       (display-buffer buffer t)
2230       (patcher-error "\
2231 Error during diff.  Please fix the problem and type \
2232 \\[patcher-generate-diff] to try again."))
2233     (patcher-insert-diff buffer)))
2234
2235 (defun patcher-insert-change-logs-verbatim ()
2236   ;; Insert ChangeLog contents verbatim in the current Patcher mail buffer,
2237   ;; and create the patcher-change-logs extent.
2238   (let ((prologue
2239          (patcher-project-option patcher-project :change-logs-prologue)))
2240     (patcher-with-progression "Inserting ChangeLog contents"
2241       (save-excursion
2242         (goto-char patcher-change-logs-marker)
2243         (dolist (change-log (patcher-files-buffers patcher-change-logs 'find))
2244           (let ((extent
2245                  (patcher-change-log-extent change-log (current-buffer)))
2246                 (beg (point)))
2247             (insert "\n")
2248             (when (> (length prologue) 0)
2249               (insert (replace-in-string prologue "%f"
2250                                          (patcher-file-relative-name
2251                                           (buffer-file-name change-log)))
2252                       "\n\n"))
2253             (insert (extent-string extent))
2254             (set-extent-properties (make-extent beg (point))
2255               `(start-open t patcher-change-log
2256                            ,(buffer-file-name change-log)))))
2257         (set-extent-properties (make-extent patcher-change-logs-marker (point))
2258           '(start-open t patcher-change-logs t))))))
2259
2260 (defun patcher-insert-change-logs-diff-prologue (command)
2261   ;; Insert a ChangeLog diff prologue at point in current Patcher mail buffer.
2262   (let ((function (patcher-project-option patcher-project
2263                     :diff-prologue-function)))
2264     (when function
2265       (let ((name (patcher-project-name patcher-project))
2266             (change-log-files (patcher-files-string patcher-change-logs))
2267             (change-log-diff (patcher-command patcher-project command)))
2268         (funcall function 'change-logs)))))
2269
2270 (defun patcher-diff-change-logs (command)
2271   ;; Create a diff with only ChangeLogs, insert it in the current Patcher mail
2272   ;; buffer at the patcher-change-logs-marker position if it succeeded, and
2273   ;; create the patcher-change-logs extent.
2274   (patcher-save-buffers (patcher-files-buffers patcher-change-logs))
2275   (let ((buffer (patcher-process-output-buffer)))
2276     (patcher-with-progression "Generating the ChangeLogs diff"
2277       (patcher-call-process
2278        (patcher-command patcher-project command
2279                         (mapcar #'patcher-file-relative-name
2280                                 patcher-change-logs))
2281        buffer))
2282     (patcher-run-after-diff-hook buffer)
2283     (when (patcher-parse-region nil nil buffer)
2284       (display-buffer buffer t)
2285       (patcher-error "\
2286 Error during diff.  Please fix the problem and type \
2287 \\[patcher-insert-change-logs] to try again."))
2288     ;; #### FIXME: maybe check that all changelogs are diff'ed (meaning the
2289     ;; #### user has not forgotten to update one of them).
2290     (save-excursion
2291       (goto-char patcher-change-logs-marker)
2292       (patcher-insert-change-logs-diff-prologue command)
2293       (let ((font-lock-always-fontify-immediately t))
2294         (insert (buffer-substring nil nil buffer)))
2295       (set-extent-properties (make-extent patcher-change-logs-marker (point))
2296         '(start-open t patcher-change-logs t)))))
2297
2298 (defun patcher-pack-change-logs ()
2299   ;; Pack ChangeLog diffs to the change-logs marker in the current Patcher
2300   ;; mail buffer, and create the patcher-change-logs extent.
2301   (patcher-with-progression "Packing ChangeLog diffs"
2302     (save-excursion
2303       (goto-char patcher-change-logs-marker)
2304       (patcher-insert-change-logs-diff-prologue patcher-diff-command)
2305       (patcher-map-change-log-extents nil
2306         (let ((contents (extent-string extent))
2307               (change-log (extent-property extent 'patcher-change-log))
2308               (beg (point)))
2309           (patcher-delete-extent-and-region extent)
2310           (insert contents)
2311           (set-extent-properties (make-extent beg (point))
2312             `(start-open t patcher-change-log ,change-log))))
2313       (set-extent-properties (make-extent patcher-change-logs-marker (point))
2314         `(start-open t patcher-change-logs t)))))
2315
2316 (defun patcher-extent-error (extent)
2317   ;; Look for an error in EXTENT.
2318   ;; Update the 'patcher-error property as needed.
2319   ;; Return 0 if status is unchanged, 1 if an error appeared, -1 if an error
2320   ;; was fixed.
2321   (let* ((failed-command-regexp
2322           (patcher-project-option patcher-project :failed-command-regexp))
2323          (old-error (if (extent-property extent 'patcher-error) 1 0))
2324          ;; #### FIXME: what about the exit status ??
2325          (new-error (if (and failed-command-regexp
2326                              (save-excursion
2327                                (goto-char (extent-start-position extent))
2328                                (re-search-forward failed-command-regexp
2329                                                   (extent-end-position extent)
2330                                                   t)))
2331                         1
2332                       0))
2333          (error (- new-error old-error)))
2334     (cond ((eq error 1)
2335            (set-extent-property extent 'patcher-error t))
2336           ((eq error -1)
2337            (set-extent-property extent 'patcher-error nil)))
2338     error))
2339
2340 (defun patcher-convert-change-log-diffs (command)
2341   ;; Scan all ChangeLog diffs in the current Patcher mail buffer, and remake
2342   ;; them one by one with the proper diff COMMAND, directly in place.
2343   (save-excursion
2344     (let ((diff-extent (patcher-extent 'patcher-diff))
2345           (errors 0)
2346           change-log beg end)
2347       ;; #### Don't forget to start-close the diff extent !! A ChangeLog could
2348       ;; #### appear at the beginning of the diff.
2349       (set-extent-property diff-extent 'start-open nil)
2350       (patcher-with-progression "Regenerating ChangeLog diffs"
2351         (patcher-map-change-log-extents nil
2352           ;; #### WARNING: it seems that if I modify the extent contents here,
2353           ;; #### instead of deleting and recreating it, map(car)-extents goes
2354           ;; #### into an infinite loop, on all extents over and over again.
2355           (setq change-log (extent-property extent 'patcher-change-log))
2356           (goto-char (extent-start-position extent))
2357           (setq beg (point-marker))
2358           (patcher-delete-extent-and-region extent)
2359           (patcher-call-process
2360            (patcher-command patcher-project command
2361                             (list (patcher-file-relative-name change-log))))
2362           (setq end (point-marker))
2363           (patcher-run-after-diff-hook (current-buffer) beg end)
2364           (setq extent (make-extent beg end))
2365           (set-extent-properties extent
2366             `(start-open t patcher-change-log ,change-log))
2367           (setq errors (+ errors (patcher-extent-error extent)))))
2368       (set-extent-property diff-extent 'start-open t)
2369       (and (> errors 0)
2370            (set-extent-property (patcher-extent 'patcher-diff)
2371                                 'patcher-error errors)
2372            (patcher-error "\
2373 Problems during diff.  \
2374 Please type \\[patcher-insert-change-logs] to try again.")))))
2375
2376 (defun patcher-insert-diff-prologue (command)
2377   ;; Insert a prologue at the top of the diff in the current Patcher mail
2378   ;; buffer.
2379   (let ((function (patcher-project-option patcher-project
2380                     :diff-prologue-function)))
2381     (when function
2382       (let ((extent (patcher-extent 'patcher-diff))
2383             (name (patcher-project-name patcher-project))
2384             (source-diff
2385              (patcher-command patcher-project patcher-diff-command))
2386             ;; #### NOTE: maybe passing a list instead of a string would be
2387             ;; #### better.  I won't break backward compatibility though, at
2388             ;; #### least not before a major release.
2389             (source-files (patcher-files-string (patcher-sources)))
2390             (change-log-files (patcher-files-string patcher-change-logs))
2391             (change-log-diff
2392              (and (stringp command)
2393                   (patcher-command patcher-project command))))
2394         (set-extent-property extent 'start-open nil)
2395         (save-excursion
2396           (goto-char patcher-diff-marker)
2397           (funcall function (if (symbolp command) command 'mixed)))
2398         (set-extent-property extent 'start-open t)))))
2399
2400 (defun patcher-diff-base (buffer)
2401   ;; Create the initial diff and deduce some information about the patch. Note
2402   ;; that if we do ChangeLogs, these files can't be deduced from the variable
2403   ;; `patcher-sources', even when set, because it might contain directory
2404   ;; specifications, so we need to deduce them from the diff output.
2405   (patcher-with-progression "Diff'ing the project"
2406     (patcher-call-process
2407      (patcher-command patcher-project patcher-diff-command patcher-sources)
2408      buffer))
2409   (patcher-run-after-diff-hook buffer)
2410   (when (patcher-parse-region nil nil buffer)
2411     (display-buffer buffer t)
2412     (patcher-error "\
2413 Error during diff.  \
2414 Please fix the problem and type \\[patcher-generate-diff] to try again."))
2415   (unless (patcher-sources buffer)
2416     (patcher-error "Your source files do not differ from the archive."))
2417   (when (patcher-project-option patcher-project :change-logs-updating)
2418     (let ((notice-change-log-hook
2419            (patcher-project-option patcher-project :notice-change-log-hook)))
2420       (setq patcher-change-logs nil)
2421       (patcher-map-source-extents buffer
2422         (let* ((file (extent-property extent 'patcher-source))
2423                (change-log (with-temp-buffer
2424                              (cd (file-name-directory file))
2425                              (find-change-log))))
2426           (unless (assoc change-log patcher-change-logs)
2427             (push (cons change-log (not (get-file-buffer change-log)))
2428                   patcher-change-logs)
2429             (patcher-with-progression "Running the notice-change-log hooks"
2430               (run-hook-with-args
2431                'notice-change-log-hook
2432                (patcher-file-relative-name change-log)))))))))
2433
2434 (defun patcher-change-logs-diff-error ()
2435   (patcher-error "\
2436 Patcher has detected a ChangeLog diff.  This can mean two things:
2437
2438 - your project might be out of date (someone else has modified the ChangeLog
2439   file in the meantime.  You should then update your project before running
2440   Patcher.
2441
2442 - you have spurious ChangeLog entries.  This happens for instance when you have
2443   filled the ChangeLogs files manually, but Patcher is supposed to do so (the
2444   :change-log-updating project option is 'automatic).  You should then clean up
2445   your ChangeLog file before running Patcher."))
2446
2447 (defun patcher-generate-diff-1 ()
2448   ;; (Re)Create a diff in the current Patcher mail buffer.
2449   (let ((buffer (patcher-process-output-buffer))
2450         (updating (patcher-project-option patcher-project
2451                     :change-logs-updating))
2452         (appearance (patcher-project-option patcher-project
2453                       :change-logs-appearance))
2454         (regenerate (or (patcher-extent 'patcher-diff) patcher-change-logs)))
2455     ;; Maybe clean up the place for a new diff.
2456     (and regenerate
2457          (patcher-delete-extent-and-region (patcher-extent 'patcher-diff)))
2458     (if (not updating)
2459         ;; We don't do ChangeLogs: just (re)diff the project.
2460         (progn
2461           (patcher-diff-base buffer)
2462           (patcher-insert-diff buffer)
2463           (patcher-insert-diff-prologue 'sources)
2464           (patcher-message "\
2465 To commit your changes, type \\[patcher-commit-change]."))
2466       ;; We do ChangeLogs, so deal with the formatting.
2467       (cond ((eq updating 'automatic)
2468              ;; In the "automatic" case, ChangeLog contents insertion is
2469              ;; postponed until the user has edited the skeletons.  If no files
2470              ;; were specified, we have a chance to check that the project is
2471              ;; up to date: if a ChangeLog appears in the diff, the project
2472              ;; needs to be updated first.  Note that this does not catch all
2473              ;; cases though.
2474              (cond ((or (eq appearance 'verbatim)
2475                         (eq appearance 'packed))
2476                     (let ((generate-change-logs t)
2477                           (change-logs-extent
2478                            (patcher-extent 'patcher-change-logs)))
2479                       (when regenerate
2480                         (patcher-with-information
2481                             (format "\
2482 ChangeLog skeletons for this patch have already been generated%s.
2483
2484 If you answer `yes' to the question below, both the diff and the ChangeLog
2485 entries will be regenerated.  This means that current ChangeLog entries will be
2486 lost.  If otherwise your answer is `no', only the diff will be regenerated."
2487                                     (if change-logs-extent " and inserted" ""))
2488                           (setq generate-change-logs (yes-or-no-p "\
2489 Regenerate ChangeLog skeletons ? ")))
2490                         (when generate-change-logs
2491                           (patcher-delete-extent-and-region change-logs-extent)
2492                           (patcher-ungenerate-change-logs)))
2493                       (patcher-diff-base buffer)
2494                       (when (if regenerate
2495                                 (and generate-change-logs
2496                                      (not patcher-sources)
2497                                      (patcher-change-logs buffer))
2498                               (and (not patcher-sources)
2499                                    (patcher-change-logs buffer)))
2500                         (patcher-change-logs-diff-error))
2501                       ;; ChangeLogs appear outside the patch, so we can insert
2502                       ;; the diff right now, and then generate the skeletons.
2503                       (patcher-insert-diff buffer)
2504                       (patcher-insert-diff-prologue 'sources)
2505                       (if generate-change-logs
2506                           (progn
2507                             (patcher-generate-change-logs patcher-diff-marker
2508                                                           (extent-end-position
2509                                                            (patcher-extent
2510                                                             'patcher-diff)))
2511                             (patcher-message "\
2512 Please annotate the ChangeLog skeletons, \
2513 and type \\[patcher-insert-change-logs] to %s them."
2514                                              (if (eq appearance 'verbatim)
2515                                                  "insert"
2516                                                "diff")))
2517                         ;; not generate-change-logs
2518                         (if change-logs-extent
2519                             (patcher-message "\
2520 To commit your changes, type \\[patcher-commit-change].")
2521                           (patcher-message "\
2522 Please type \\[patcher-insert-change-logs] to %s the ChangeLogs"
2523                                            (if (eq appearance 'verbatim)
2524                                                "insert"
2525                                              "diff"))))))
2526                    ((eq appearance 'patch)
2527                     (let ((generate-change-logs t))
2528                       (when regenerate
2529                         (patcher-with-information "\
2530 ChangeLog skeletons for this patch have already been generated.
2531
2532 If you answer `yes' to the question below, the ChangeLog entries will be
2533 regenerated.  This means that current ones will be lost.  If otherwise your
2534 answer is `no', it is assumed that you have edited the skeletons, and the
2535 project will be rediff'ed with them."
2536                           (setq generate-change-logs (yes-or-no-p "\
2537 Regenerate ChangeLog skeletons ? ")))
2538                         (and generate-change-logs
2539                              (patcher-ungenerate-change-logs)))
2540                       (if generate-change-logs
2541                           (progn
2542                             (patcher-diff-base buffer)
2543                             (when (and (not patcher-sources)
2544                                        (patcher-change-logs buffer))
2545                               (patcher-change-logs-diff-error))
2546                             ;; ChangeLogs must appear in the patch, so there's
2547                             ;; no point in inserting the diff right now.  It
2548                             ;; needs to be redone afterwards.
2549                             (patcher-generate-change-logs nil nil buffer)
2550                             (patcher-message "\
2551 Please annotate the ChangeLog skeletons, \
2552 and type \\[patcher-insert-change-logs] to create the whole diff."))
2553                         ;; not generate-change-logs
2554                         ;; ChangeLogs are supposed to be written, so
2555                         ;; everything goes as if we were in a 'manual case:
2556                         (let ((command (patcher-project-option patcher-project
2557                                          :change-logs-diff-command t)))
2558                           (cond ((eq command 'diff)
2559                                  (patcher-diff-all)
2560                                  (patcher-insert-diff-prologue 'mixed))
2561                                 ((stringp command)
2562                                  (patcher-diff-all)
2563                                  (patcher-convert-change-log-diffs command)
2564                                  (patcher-insert-diff-prologue command))
2565                                 (t
2566                                  (patcher-error "\
2567 invalid `change-logs-diff-command' option: %s" command))))
2568                         (patcher-message "\
2569 To commit your changes, type \\[patcher-commit-change]."))))
2570                    ((not appearance)
2571                     (let ((generate-change-logs t))
2572                       (when regenerate
2573                         (patcher-with-information "\
2574 ChangeLog skeletons for this patch have already been generated.
2575
2576 If you answer `yes' to the question below, the ChangeLog entries will be
2577 regenerated.  This means that current ones will be lost.  If otherwise your
2578 answer is `no', the current CHangeLog entries won't be touched."
2579                           (setq generate-change-logs (yes-or-no-p "\
2580 Regenerate ChangeLog skeletons ? ")))
2581                         (and generate-change-logs
2582                              (patcher-ungenerate-change-logs)))
2583                       (if generate-change-logs
2584                           (progn
2585                             (patcher-diff-base buffer)
2586                             (when (and (not patcher-sources)
2587                                        (patcher-change-logs buffer))
2588                               (patcher-change-logs-diff-error))
2589                             ;; ChangeLogs do not appear, so we can insert the
2590                             ;; diff right now, and then generate the
2591                             ;; skeletons.
2592                             (patcher-insert-diff buffer)
2593                             (patcher-insert-diff-prologue 'sources)
2594                             (patcher-generate-change-logs patcher-diff-marker
2595                                                           (extent-end-position
2596                                                            (patcher-extent
2597                                                             'patcher-diff)))
2598                             (message "\
2599 Please don't forget to annotate the ChangeLog skeletons."))
2600                         ;; not generate-change-logs
2601                         (patcher-diff-base buffer)
2602                         (patcher-remove-change-logs buffer)
2603                         (patcher-insert-diff buffer)
2604                         (patcher-insert-diff-prologue 'sources)
2605                         (patcher-message "\
2606 To commit your changes, type \\[patcher-commit-change]."))))
2607                    (t
2608                     (patcher-error "\
2609 invalid `change-logs-appearance' option: %s" appearance))))
2610             ((eq updating 'manual)
2611              ;; In the "manual" case, ChangeLogs are supposed to be already
2612              ;; written, so their insertion does not have to be postponed.  If
2613              ;; no files were specified, we have a chance to check that
2614              ;; ChangeLogs /really/ are up to date: the diff output should
2615              ;; contain all ChangeLog entries.
2616              (patcher-diff-base buffer)
2617              (when (and (not patcher-sources)
2618                         (not (equal (patcher-change-logs buffer)
2619                                     (mapcar 'car patcher-change-logs))))
2620                (patcher-error "\
2621 Some ChangeLog files are not updated.  \
2622 Please update them before running Patcher."))
2623              (cond ((eq appearance 'verbatim)
2624                     ;; #### NOTE: when ChangeLog entries are part of the diff,
2625                     ;; #### we could try to convert the diff to a verbatim
2626                     ;; #### version  instead of calling
2627                     ;; `patcher-insert-change-logs-verbatim'.
2628                     (patcher-remove-change-logs buffer)
2629                     (patcher-insert-diff buffer)
2630                     (patcher-insert-diff-prologue 'sources)
2631                     (or regenerate (patcher-insert-change-logs-verbatim)))
2632                    ((eq appearance 'packed)
2633                     (let ((command (patcher-project-option patcher-project
2634                                      :change-logs-diff-command t)))
2635                       (cond ((eq command 'diff)
2636                              ;; We use the same diff command:
2637                              (if (not patcher-sources)
2638                                  ;; All ChangeLogs appear in the diff.  We can
2639                                  ;; just move them to a pack.
2640                                  (progn
2641                                    (and regenerate
2642                                         (patcher-remove-change-logs buffer))
2643                                    (patcher-insert-diff buffer)
2644                                    (patcher-insert-diff-prologue 'sources)
2645                                    (or regenerate
2646                                        (patcher-pack-change-logs)))
2647                                ;; Otherwise, some ChangeLogs may not be there,
2648                                ;; so rediff them all.
2649                                (patcher-remove-change-logs buffer)
2650                                (patcher-insert-diff buffer)
2651                                (patcher-insert-diff-prologue 'sources)
2652                                (or regenerate
2653                                    (patcher-diff-change-logs
2654                                     patcher-diff-command))))
2655                             ((stringp command)
2656                              ;; The diff command is different.  We have to
2657                              ;; (re)diff them anyway.
2658                              (patcher-remove-change-logs buffer)
2659                              (patcher-insert-diff buffer)
2660                              (patcher-insert-diff-prologue 'sources)
2661                              (or regenerate
2662                                  (patcher-diff-change-logs command)))
2663                             (t
2664                              (patcher-error "\
2665 invalid `change-logs-diff-command' option: %s" command)))))
2666                    ((eq appearance 'patch)
2667                     (let ((command (patcher-project-option patcher-project
2668                                      :change-logs-diff-command t)))
2669                       (cond ((eq command 'diff)
2670                              (if patcher-sources
2671                                  ;; Some ChangeLog entries might not be
2672                                  ;; present, so we must rediff the whole
2673                                  ;; stuff.
2674                                  (progn
2675                                    (patcher-diff-all)
2676                                    (patcher-insert-diff-prologue 'mixed))
2677                                ;; Otherwise, the ChangeLog entries are in the
2678                                ;; diff.
2679                                (patcher-insert-diff buffer)
2680                                (patcher-insert-diff-prologue 'mixed)))
2681                             ((stringp command)
2682                              (if (not patcher-sources)
2683                                  (progn
2684                                    (patcher-insert-diff buffer)
2685                                    (patcher-insert-diff-prologue command)
2686                                    (patcher-convert-change-log-diffs command))
2687                                ;; else some ChangeLog entries might not be
2688                                ;; present, so we must rediff the whole thing,
2689                                ;; and convert each ChangeLog diff to the
2690                                ;; proper command.
2691                                (patcher-diff-all)
2692                                (patcher-insert-diff-prologue command)
2693                                (patcher-convert-change-log-diffs command)))
2694                             (t
2695                              (patcher-error "\
2696 invalid `change-logs-diff-command' option: %s" command)))))
2697                    ((not appearance)
2698                     (patcher-remove-change-logs buffer)
2699                     (patcher-insert-diff buffer)
2700                     (patcher-insert-diff-prologue 'sources))
2701                    (t
2702                     (patcher-error "\
2703 invalid `change-logs-appearance' option: %s"
2704                                    appearance)))
2705              (patcher-message "\
2706 To commit your changes, type \\[patcher-commit-change]."))
2707             (t
2708              (patcher-error "invalid `change-logs-updating' option: %s"
2709                             updating))))))
2710
2711
2712 ;; Patcher minor-mode ========================================================
2713
2714 (defun patcher-insert-change-logs ()
2715   "(Re)Insert ChangeLog entries in the current Patcher mail buffer."
2716   (interactive)
2717   (let ((updating
2718          (or (patcher-project-option patcher-project :change-logs-updating)
2719              (patcher-error "This project does not handle ChangeLogs")))
2720         (appearance
2721          (or (patcher-project-option patcher-project :change-logs-appearance)
2722              (patcher-error
2723               "ChangeLogs are not supposed to appear in the message."))))
2724     (cond ((or (eq updating 'automatic)
2725                (eq updating 'manual))
2726            (cond ((eq appearance 'verbatim)
2727                   (or (patcher-extent 'patcher-diff)
2728                       (patcher-error "Please generate the diff first."))
2729                   (let* ((extent (patcher-extent 'patcher-change-logs))
2730                          (do-it (or (not extent) (y-or-n-p "\
2731 ChangeLog entries already inserted.  Replace ? "))))
2732                     (when do-it
2733                       (patcher-delete-extent-and-region extent)
2734                       (patcher-insert-change-logs-verbatim))))
2735                  ((eq appearance 'packed)
2736                   (or (patcher-extent 'patcher-diff)
2737                       (patcher-error "Please generate the diff first."))
2738                   (let* ((extent (patcher-extent 'patcher-change-logs))
2739                          (do-it (or (not extent) (y-or-n-p "\
2740 ChangeLog entries already inserted.  Replace ? "))))
2741                     (when do-it
2742                       (patcher-delete-extent-and-region extent)
2743                       (let ((command (patcher-project-option patcher-project
2744                                        :change-logs-diff-command t)))
2745                         (cond ((eq command 'diff)
2746                                ;; We use the same diff command:
2747                                (patcher-diff-change-logs patcher-diff-command))
2748                               ((stringp command)
2749                                (patcher-diff-change-logs command))
2750                               (t
2751                                (patcher-error "\
2752 invalid `change-logs-diff-command' option: %s" command)))))))
2753                  ((eq appearance 'patch)
2754                   (when (or (not (patcher-change-logs))
2755                             (y-or-n-p "\
2756 ChangeLog entries already inserted.  Replace ? "))
2757                     (patcher-delete-extent-and-region
2758                      (patcher-extent 'patcher-diff))
2759                     (let ((command (patcher-project-option patcher-project
2760                                      :change-logs-diff-command t)))
2761                       (cond ((eq command 'diff)
2762                              (patcher-diff-all)
2763                              (patcher-insert-diff-prologue 'mixed))
2764                             ((stringp command)
2765                              (patcher-diff-all)
2766                              (patcher-convert-change-log-diffs command)
2767                              (patcher-insert-diff-prologue command))
2768                             (t
2769                              (patcher-error "\
2770 invalid `change-logs-diff-command' option: %s" command))))))
2771                  (t
2772                   (patcher-error "invalid `change-logs-appearance' option: %s"
2773                                  appearance))))
2774           (t
2775            (patcher-error "invalid `change-logs-updating' option: %s"
2776                           updating)))))
2777
2778 (defun patcher-commit-change (&optional arg)
2779   "Prepare to, and possibly commit a change to a project's repository.
2780 The change is the one that is announced in the mail buffer.
2781
2782 When called interactively, use a prefix (ARG) to override the commit
2783 command to use.  Note that this is not meant to modify the source and
2784 ChangeLog files affected by the commit: they are computed automatically."
2785   (interactive "P")
2786   (and patcher-change-committed
2787        (patcher-error "Change already committed !"))
2788   (let* ((buffer (generate-new-buffer "*Patcher Log Message*"))
2789          (project patcher-project)
2790          (directory default-directory)
2791          (mail-buffer (current-buffer)))
2792     (with-current-buffer buffer
2793       (patcher-logmsg-mode)
2794       (cd directory)
2795       (setq patcher-project project)
2796       (setq patcher-mail-buffer mail-buffer)
2797       (setq patcher-logmsg-file-name
2798             (replace-in-string (make-temp-name
2799                                 (expand-file-name "patch" (temp-directory)))
2800                                "\\\\" "/"))
2801       (setq patcher-logmsg-commit-command
2802             (patcher-project-option patcher-project :commit-command t))
2803       (and arg
2804            (setq patcher-logmsg-commit-command
2805                  (read-shell-command "Commit command: "
2806                                      patcher-logmsg-commit-command))))
2807     (setq patcher-logmsg-buffer buffer)
2808     (setq patcher-pre-commit-window-config (current-window-configuration))
2809     (pop-to-buffer buffer)
2810     (patcher-logmsg-init-message)))
2811
2812 (defun patcher-generate-diff (&optional arg)
2813   "(Re)generate the diff in the current Patcher mail buffer.
2814 When called interactively, use a prefix to override the diff command
2815 used for this project.
2816
2817 Note that this is *not* the way to specify files affected by this patch.
2818 See the variable `patcher-subprojects' or the function
2819 `patcher-mail-subproject' for that."
2820   (interactive "P")
2821   (when (or (and (not (patcher-extent 'patcher-diff))
2822                  (not patcher-change-logs))
2823             (y-or-n-p "Really regenerate the diff ? "))
2824     (and arg (setq patcher-diff-command
2825                    (read-shell-command "Diff command: " patcher-diff-command)))
2826     (patcher-generate-diff-1)))
2827
2828 (defun patcher-insert-patcher-header ()
2829   ;; Insert a Patcher version header in the message.
2830   (save-excursion
2831     (goto-char (point-min))
2832     (unless (re-search-forward "^X-Generated-By: Patcher " nil t)
2833       ;; This search can fail in case of fake mail method.
2834       (when (re-search-forward
2835              (concat "^" (regexp-quote mail-header-separator)) nil t)
2836         (goto-char (point-at-bol))
2837         (insert "X-Generated-By: " (patcher-version) "\n")))))
2838
2839
2840 (defcustom patcher-minor-mode-string " Patch"
2841   "*Patcher minor mode modeline string."
2842   :group 'patcher
2843   :type 'string)
2844
2845 (defcustom patcher-minor-mode-hook nil
2846   "*Hooks to run after setting up Patcher minor mode."
2847   :group 'patcher
2848   :type 'hook)
2849
2850 (defvar patcher-minor-mode-map
2851   (let ((map (make-sparse-keymap 'patcher-minor-mode-map)))
2852     (define-key map [(control c) (control p) d] 'patcher-generate-diff)
2853     (define-key map [(control c) (control p) i] 'patcher-insert-change-logs)
2854     (define-key map [(control c) (control p) c] 'patcher-commit-change)
2855     (define-key map [(control c) (control p) v] 'patcher-version)
2856     map)
2857   ;; Patcher minor mode keymap.
2858   )
2859
2860 (make-variable-buffer-local
2861  (defvar patcher-minor-mode nil))
2862
2863 (defun patcher-minor-mode (arg)
2864   "Toggles Patcher minor mode.
2865 Used for mails prepared with `patcher-mail'.  You're not supposed to use
2866 this, unless you know what you're doing.
2867
2868 \\{patcher-minor-mode-map}"
2869   (interactive "*P")
2870   (setq patcher-minor-mode
2871         (if (null arg) (not patcher-minor-mode)
2872           (> (prefix-numeric-value arg) 0)))
2873   (patcher-insert-patcher-header)
2874   (run-hooks 'patcher-minor-mode-hook))
2875
2876 (add-minor-mode
2877  'patcher-minor-mode patcher-minor-mode-string patcher-minor-mode-map)
2878
2879
2880 ;; ===========================================================================
2881 ;; Mail preparation routines
2882 ;; ===========================================================================
2883
2884 (patcher-globally-declare-boundp '(message-exit-actions))
2885
2886
2887 (defvar patcher-projects-history nil
2888   ;; History used for prompting patcher projects.
2889   )
2890
2891 (defvar patcher-subjects-history nil
2892   ;; History used for prompting patcher mail subjects.
2893   )
2894
2895 (defgroup patcher-mail nil
2896   "Mailing options for Patcher projects."
2897   :group 'patcher)
2898
2899 (defcustom patcher-mail-check-change-logs-insertion 'ask
2900   "*ChangeLogs insertion checking prior to sending a Patcher mail.
2901
2902 This option affects the behavior of Patcher when ChangeLogs are supposed
2903 to appear by manual insertion into the mail buffer:
2904 - if nil, Patcher never checks that you have inserted them, and lets you
2905   send the message as-is,
2906 - if t, Patcher blindly aborts the sending process if you have forgotten
2907   to insert the ChangeLogs in the message buffer,
2908 - if 'ask (the default), Patcher asks you whether you want to proceed with
2909   sending or not."
2910   :group 'patcher-mail
2911   :type '(radio (const :tag "Never check" nil)
2912                 (const :tag "Abort sending upon omission" t)
2913                 (const :tag "Ask the user" ask)))
2914
2915 (defcustom patcher-mail-check-commit-action 'ask
2916   "*Commit action checking prior to sending a Patcher mail.
2917
2918 This option affects the behavior of Patcher when you have set the
2919 :commit-privilege project option:
2920 - if nil, Patcher never checks that you have commited your changes,
2921   and lets you send the message without having done so,
2922 - if t, Patcher blindly aborts the sending process if you have forgotten
2923   to commit your changes,
2924 - if 'ask (the default), Patcher asks you whether you want to proceed with
2925   sending or not."
2926   :group 'patcher-mail
2927   :type '(radio (const :tag "Never check" nil)
2928                 (const :tag "Abort sending upon omission" t)
2929                 (const :tag "Ask the user" ask)))
2930
2931 (defun patcher-before-send ()
2932   ;; Function hooked in the different mailing methods to perform some
2933   ;; checkings prior to sending the message.
2934   ;; #### NOTE: it is currently impossible (and probably not worth it) to
2935   ;; #### offer an automatic ChangeLog insertion or commit operation at that
2936   ;; #### point: we're already in an interactive call (the message sending
2937   ;; #### pocess) and a complex trickery would be necessary in case of
2938   ;; #### operation failure.  So it's simpler to just abort the sending, let
2939   ;; #### the user manually fix things, and re-send the message.
2940
2941   ;; Check ChangeLogs insertion:
2942   (let ((updating
2943          (patcher-project-option patcher-project :change-logs-updating))
2944         (appearance
2945          (patcher-project-option patcher-project :change-logs-appearance)))
2946     (when (and patcher-mail-check-change-logs-insertion
2947                (eq updating 'automatic) appearance)
2948       (cond ((or (eq appearance 'verbatim) (eq appearance 'packed))
2949              (or (patcher-extent 'patcher-diff)
2950                  (patcher-error "There's no diff in this message !"))
2951              (when (null (patcher-extent 'patcher-change-logs))
2952                (let ((proceed
2953                       (or (null patcher-mail-check-change-logs-insertion)
2954                           (and (eq patcher-mail-check-change-logs-insertion
2955                                    'ask)
2956                                (y-or-n-p "\
2957 You did not insert the ChangeLog entries.  Proceed with sending anyway ? ")))))
2958                  (unless proceed (patcher-error "\
2959 Sending aborted.  Please insert the ChangeLogs first.")))))
2960             ((eq appearance 'patch)
2961              (unless (patcher-change-logs)
2962                (let ((proceed
2963                       (or (null patcher-mail-check-change-logs-insertion)
2964                           (and (eq patcher-mail-check-change-logs-insertion
2965                                    'ask)
2966                                (y-or-n-p "\
2967 You did not insert the ChangeLog entries.  Proceed with sending anyway ? ")))))
2968                  (unless proceed(patcher-error "\
2969 Sending aborted.  Please insert the ChangeLogs first.")))))
2970             (t
2971              (patcher-error "invalid `change-logs-appearance' option: %s"
2972                             appearance)))))
2973   ;; Check commit operation:
2974   (when (and (patcher-project-option patcher-project :commit-privilege)
2975              (not patcher-change-committed))
2976     (let ((proceed (or (null patcher-mail-check-commit-action)
2977                        (and (eq patcher-mail-check-commit-action 'ask)
2978                             (y-or-n-p "\
2979 You did not commit your changes.  Proceed with sending anyway ? ")))))
2980       (unless proceed (patcher-error "\
2981 Sending aborted.  Please commit your changes first.")))))
2982
2983 (defun patcher-after-send (&optional unused)
2984   ;; Function hooked in the different mailing methods to clean up the place
2985   ;; when a Patcher mail is sent.
2986   (setq patcher-instances (remassoc (buffer-name) patcher-instances))
2987   (let ((buffers (patcher-files-buffers patcher-change-logs)))
2988     (cond ((patcher-project-option patcher-project
2989              :kill-change-logs-after-sending)
2990            (patcher-save-buffers buffers)
2991            (dolist (b buffers)
2992              (let ((ac (assoc (buffer-file-name b) patcher-change-logs)))
2993                (when (or (not ac) ;; #### ??????
2994                          (cdr ac))
2995                  (kill-buffer b)))))
2996           (t
2997            (let ((after-save-change-log-hook
2998                   (patcher-project-option patcher-project
2999                     :after-save-change-log-hook)))
3000              (dolist (buffer buffers)
3001                (with-current-buffer buffer
3002                  (dolist (hook after-save-change-log-hook)
3003                    (remove-hook 'after-save-hook hook t))))))))
3004   (when patcher-logmsg-buffer
3005     (kill-buffer patcher-logmsg-buffer))
3006   ;; #### Implement kill-source-files-after-sending here.
3007   (when patcher-pre-commit-window-config
3008     (set-window-configuration patcher-pre-commit-window-config)))
3009
3010 (defun patcher-install-send-hooks ()
3011   ;; Install before- and after-send hooks into the MUA.
3012   (cond ((eq major-mode 'mail-mode)
3013          (add-local-hook 'mail-send-hook 'patcher-before-send)
3014          (push '(patcher-after-send) mail-send-actions))
3015         ((eq major-mode 'message-mode)
3016          (add-local-hook 'message-send-hook 'patcher-before-send)
3017          ;; `message-exit-actions' is probably more appropriate than
3018          ;; `message-send-actions' to perform the cleanup.
3019          (push '(patcher-after-send) message-exit-actions))
3020         (t
3021          (patcher-warning "\
3022 Major mode: %s.
3023 This mailing method is not fully supported by Patcher.
3024 This is not critical though: Patcher won't be able to perform checks or
3025 cleanups during mail sending.
3026
3027 Please report to <didier@xemacs.org>."
3028                           major-mode))))
3029
3030
3031 ;; Patcher FakeMail mode ====================================================
3032
3033 (defun patcher-fakemail-send ()
3034   "Pretend to send a fake Patcher mail.
3035 Only perform the usual cleanup after real Patcher mails are sent."
3036   (interactive)
3037   (patcher-before-send)
3038   (patcher-after-send)
3039   (kill-buffer (current-buffer)))
3040
3041 (defvar patcher-fakemail-mode-map
3042   (let ((map (make-sparse-keymap)))
3043     (define-key map [(control c) (control c)] 'patcher-fakemail-send)
3044     map))
3045
3046 (defun patcher-fakemail-mode ()
3047   "Sets up Patcher-FakeMail major mode.
3048 Used for editing a fake Patcher mail.
3049
3050 \\{patcher-fakemail-mode-map}"
3051   (interactive)
3052   (kill-all-local-variables)
3053   (setq major-mode 'patcher-fakemail)
3054   (setq mode-name "Patcher-FakeMail")
3055   (use-local-map patcher-fakemail-mode-map)
3056   (run-hooks 'patcher-logmsg-mode-hook))
3057
3058
3059 ;; Interface to the different mailing methods ================================
3060
3061 (patcher-globally-declare-fboundp
3062  '(gnus-alive-p gnus gnus-other-frame gnus-post-news
3063                 message-mail message-goto-body))
3064
3065 (defmacro patcher-with-mail-parameters (project &rest body)
3066   ;; Wrap BODY in a let construct possibly defining user-full-name and
3067   ;; user-mail-address by Patcher options.
3068   ;; Return the value of BODY execution.
3069   ;; #### NOTE: why is it called like this ? Because I'm sure one day or
3070   ;; #### another, some sucker will ask for more parameters, like the mail
3071   ;; #### signature for instance ;-)
3072   `(let ((user-full-name (or (patcher-project-option ,project :user-name)
3073                              user-full-name))
3074          (user-mail-address (or (patcher-project-option ,project :user-mail)
3075                                 user-mail-address)))
3076     ,@body))
3077 (put 'patcher-with-mail-parameters 'lisp-indent-function 1)
3078
3079
3080 (defun patcher-mail-compose-mail (project subject)
3081   "Prepare a patch-related mail with the `compose-mail' function.
3082
3083 This function uses the `:to-address' project option to determine the email
3084 address for sending the message.  Otherwise, the address is prompted for.
3085
3086 See also the `mail-user-agent' variable."
3087
3088   (patcher-with-mail-parameters project
3089     (compose-mail (or (patcher-project-option project :to-address)
3090                       (read-string "To address: "))
3091                   subject))
3092   (patcher-install-send-hooks))
3093
3094
3095 (defun patcher-mail-sendmail (project subject)
3096   "Prepare a patch-related mail with the `mail' function.
3097 This method requires the `sendmail' library.
3098
3099 This function uses the `:to-address' project option to determine the email
3100 address for sending the message.  Otherwise, the address is prompted for."
3101   (require 'sendmail)
3102   (patcher-with-mail-parameters project
3103     (mail nil (or (patcher-project-option project :to-address)
3104                   (read-string "To address: "))
3105           subject))
3106   (add-local-hook 'mail-send-hook 'patcher-before-send)
3107   (push '(patcher-after-send) mail-send-actions))
3108
3109 (defun patcher-mail-message (project subject)
3110   "Prepare a patch-related mail with the `message-mail' function.
3111 This method requires the `message' library.
3112
3113 This function uses the `:to-address' project option to determine the email
3114 address for sending the message.  Otherwise, the address is prompted for."
3115   (require 'message)
3116   (patcher-with-mail-parameters project
3117     (message-mail (or (patcher-project-option project :to-address)
3118                       (read-string "To address: "))
3119                   subject))
3120   (add-local-hook 'message-send-hook 'patcher-before-send)
3121   ;; `message-exit-actions' is probably more appropriate than
3122   ;; `message-send-actions' to perform the cleanup.
3123   (push '(patcher-after-send) message-exit-actions))
3124
3125
3126 (defcustom patcher-mail-run-gnus 'prompt
3127   "*Whether Patcher should run Gnus.
3128
3129 The 'gnus mailing method of Patcher needs a running Gnus session.
3130 If Gnus is not running at the time it is needed, Patcher can start
3131 it (or not) depending on this variable:
3132 - if nil, Patcher will abort execution,
3133 - it 'prompt (the default), Patcher will ask you what to do,
3134 - if t Patcher will unconditionally start Gnus.
3135
3136 See also the function `patcher-mail-gnus'."
3137   :group 'patcher-mail
3138   :type '(radio (const :tag "never" nil)
3139                 (const :tag "ask user" prompt)
3140                 (const :tag "as needed" t)))
3141
3142 (defcustom patcher-mail-run-gnus-other-frame t
3143   "*Whether Patcher should start Gnus in a new frame.
3144
3145 This is used in case Patcher has to start Gnus by itself \(see the
3146 variable `patcher-mail-run-gnus').  Possible values are:
3147 - nil:     start Gnus in the current frame,
3148 - t:       start Gnus in a new frame,
3149 - 'follow: start Gnus in a new frame, and also use this frame to prepare
3150            the new Patcher message."
3151   :group 'patcher-mail
3152   :type '(radio (const :tag "Use current frame" nil)
3153                 (const :tag "Create new frame" t)
3154                 (const :tag "Create new frame, and use it for patcher"
3155                        follow)))
3156
3157 (defun patcher-mail-run-gnus ()
3158   ;; Start a gnus session.
3159   (require 'gnus)
3160   (save-excursion
3161     (cond ((eq patcher-mail-run-gnus-other-frame t)
3162            (save-selected-frame (gnus-other-frame)))
3163           ((eq patcher-mail-run-gnus-other-frame 'follow)
3164            (gnus-other-frame))
3165           ((not patcher-mail-run-gnus-other-frame)
3166            (gnus))
3167           (t
3168            (patcher-error "\
3169 Invalid value for `patcher-mail-run-gnus-other-frame': "
3170                           patcher-mail-run-gnus-other-frame)))))
3171
3172 (defun patcher-mail-gnus (project subject)
3173   "Prepare a patch-related mail with the `gnus-post-news' function.
3174 Don't worry, this function can also send mails ;-).  This method
3175 requires that you have Gnus *running* in your XEmacs session \(see
3176 the variable `patcher-mail-run-gnus').
3177
3178 This function uses the `:gnus-group' project option to determine the Gnus
3179 group to use \(as if you had typed `C-u a' on that group in the Group
3180 buffer).  Otherwise, the group is prompted for."
3181   (require 'gnus-util)
3182   (unless (gnus-alive-p)
3183     (cond ((not patcher-mail-run-gnus)
3184            (patcher-error
3185             "The 'gnus mailing method requires a running Gnus session"))
3186           ((eq patcher-mail-run-gnus t)
3187            (patcher-mail-run-gnus))
3188           ((eq patcher-mail-run-gnus 'prompt)
3189            (if (y-or-n-p "Gnus is not currently running.  Start it ? ")
3190                (patcher-mail-run-gnus)
3191              (patcher-error
3192               "The 'gnus mailing method requires a running Gnus session")))
3193           (t
3194            (patcher-error "Invalid value for `patcher-mail-run-gnus': "
3195                           patcher-mail-run-gnus))))
3196   ;; This binding is necessary to let message-mode hooks perform correctly.
3197   (let ((gnus-newsgroup-name (or (patcher-project-option project :gnus-group)
3198                                  (read-string "Gnus group name: ")))
3199         gnus-article-copy)
3200     (patcher-with-mail-parameters project
3201       (gnus-post-news 'post gnus-newsgroup-name)))
3202   (when (patcher-goto-subject)
3203     (insert subject))
3204   (message-goto-body)
3205   (add-local-hook 'message-send-hook 'patcher-before-send)
3206   ;; `message-exit-actions' is probably more appropriate than
3207   ;; `message-send-actions' to perform the cleanup.
3208   (push '(patcher-after-send) message-exit-actions))
3209
3210 (defun patcher-mail-fake (project subject)
3211   "Prepare a patch-related fake mail.
3212 Use this function if you want to do all that Patcher can do, apart from
3213 sending a real mail.  This function generates a fake mail buffer which acts
3214 as a standard Patcher mail buffer, apart from the fact that when you type
3215 \\<patcher-fakemail-mode-map>\\[patcher-fakemail-send] in it, it doesn't
3216 really send a mail, but just clean things up."
3217   (let ((buffer (generate-new-buffer "*Patcher Fake Mail*")))
3218     (switch-to-buffer buffer)
3219     ;; #### NOTE: Patcher asks for a subject even with the fakemail method,
3220     ;; #### which is arguable.  However, even with a fake mail, one could
3221     ;; #### require log message initialization from a fake subject.  We could
3222     ;; #### do something more clever here.
3223     (insert "Subject: " subject "\n")
3224     (patcher-fakemail-mode)))
3225
3226 (defun patcher-mail-setup (project files)
3227   ;; Setup patcher-minor-mode and initialize Patcher local variables in mails
3228   ;; (both generated or adapted).
3229   (push (cons (buffer-name) (current-buffer)) patcher-instances)
3230   (patcher-minor-mode t)
3231   (setq patcher-project project)
3232   (let ((command-directory
3233          (patcher-project-option patcher-project :command-directory)))
3234     (when files (setq files (split-string files)))
3235     (if command-directory
3236         (let ((project-directory (patcher-project-directory project)))
3237           (setq command-directory
3238                 (expand-file-name command-directory project-directory))
3239           (cd command-directory)
3240           (setq patcher-sources
3241                 (if (not files)
3242                     (list (patcher-file-relative-name
3243                            project-directory command-directory 'raw))
3244                   (mapcar
3245                    (lambda (file)
3246                      (patcher-file-relative-name
3247                       (expand-file-name file project-directory)
3248                       command-directory 'raw))
3249                    files))))
3250       ;; else: (null command-directory)
3251       (cd (patcher-project-directory project))
3252       (setq patcher-sources files)))
3253   (setq patcher-diff-command (patcher-project-option project :diff-command t)))
3254
3255
3256 ;; Mail generation entry points =============================================
3257
3258 (defun patcher-mail-1 (project subject files override)
3259   ;; Perform the real job of preparing the mail buffer.
3260   (let ((subject-prefix (patcher-project-option project :subject-prefix))
3261         extent)
3262     ;; Construct the subject, maybe with an extent marking the prefix:
3263     (when (> (length subject-prefix) 0)
3264       (setq subject-prefix (patcher-substitute-name project subject-prefix))
3265       (setq extent (make-extent 0 (length subject-prefix) subject-prefix))
3266       (set-extent-properties extent
3267         '(duplicable t patcher-subject-prefix t)))
3268     (setq subject (concat subject-prefix
3269                           (and (> (length subject-prefix) 0)
3270                                (> (length subject) 0)
3271                                " ")
3272                           subject))
3273     (funcall
3274      (intern (concat "patcher-mail-"
3275                      (symbol-name
3276                       (patcher-project-option project :mail-method t))))
3277      project subject))
3278   (patcher-mail-setup project files)
3279   (let ((mail-prologue (patcher-project-option project :mail-prologue)))
3280     (when (> (length mail-prologue) 0)
3281       (insert "\n" mail-prologue)))
3282   (save-excursion
3283     (insert "\n\n")
3284     (when (patcher-project-option project :change-logs-updating)
3285       (let ((appearance
3286              (patcher-project-option project :change-logs-appearance)))
3287         (when (and appearance (not (eq appearance 'patch)))
3288           (setq patcher-change-logs-marker (point-marker))
3289           (insert "\n"))))
3290     (setq patcher-diff-marker (point-marker))
3291     (patcher-generate-diff override)))
3292
3293
3294 ;;;###autoload
3295 (defun patcher-mail-subproject (project subject files &optional arg)
3296   "Prepare a mail about a patch to apply on part of a project.
3297 PROJECT is the name of the project (see the variables `patcher-projects'
3298 and `patcher-subprojects').
3299 SUBJECT is the subject of the mail.
3300 FILES is a string listing one or more files, possibly with wild cards --
3301   essentially, part of a command line to be interpolated into the `diff'
3302   and maybe the `commit' commands issued by Patcher.
3303
3304 When called interactively, use a prefix (ARG) to override the value of
3305 the diff command to use for this project.
3306
3307 This function is intended for one-time only subprojects.  Alternately, you
3308 can define subprojects in the variable `patcher-subprojects' and continue
3309 using `patcher-mail'.  If you call this function on a predefined subproject,
3310 you will have the opportunity to modify the predefined list of files or
3311 directories the subproject is composed of.
3312
3313 When you use this command instead of `patcher-mail', any commits issued
3314 from the mail buffer (using \\<patcher-minor-mode-map>\\[patcher-commit-change]) will automatically include
3315 the associated ChangeLogs, as well as the file(s) specified as part of
3316 this command.
3317
3318 Please note that you can have multiple occurrences of a Patcher mail at
3319 the same time, but not more than one at a time on the same project unless
3320 you use `patcher-mail-subproject' and the sections of the project don't
3321 overlap."
3322   (interactive
3323    (let* ((prj (assoc (completing-read "Project: " (append patcher-subprojects
3324                                                            patcher-projects)
3325                                        nil t nil 'patcher-projects-history)
3326                       (append patcher-subprojects patcher-projects)))
3327           (sbj (read-string
3328                 "Subject: "
3329                 (let ((s (patcher-project-option prj :subject)))
3330                   (when (> (length s) 0)
3331                     (patcher-substitute-name prj s)))
3332                 patcher-subjects-history))
3333           (dir (patcher-project-directory prj))
3334           (fls (let ((default-directory (file-name-as-directory dir)))
3335                  (or (let ((f (patcher-project-option prj :files)))
3336                        (and f (read-shell-command "Files: " (concat f " ")
3337                                                   nil f)))
3338                      (let* ((default-file (and (buffer-file-name)
3339                                                (patcher-file-relative-name
3340                                                 (buffer-file-name)
3341                                                 dir 'raw)))
3342                             (default-file
3343                               ;; If the file is not actually underneath the
3344                               ;; project, then don't suggest it as a
3345                               ;; possibility.
3346                               (and default-file
3347                                    (if (string-match "^\\.\\.$\\|^\\.\\.[/\\]"
3348                                                      default-file)
3349                                        nil default-file))))
3350                        (read-shell-command
3351                         "Files: "
3352                         default-file nil default-file))))))
3353      (list prj sbj fls current-prefix-arg)))
3354   (patcher-mail-1 project subject files (and (interactive-p) arg)))
3355
3356 ;;;###autoload
3357 (defun patcher-mail (project subject &optional arg)
3358   "Prepare a mail about a patch to apply on a project.
3359 PROJECT is the name of the project (see the variables `patcher-projects'
3360 and `patcher-subprojects').
3361 SUBJECT is the subject of the mail.
3362
3363 When called interactively, use a prefix (ARG) to override the value of
3364 the diff command to use for this project.  Note that this is *not* the way
3365 to restrict the diff to certain files.  If you want to work on a subset of
3366 the project (e.g. some files, subdirectories etc), you have two
3367 alternatives:
3368
3369 - for temporary subprojects, you can use the function
3370   `patcher-mail-subproject', which lets you specify the list of modified
3371   files / directories.
3372 - otherwise, you can also define the subprojects in the variable
3373   `patcher-subprojects' and continue using this function.
3374
3375 Please note that you can have multiple occurrences of a Patcher mail at
3376 the same time, but not more than one at a time on the same project unless
3377 you use `patcher-mail-subproject' and the sections of the project don't
3378 overlap."
3379   (interactive
3380    (let* ((prj (assoc (completing-read "Project: " (append patcher-subprojects
3381                                                            patcher-projects)
3382                                        nil t nil 'patcher-projects-history)
3383                       (append patcher-subprojects patcher-projects)))
3384           (sbj (read-string
3385                 "Subject: "
3386                 (let ((s (patcher-project-option prj :subject)))
3387                   (when (> (length s) 0)
3388                     (patcher-substitute-name prj s)))
3389                 patcher-subjects-history)))
3390      (list prj sbj current-prefix-arg)))
3391   (patcher-mail-1 project subject (patcher-project-option project :files)
3392                   (and (interactive-p) arg)))
3393
3394
3395 ;; Mail adaptation entry points =============================================
3396
3397 ;; #### NOTE: the prefix argument usage in patcher-mail[-subproject]
3398 ;; #### to override the diff command is broken by design (it comes from an
3399 ;; #### early version of Patcher): why the diff command and not any other
3400 ;; #### option ? I'm not going to propagate this misconception here, so the
3401 ;; #### adaptation functions don't have a prefix argument at all.
3402
3403 (defun patcher-mail-adapt-1 (project files)
3404   ;; Like `patcher-mail-1', but for already existing mails.
3405   (let ((subject-prefix (patcher-project-option project :subject-prefix))
3406         extent)
3407     ;; Construct the subject, maybe with an extent marking the prefix:
3408     (when (> (length subject-prefix) 0)
3409       (setq subject-prefix (patcher-substitute-name project subject-prefix))
3410       (setq extent (make-extent 0 (length subject-prefix) subject-prefix))
3411       (set-extent-properties extent '(duplicable t patcher-subject-prefix t))
3412       (when (patcher-goto-subject)
3413         (insert subject-prefix " "))))
3414   (patcher-install-send-hooks)
3415   (patcher-mail-setup project files)
3416   ;; #### FIXME: currently, I have simply discarded the mail-prologue
3417   ;; #### insertion for adapted mails. This is because mail adaptation is
3418   ;; #### mostly for replies in which you probably don't want the standard
3419   ;; #### prologue. However, this could be turned into a standard option.
3420   ;;  (let ((mail-prologue (patcher-project-option project :mail-prologue)))
3421   ;;    (when (> (length mail-prologue) 0)
3422   ;;      (insert "\n" mail-prologue)))
3423   (patcher-goto-signature)
3424   (when (patcher-project-option project :change-logs-updating)
3425     (let ((appearance
3426            (patcher-project-option project :change-logs-appearance)))
3427       (when (and appearance (not (eq appearance 'patch)))
3428         (setq patcher-change-logs-marker (point-marker))
3429         (insert "\n"))))
3430   (setq patcher-diff-marker (point-marker))
3431   (patcher-generate-diff))
3432
3433 ;;;###autoload
3434 (defun patcher-mail-adapt (project)
3435   "Same as `patcher-mail', but for already started mails.
3436 This function is mostly designed to adapt replies or followups probably
3437 started with your usual MUA to Patcher.
3438
3439 Note two differences with `patcher-mail' however:
3440 1. there is no SUBJECT argument to this function,
3441 2. no prefix argument is available to override the diff command."
3442   (interactive
3443    (list (assoc (completing-read "Project: " (append patcher-subprojects
3444                                                      patcher-projects)
3445                                  nil t nil 'patcher-projects-history)
3446                 (append patcher-subprojects patcher-projects))))
3447   (patcher-mail-adapt-1 project (patcher-project-option project :files)))
3448
3449 ;;;###autoload
3450 (defun patcher-mail-adapt-subproject (project files)
3451   "Same as `patcher-mail-subproject', but for already started mails.
3452 This function is mostly designed to adapt replies or followups probably
3453 started with your usual MUA to Patcher.
3454
3455 Note two differences with `patcher-mail-subproject' however:
3456 1. there is no SUBJECT argument to this function,
3457 2. no prefix argument is available to override the diff command."
3458   (interactive
3459    (let* ((prj (assoc (completing-read "Project: " (append patcher-subprojects
3460                                                            patcher-projects)
3461                                        nil t nil 'patcher-projects-history)
3462                       (append patcher-subprojects patcher-projects)))
3463           (dir (patcher-project-directory prj))
3464           (fls (let ((default-directory (file-name-as-directory dir)))
3465                  (or (let ((f (patcher-project-option prj :files)))
3466                        (and f (read-shell-command "Files: " (concat f " ")
3467                                                   nil f)))
3468                      (let* ((default-file (and (buffer-file-name)
3469                                                (patcher-file-relative-name
3470                                                 (buffer-file-name)
3471                                                 dir 'raw)))
3472                             (default-file
3473                               ;; If the file is not actually underneath the
3474                               ;; project, then don't suggest it as a
3475                               ;; possibility.
3476                               (and default-file
3477                                    (if (string-match "^\\.\\.$\\|^\\.\\.[/\\]"
3478                                                      default-file)
3479                                        nil default-file))))
3480                        (read-shell-command
3481                         "Files: "
3482                         default-file nil default-file))))))
3483      (list prj fls)))
3484   (patcher-mail-adapt-1 project files))
3485
3486
3487 ;; Patcher Gnus Summary minor mode ==========================================
3488
3489 (patcher-globally-declare-fboundp
3490  '(gnus-summary-followup gnus-summary-followup-with-original
3491    gnus-summary-reply gnus-summary-reply-with-original))
3492
3493 (defun patcher-gnus-summary-followup (&optional arg)
3494   "Prepare a Patcher followup from the Gnus Summary buffer.
3495 With a prefix argument, behave like `patcher-mail-subproject' instead of
3496 `patcher-mail'."
3497   (interactive "P")
3498   (gnus-summary-followup nil)
3499   (call-interactively
3500    (if arg
3501        'patcher-mail-adapt-subproject
3502      'patcher-mail-adapt)))
3503
3504 (defun patcher-gnus-summary-followup-with-original (&optional arg)
3505   "Prepare a Patcher followup from the Gnus Summary buffer.
3506 The original message is yanked.
3507 With a prefix argument, behave like `patcher-mail-subproject' instead of
3508 `patcher-mail'."
3509   (interactive "P")
3510   (gnus-summary-followup-with-original nil)
3511   (call-interactively
3512    (if arg
3513        'patcher-mail-adapt-subproject
3514      'patcher-mail-adapt)))
3515
3516 (defun patcher-gnus-summary-reply (&optional arg)
3517   "Prepare a Patcher reply from the Gnus Summary buffer.
3518 With a prefix argument, behave like `patcher-mail-subproject' instead of
3519 `patcher-mail'."
3520   (interactive "P")
3521   ;; #### NOTE: it is strange that this function's first argument is not
3522   ;; #### mandatory, as in the 3 other ones.
3523   (gnus-summary-reply)
3524   (call-interactively
3525    (if arg
3526        'patcher-mail-adapt-subproject
3527      'patcher-mail-adapt)))
3528
3529 (defun patcher-gnus-summary-reply-with-original (&optional arg)
3530   "Prepare a Patcher reply from the Gnus Summary buffer.
3531 The original message is yanked.
3532 With a prefix argument, behave like `patcher-mail-subproject' instead of
3533 `patcher-mail'."
3534   (interactive "P")
3535   (gnus-summary-reply-with-original nil)
3536   (call-interactively
3537    (if arg
3538        'patcher-mail-adapt-subproject
3539      'patcher-mail-adapt)))
3540
3541 (defcustom patcher-gnus-summary-minor-mode-string " Patch"
3542   "*Patcher Gnus Summary minor mode modeline string."
3543   :group 'patcher
3544   :type 'string)
3545
3546 (defcustom patcher-gnus-summary-minor-mode-hook nil
3547   "*Hooks to run after setting up Patcher Gnus Summary minor mode."
3548   :group 'patcher
3549   :type 'hook)
3550
3551 (defvar patcher-gnus-summary-minor-mode-map
3552   (let ((map (make-sparse-keymap 'patcher-minor-mode-map)))
3553     (define-key map [(control c) (control p) f]
3554       'patcher-gnus-summary-followup)
3555     (define-key map [(control c) (control p) F]
3556       'patcher-gnus-summary-followup-with-original)
3557     (define-key map [(control c) (control p) r]
3558       'patcher-gnus-summary-reply)
3559     (define-key map [(control c) (control p) R]
3560       'patcher-gnus-summary-reply-with-original)
3561     map)
3562   ;; Patcher Gnus Summary minor mode keymap.
3563   )
3564
3565 (make-variable-buffer-local
3566  (defvar patcher-gnus-summary-minor-mode nil))
3567
3568 (defun patcher-gnus-summary-minor-mode (arg)
3569   "Toggles Patcher Gnus Summary minor mode.
3570 Used for Patcher messages composed as Gnus replies and followups.
3571 You're not supposed to use this, unless you know what you're doing.
3572
3573 \\{patcher-gnus-summary-minor-mode-map}"
3574   (interactive "*P")
3575   (setq patcher-gnus-summary-minor-mode
3576         (if (null arg) (not patcher-gnus-summary-minor-mode)
3577           (> (prefix-numeric-value arg) 0)))
3578   (run-hooks 'patcher-gnus-summary-minor-mode-hook))
3579
3580 (add-minor-mode
3581  'patcher-gnus-summary-minor-mode
3582  patcher-gnus-summary-minor-mode-string
3583  patcher-gnus-summary-minor-mode-map)
3584
3585
3586 ;; Patcher Gnus Article minor mode ==========================================
3587
3588 (defcustom patcher-gnus-article-minor-mode-string " Patch"
3589   "*Patcher Gnus Article minor mode modeline string."
3590   :group 'patcher
3591   :type 'string)
3592
3593 (defcustom patcher-gnus-article-minor-mode-hook nil
3594   "*Hooks to run after setting up Patcher Gnus Article minor mode."
3595   :group 'patcher
3596   :type 'hook)
3597
3598 (defvar patcher-gnus-article-minor-mode-map
3599   (let ((map (make-sparse-keymap 'patcher-minor-mode-map)))
3600     (define-key map [(control c) (control p) f]
3601       'patcher-gnus-summary-followup)
3602     (define-key map [(control c) (control p) F]
3603       'patcher-gnus-summary-followup-with-original)
3604     (define-key map [(control c) (control p) r]
3605       'patcher-gnus-summary-reply)
3606     (define-key map [(control c) (control p) R]
3607       'patcher-gnus-summary-reply-with-original)
3608     map)
3609   ;; Patcher Gnus Article minor mode keymap.
3610   )
3611
3612 (make-variable-buffer-local
3613  (defvar patcher-gnus-article-minor-mode nil))
3614
3615 (defun patcher-gnus-article-minor-mode (arg)
3616   "Toggles Patcher Gnus Article minor mode.
3617 Used for Patcher messages composed as Gnus replies and followups.
3618 You're not supposed to use this, unless you know what you're doing.
3619
3620 \\{patcher-gnus-article-minor-mode-map}"
3621   (interactive "*P")
3622   (setq patcher-gnus-article-minor-mode
3623         (if (null arg) (not patcher-gnus-article-minor-mode)
3624           (> (prefix-numeric-value arg) 0)))
3625   (run-hooks 'patcher-gnus-article-minor-mode-hook))
3626
3627 (add-minor-mode
3628  'patcher-gnus-article-minor-mode
3629  patcher-gnus-article-minor-mode-string
3630  patcher-gnus-article-minor-mode-map)
3631
3632
3633 ;; ==========================================================================
3634 ;; Routines to plug Patcher into external libraries
3635 ;; ==========================================================================
3636
3637 (patcher-globally-declare-boundp
3638  '(gnus-summary-mode-hook gnus-article-mode-hook))
3639
3640 ;;;###autoload
3641 (defun patcher-insinuate-gnus ()
3642   "This function plugs Patcher into Gnus.
3643 It should be called from your gnusrc file."
3644   (add-hook 'gnus-summary-mode-hook
3645             '(lambda () (patcher-gnus-summary-minor-mode 1)))
3646   (add-hook 'gnus-article-mode-hook
3647             '(lambda () (patcher-gnus-article-minor-mode 1))))
3648
3649
3650 (provide 'patcher)
3651
3652 ;;; patcher.el ends here