1 ;;; patcher.el --- Utility for mailing patch information
3 ;; Copyright (C) 2008, 2009, 2010 Didier Verna
4 ;; Copyright (C) 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2007 Didier Verna
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
13 ;; This file is part of Patcher.
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.
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.
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.
31 ;; Contents management by FCM version 0.1.
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.
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.
43 ;; Suggestions for further improvements:
45 ;; #### Check the exit code of processes.
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)
51 ;; #### When the commit command is displayed, it would be nive to also see a
52 ;; list of the files involved, for information.
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.
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.
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
67 ;; #### Investigate on the notion of adding new files (it's different across
70 ;; #### If the user answers `no' to the confirm commit question, it should be
71 ;; possible to edit manually the computed commit command.
73 ;; #### The subject-related strings could benefit from almost all %
76 ;; #### Provide a way to attach patches instead of inserting them as plain
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.
83 ;; #### Before sending the message, we could check that the contents is ok
84 ;; (like, there's no more diff errors and stuff).
86 ;; #### Implement a real error / warning mechanism.
88 ;; #### When a project is found to be out of date, we could implement
89 ;; something to update it and re-run patcher again.
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.
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.
99 ;; #### Also add an option to kill, not just bury, the mail message when
102 ;; #### When the message is sent, the cvs commit results window should be
103 ;; removed and the corresponding buffer buried.
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.
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
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.
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.
156 ;; These macros are copied from bytecomp-runtime.el because they're only
157 ;; available in XEmacs 21.5.
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)))
175 (defmacro patcher-globally-declare-boundp (symbol)
176 (setq symbol (eval symbol))
177 (if (not (consp symbol))
178 (setq symbol (list symbol)))
180 ;; (defvar FOO) has no side effects.
181 ,@(mapcar #'(lambda (sym) `(defvar ,sym)) symbol)))
184 (patcher-globally-declare-boundp 'font-lock-always-fontify-immediately)
188 ;; ===========================================================================
189 ;; Version management
190 ;; ===========================================================================
192 (defconst patcher-version "3.11"
193 "Current version of Patcher.")
196 (defun patcher-version ()
197 "Show the current version of Patcher."
199 (message "Patcher version %s" patcher-version))
203 ;; ===========================================================================
205 ;; ===========================================================================
207 (defsubst patcher-message (msg &rest args)
208 ;; Print a message, letting XEmacs time to display it. Also, handle command
210 (message (substitute-command-keys (apply 'format msg args)))
212 ;; sit-for may change the current buffer and we don't want that.
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))))
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))))
223 (defmacro patcher-with-progression (msg &rest body)
224 ;; wrap BODY in "msg..." / "msg...done" messages.
225 ;; Return the value of BODY execution.
227 (patcher-message (concat ,msg "... please wait."))
229 (patcher-message (concat ,msg "... done."))))
230 (put 'patcher-with-progression 'lisp-indent-function 1)
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
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))))
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 " "))
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)))))
265 (defun patcher-save-buffers (buffers)
266 ;; Offer to save some buffers.
267 ;; #### FIXME: this should be a standard function somewhere.
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))))
282 '("buffer" "buffers" "save")))
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 ?
290 (goto-char (point-min))
291 (setq pos (re-search-forward "^Subject: " nil t)))
292 (and pos (goto-char pos))))
294 (patcher-globally-declare-boundp
295 '(message-signature-separator))
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
306 ((eq major-mode 'message-mode)
307 message-signature-separator)
311 Your mailing method is not fully supported by Patcher.
312 This is not critical though: Patcher may not find the message signature
315 Please report to <didier@xemacs.org>."
317 ;; Use the standard one by default.
320 (goto-char (match-beginning 0))
321 ;; else: no signature
322 (goto-char (point-max))))
326 ;; ===========================================================================
327 ;; Projects description
328 ;; ===========================================================================
330 (defgroup patcher nil
331 "Automatic archive-base project maintenance.")
333 (defgroup patcher-default nil
334 "Default settings for Patcher project options."
337 (defcustom patcher-default-name nil
338 "*Default name for Patcher projects.
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")))
348 (defcustom patcher-default-mail-method 'compose-mail
349 "*Default method used by Patcher to prepare a mail.
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.
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)
366 (symbol :tag "other")))
368 (defcustom patcher-default-user-name nil
369 "*Default user full name to use when sending a Patcher mail.
371 If nil, `user-full-name' is used."
372 :group 'patcher-default
373 :type '(choice (const :tag "user-full-name" nil)
376 (defcustom patcher-default-user-mail nil
377 "*Default user mail address to use when sending a Patcher mail.
379 If nil, `user-mail-address' is used."
380 :group 'patcher-default
381 :type '(choice (const :tag "user-mail-address" nil)
384 (defcustom patcher-default-to-address nil
385 "*Default To: header value to use when sending a Patcher mail.
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)
393 (defcustom patcher-default-gnus-group nil
394 "*Default Gnus group to use when sending a Patcher mail.
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,
400 :group 'patcher-default
401 :type '(choice (const :tag "Ask" nil)
404 (defcustom patcher-default-subject-prefix "[PATCH]"
405 "*Default prefix for the subject of Patcher mails.
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.
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)
420 (defcustom patcher-default-subject-committed-prefix "[COMMIT]"
421 "*Default prefix for the subject of Patcher mails.
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)
429 (defcustom patcher-default-subject nil
430 "*Default subject for Patcher mails.
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.
437 Please note that this is used *only* to provide a default value for prompted
438 subjects. Subjects are *always* prompted for.
440 See also `patcher-default-subject-prefix' and
441 `patcher-default-subject-committed-prefix', which are not subject to
443 :group 'patcher-default
444 :type '(choice (const :tag "None" nil)
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)
453 (defcustom patcher-default-change-logs-updating 'automatic
454 "*Default ChangeLog updating mode.
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
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)))
468 (defcustom patcher-default-change-logs-user-name nil
469 "*Default user full name for generated ChangeLog entries.
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")))
477 (defcustom patcher-default-change-logs-user-mail nil
478 "*Default user mail address for generated ChangeLog entries.
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")))
486 (defcustom patcher-default-change-logs-appearance 'verbatim
487 "*Default appearance of ChangeLog entries in Patcher mails.
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
495 - 'patch: ChangeLog files are diff'ed, and the output appears as part of
497 - nil: ChangeLog entries don't appear at all.
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)))
506 (defcustom patcher-default-change-logs-prologue "%f addition:"
507 "*Default ChangeLogs prologue for every Patcher mail.
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)
516 (defcustom patcher-default-diff-prologue-function
517 'patcher-default-diff-prologue
518 "*Default function used to insert a prologue before each diff output.
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.
526 The following variables are bound (when appropriate) when this function
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.
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")))
541 (defcustom patcher-default-command-directory nil
542 "*Default command directory for Patcher projects.
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")))
552 (defcustom patcher-default-pre-command ""
553 "*Default string to prefix patcher commands with.
555 This is where you would put things like \"runsocks\"."
556 :group 'patcher-default
557 :type '(choice (const :tag "None" nil)
560 (defcustom patcher-default-diff-command nil
561 "*Default method used by Patcher to generate a patch.
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)
574 (defcustom patcher-default-diff-cleaner 'patcher-generic-diff-cleaner
575 "*Default function used for cleaning up a diff.
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)
583 (defcustom patcher-default-diff-header nil
584 "*Default diff header used by Patcher to determine the diff'ed file.
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.
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)
595 (integer :tag "Old file match number")
596 (integer :tag "New file match number"))))
598 (defcustom patcher-default-after-diff-hook nil
599 "*Default hook run on the output of a Patcher diff comand.
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).
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
610 (defcustom patcher-default-notice-change-log-hook nil
611 "*Default hook run every time Patcher notices a new ChangeLog file.
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
617 :group 'patcher-default
620 (defcustom patcher-default-after-save-change-log-hook nil
621 "*Default hook run after a ChangeLog file is saved.
623 The functions in this hook are executed in the ChangeLog's buffer."
624 :group 'patcher-default
627 (defcustom patcher-default-diff-line-filter nil
628 "*Default line filter to pass Patcher diffs through.
630 When inserting a diff in Patcher mails, lines matching this regexp will
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.
636 A value of nil (the default) means no line filter."
637 :group 'patcher-default
638 :type '(choice (const :tag "None" nil)
641 (defcustom patcher-default-change-logs-diff-command 'diff
642 "*Default command to use to generate ChangeLog diffs.
644 This value is used when the ChangeLog appearance is either 'packed or
645 'patch (see the variable `patcher-default-change-logs-appearance').
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.
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.
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")))
662 (defcustom patcher-default-commit-privilege nil
663 "*Default value for Patcher commit privilege status.
665 If you have the privilege to commit patches yourself, you should set
667 :group 'patcher-default
670 (defcustom patcher-default-commit-command nil
671 "*Default method used by Patcher to commit a patch.
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.
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)
695 (defcustom patcher-default-confirm-commits t
696 "*Whether Patcher asks for a confirmation before doing a commit by default."
697 :group 'patcher-default
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)
707 (defcustom patcher-default-failed-command-regexp nil
708 "*Default regular expression for matching the result of a failed command.
710 Commands in question are the diff and the commit one."
711 :group 'patcher-default
712 :type '(choice (const :tag "None" nil)
715 (defcustom patcher-default-log-message-items '(subject)
716 "*Default elements used to initialize a Patcher commit log message.
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
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)))
730 (defcustom patcher-default-change-logs-separator
731 "-------------------- ChangeLog entries follow: --------------------"
732 "*Default ChangeLog entries separator for Patcher commit log messages.
734 Either nil, or a string which will be inserted in a line of its own.
736 See also the function `patcher-logmsg-insert-change-logs'."
737 :group 'patcher-default
738 :type '(choice (const :tag "None" nil)
741 (defcustom patcher-default-edit-log-message t
742 "*Whether Patcher lets you edit the commit log message by default.
744 If nil, Patcher will directly use the initialization value \(see
745 `patcher-default-init-log-message')."
746 :group 'patcher-default
749 (defcustom patcher-default-kill-source-files-after-sending t
750 "*Whether to kill source files after sending the mail be default.
752 This is effective only when sources files have not been killed already
753 \(see the variable `patcher-default-kill-source-files-after-diffing').
755 That feature is not implemented yet."
756 :group 'patcher-default
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
764 (defcustom patcher-default-kill-source-files-after-diffing t
765 "*Whether to kill source files after building the ChangeLog skeletons
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.
772 See also the variable `patcher-default-kill-source-files-after-sending'."
773 :group 'patcher-default
776 (defcustom patcher-default-themes nil
777 "*Default themes to use in Patcher projects.
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")))
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)
795 (defconst patcher-project-options-custom-type
796 '((list :inline t :tag "Project name"
798 (const :tag "" :value :name)
799 (choice (const :tag "Patcher name" nil)
800 (string :tag "Other name")))
801 (list :inline t :tag "Mail method"
803 (const :tag "" :value :mail-method)
804 (choice (const compose-mail)
809 (symbol :tag "other")))
810 (list :inline t :tag "User name"
812 (const :tag "" :value :user-name)
813 (choice (const :tag "user-full-name" nil)
815 (list :inline t :tag "User mail"
817 (const :tag "" :value :user-mail)
818 (choice (const :tag "user-mail-address" nil)
820 (list :inline t :tag "To address"
822 (const :tag "" :value :to-address)
823 (choice (const :tag "Ask" nil)
825 (list :inline t :tag "Gnus group"
827 (const :tag "" :value :gnus-group)
828 (choice (const :tag "Ask" nil)
830 (list :inline t :tag "Subject prefix"
832 (const :tag "" :value :subject-prefix)
833 (choice (const :tag "None" nil)
835 (list :inline t :tag "Subject committed prefix"
837 (const :tag "" :value :subject-committed-prefix)
838 (choice (const :tag "None" nil)
840 (list :inline t :tag "Subject"
842 (const :tag "" :value :subject)
843 (choice (const :tag "None" nil)
845 (list :inline t :tag "Mail prologue"
847 (const :tag "" :value :mail-prologue)
848 (choice (const :tag "None" nil)
850 (list :inline t :tag "ChangeLogs updating"
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"
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"
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"
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"
875 (const :tag "" :value :change-logs-prologue)
876 (choice (const :tag "None" nil)
878 (list :inline t :tag "Diff prologue function"
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"
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"
891 (const :tag "" :value :pre-command)
892 (choice (const :tag "None" nil)
894 (list :inline t :tag "Diff command"
896 (const :tag "" :value :diff-command)
897 ;; #### NOTE: nil forbidden.
899 (list :inline t :tag "Diff cleaner"
901 (const :tag "" :value :diff-cleaner)
902 (choice (const :tag "None" nil)
904 (list :inline t :tag "Diff header"
906 (const :tag "" :value :diff-header)
907 ;; #### NOTE: nil forbidden.
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"
914 (const :tag "" :value :after-diff-hook)
916 (list :inline t :tag "Notice ChangeLog hook"
918 (const :tag "" :value :notice-change-log-hook)
920 (list :inline t :tag "After save ChangeLog hook"
922 (const :tag "" :value :after-save-change-log-hook)
924 (list :inline t :tag "Diff line filter"
926 (const :tag "" :value :diff-line-filter)
927 (choice (const :tag "None" nil)
929 (list :inline t :tag "ChangeLogs diff command"
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"
936 (const :tag "" :value :commit-privilege)
938 (list :inline t :tag "Commit command"
940 (const :tag "" :value :commit-command)
941 ;; #### NOTE: nil forbidden.
943 (list :inline t :tag "Confirm commits"
945 (const :tag "" :value :confirm-commits)
947 (list :inline t :tag "Committed notice"
949 (const :tag "" :value :committed-notice)
950 (choice (const :tag "None" nil)
952 (list :inline t :tag "Failed command regexp"
954 (const :tag "" :value :failed-command-regexp)
955 (choice (const :tag "None" nil)
957 (list :inline t :tag "Log message items"
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"
965 (const :tag "" :value :change-logs-separator)
966 (choice (const :tag "None" nil)
968 (list :inline t :tag "Edit log message"
970 (const :tag "" :value :edit-log-message)
973 :tag "Kill source files after sending"
975 (const :tag "" :value :kill-source-files-after-sending)
978 :tag "Kill changeLogs after sending"
980 (const :tag "" :value :kill-change-logs-after-sending)
983 :tag "Kill source files after diffing"
985 (const :tag "" :value :kill-source-files-after-diffing)
987 (list :inline t :tag "Themes"
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"
1001 ;; Custom type elements for Patcher project options common to
1002 ;; `patcher-projects' and `patcher-subprojects'.
1006 (defgroup patcher-themes nil
1007 "Theme settings for Patcher projects."
1010 (defcustom patcher-themes '()
1011 "*List of themes to use in Patcher projects.
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').
1017 Themes are searched for respectively in this variable and in
1018 `patcher-built-in-themes'.
1020 See also `patcher-max-theme-depth'."
1021 :group 'patcher-themes
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)))))
1033 (defconst patcher-built-in-themes
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))
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"
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"
1051 :diff-command "git diff --no-prefix HEAD%?f{ -- }%f"
1054 \\(\\(deleted file\\|new file\\).*\n\\)?\
1055 \\(similarity \\)?index .*\n\
1056 \\(--- \\|rename from \\)\\(\\S-+\\).*\n\
1057 \\(\\+\\+\\+ \\|rename to \\)\\(\\S-+\\).*"
1059 :change-logs-diff-command "git diff -U0 --no-prefix HEAD%?f{ -- }%f"
1060 :commit-command "git commit %!f{-a }-F %s%?f{ -- }%f")
1062 :diff-command "hg diff --git -wbB %f"
1063 :change-logs-diff-command "hg extdiff -o -wbBtU0 %f"
1064 :themes (mercurial ws))
1066 :diff-command "hg diff --git %f"
1069 \\(\\(deleted file\\|new file\\).*\n\\)?\
1070 \\(--- \\(a/\\)?\\|rename from \\|copy from \\)\\(\\S-+\\).*\n\
1071 \\(\\+\\+\\+ \\(b/\\)?\\|rename to \\|copy to \\)\\(\\S-+\\).*"
1073 :change-logs-diff-command "hg extdiff -o -U0 %f"
1074 :commit-command "hg commit --logfile %s %f")
1076 :diff-command "darcs diff --diff-opts -uwbBt %f"
1077 :change-logs-diff-command "darcs diff --diff-opts -wbBtU0 %f"
1080 :diff-command "darcs diff -u %f"
1083 --- old-.*?/\\(.*?\\)\t.*\n\
1084 \\+\\+\\+ new-.*?/\\(.*?\\)\t.*"
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:")
1090 :diff-command "svn diff -x -uwb %f"
1091 :change-logs-diff-command
1092 "svn diff --diff-cmd /usr/bin/diff -x -wbBtU0 %f"
1095 :diff-command "svn diff -x -u %f"
1097 ^Index: \\(.\\|\n\\)+?\n\
1098 --- \\(.*?\\)\t.*\n\
1099 \\+\\+\\+ \\(.*?\\)\t.*"
1101 :change-logs-diff-command "svn diff --diff-cmd /usr/bin/diff -x -U0 %f"
1102 :commit-command "svn commit -F %s %f")
1104 :diff-command "cvs -q diff -uwbBt %f"
1105 :change-logs-diff-command "cvs -q diff -wbBtU0 %f"
1108 :diff-command "cvs -q diff -u %f"
1110 ^Index: \\(.\\|\n\\)+?\n\
1111 --- \\(.*?\\)\t.*\n\
1112 \\+\\+\\+ \\(.*?\\)\t.*"
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\\]")
1119 :diff-command "prcs diff -f -P %n %f -- -uwbBt"
1120 :change-logs-diff-command "prcs diff -f -P %n %f -- -wbBtU0"
1123 :diff-command "prcs diff -f -P %n %f -- -u"
1126 --- \\(.*?\\)/\\(\\S-+\\) .*\n\
1127 \\+\\+\\+ \\(.*?\\)/\\(\\S-+\\) .*"
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.")
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.
1138 You can add new ones or override these ones in `patcher-themes'.")
1140 (defsubst patcher-themes ()
1141 ;; Return the concatenation of user defined and built-in themes.
1142 (append patcher-themes patcher-built-in-themes))
1146 (defgroup patcher-projects nil
1147 "Project settings for Patcher."
1150 (defcustom patcher-projects '()
1151 "*List of project descriptors.
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).
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-*'
1165 :group 'patcher-projects
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)
1176 (string :tag "Project"))))))))
1178 (defcustom patcher-subprojects '()
1179 "*List of Patcher subproject descriptors.
1181 Subproject descriptors are similar to project descriptors \(see the
1182 variable `patcher-projects') with a few exceptions:
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).
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
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
1215 (list :inline t :tag "Subdirectory"
1216 :format "%{%t%}: %v"
1217 (const :tag "" :value :subdirectory)
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)))))
1226 ;; Project descriptors Accessors =============================================
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.
1232 (defsubst patcher-project-patcher-name (project)
1235 (defsubst patcher-subproject-p (project)
1236 ;; Return non nil if PROJECT is defined in `patcher-subprojects'.
1237 (member project patcher-subprojects))
1239 (defcustom patcher-max-theme-depth 8
1240 "*Maximum nesting level in Patcher themes.
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
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))
1258 (let ((subthemes (member :themes theme-options)))
1259 (when (> level patcher-max-theme-depth)
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'"
1266 (patcher-themes-option
1267 (cadr subthemes) option (1+ level)))))))
1270 (defcustom patcher-max-inheritance-depth 8
1271 "*Maximum nesting level in Patcher projects.
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
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)
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.
1293 (let ((themes (member :themes options)))
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.
1300 (let ((projs (if is-subproject
1301 (list (nth 1 project))
1302 (cadr (member :inheritance options))))
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)
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))
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))
1334 (concat "patcher-default-"
1335 (substring (symbol-name option) 1)))))))
1339 (patcher-error "Project %s: option %s is null"
1340 (patcher-project-patcher-name project)
1342 (put 'patcher-project-option 'lisp-indent-function 1)
1344 (defsubst patcher-project-name (project)
1345 (let ((name (patcher-project-option project :name)))
1346 (or name (patcher-project-patcher-name project))))
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)))
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)))
1358 (expand-file-name subdir (patcher-project-directory prj))
1359 (patcher-project-directory prj))))
1360 ;; else: (member project patcher-projects)
1364 ;; ===========================================================================
1365 ;; Internal utilities
1366 ;; ===========================================================================
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).
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
1379 ;; Buffer local variables ====================================================
1381 ;; The following variables get local values in various Patcher buffers (mail
1382 ;; buffer, process output buffer etc).
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.
1390 (make-variable-buffer-local
1391 (defvar patcher-mail-buffer nil
1392 ;; Mail buffer corresponding to Patcher auxiliary buffers.
1396 ;; Utility functions =========================================================
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)
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))))
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))
1419 (replace-in-string (replace-in-string str "%N" patcher-name)
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
1427 ;; 1/ %n substitution:
1428 (setq command (patcher-substitute-name project command))
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" "")))
1436 ;; 3/ %f substitution (force Unix syntax):
1437 (setq command (replace-in-string command "%f"
1440 (mapconcat #'identity files " ")
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))))
1449 ;; 5/ Final cosmetic cleanup:
1450 (setq command (replace-in-string command "[ \t]+" " " t))
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)))
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))))
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
1479 (setq patcher-project project)
1480 (setq patcher-mail-buffer mail-buffer)
1486 ;; ==========================================================================
1487 ;; ChangeLog buffers
1488 ;; ==========================================================================
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))))
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))))
1505 (invalid-read-syntax nil)
1507 (or (funcall pred num) (beep)))
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)))
1515 (save-window-excursion
1516 (display-buffer change-log t)
1517 (let ((entries (patcher-read-natnum "Number of entries (1): " 1))
1520 (set-buffer change-log)
1523 (goto-char (point-min))
1524 (skip-chars-forward " \n\t")
1525 (unless (looking-at patcher-change-log-entry-start-regexp)
1527 Beginning of buffer doesn't look like a ChangeLog entry."))
1530 (while (> entries 0)
1531 (re-search-forward patcher-change-log-entry-start-regexp)
1532 (setq entries (1- entries)))
1535 Buffer is missing %s ChangeLog entr%s to do the count."
1536 entries (if (= entries 1) "y" "ies"))))
1538 (or (and (re-search-forward
1539 patcher-change-log-entry-start-regexp nil t)
1540 (progn (beginning-of-line) (point)))
1542 (set-extent-properties (setq extent (make-extent beg end))
1543 `(patcher ,mail)))))))
1548 ;; ==========================================================================
1549 ;; The LogMsg buffer
1550 ;; ==========================================================================
1552 (make-variable-buffer-local
1553 (defvar patcher-logmsg-file-name nil
1554 ;; Name of the temporary file where the log message is stored.
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.
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.
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
1576 (regexp-quote prologue) "%f" ".+")
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
1589 (push (match-string 2) files)
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))
1597 (delete-region beg end)
1598 (insert (mapconcat 'identity (nreverse files) ", ") ":")
1599 (when (looking-at "\\s-+")
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!
1607 (looking-at change-log-change-line))
1609 (if (looking-at "[ \t]+")
1610 (delete-region p (match-end 0))))
1611 (delete-region p end)
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)))))
1625 ;; Patcher LogMsg mode ======================================================
1627 (defun patcher-logmsg-insert-subject ()
1628 "Insert the Patcher mail subject into the current LogMsg buffer at point."
1630 (let ((subject "(none)"))
1631 (with-current-buffer patcher-mail-buffer
1633 (let ((extent (patcher-extent 'patcher-subject-prefix)))
1636 (goto-char (extent-end-position extent))
1637 (skip-chars-forward " \t\f\r")
1638 (unless (eq (point) (point-at-eol))
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))
1646 (buffer-substring (point) (point-at-eol)))))))))
1647 (let ((doit (> (length subject) 0)))
1648 (when doit (insert subject))
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
1657 (unless (point-at-bol)
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)
1670 (when (> (length prologue) 0)
1671 (insert (replace-in-string prologue "%f"
1672 (patcher-file-relative-name
1673 (buffer-file-name change-log)))
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))))))
1679 (defun patcher-logmsg-insert-compressed-change-logs ()
1680 "Insert compressed ChangeLog entries in the current Patcher LogMsg buffer."
1682 (let ((beg (point)))
1683 (patcher-logmsg-insert-change-logs)
1684 (narrow-to-region beg (point))
1685 (patcher-logmsg-compress-change-logs)
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.
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."
1700 (let ((output-buffer (patcher-process-output-buffer patcher-mail-buffer))
1701 (log-buffer (current-buffer))
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
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
1724 (goto-char (point-max))
1725 (cond ((looking-at "\\'")
1726 (skip-chars-backward "\n")
1727 (delete-region (point) (1- (point-max))))
1730 (let ((command patcher-logmsg-commit-command))
1732 (let (case-fold-search)
1735 "%S" (shell-quote-argument
1736 (buffer-substring nil (1- (point-max)))) t)))
1738 (replace-in-string command "%s" patcher-logmsg-file-name t))
1740 (patcher-command patcher-project command
1742 (append (mapcar #'patcher-file-relative-name
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)
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
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
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
1787 (with-current-buffer patcher-mail-buffer
1788 (setq patcher-change-committed t)
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))
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))
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)))))
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."
1828 (let ((items (patcher-project-option patcher-project :log-message-items))
1829 (edit-log-message (patcher-project-option patcher-project
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)
1840 ((eq item 'change-logs)
1841 (when inserted (insert "\n\n"))
1842 (patcher-logmsg-insert-change-logs inserted))
1844 (patcher-error "invalid log message item: %s" item))))
1845 (goto-char (point-min))
1846 (if edit-log-message
1848 Edit the log message, and press \\[patcher-logmsg-commit] when done.")
1849 (patcher-logmsg-commit))))
1851 (defcustom patcher-logmsg-mode-hook nil
1852 "*Hook to run after setting up Patcher-Logmsg mode."
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)
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
1872 \\{patcher-logmsg-mode-map}"
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))
1882 ;; ===========================================================================
1883 ;; The Patcher mail buffer
1884 ;; ===========================================================================
1886 (make-variable-buffer-local
1887 (defvar patcher-diff-marker nil
1888 ;; Marker indicating the beginning of the diff.
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'.
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'.
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.
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.
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.
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.
1935 (defmacro patcher-with-information (information &rest body)
1936 `(save-window-excursion
1938 (with-output-to-temp-buffer
1939 " *Patcher Information*"
1940 (set-buffer " *Patcher Information*")
1941 (insert ,information))
1943 (put 'patcher-with-information 'lisp-indent-function 1)
1945 (defsubst patcher-delete-extent-and-region (extent)
1946 ;; Delete EXTENT and the corresponding region.
1948 (delete-region (extent-start-position extent) (extent-end-position extent)
1949 (extent-object extent))
1950 (delete-extent extent)))
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
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)))
1969 old-file old-absfile old-dirname
1970 new-file new-absfile new-dirname
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")
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")
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)))
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.
1999 (cd (expand-file-name (or new-dirname old-dirname)
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))
2009 (or new-absfile old-absfile))))
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)))))))
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)
2033 (patcher-project-option patcher-project :diff-cleaner))
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
2040 (beginning-of-buffer)
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
2048 :my-email (or (patcher-project-option project
2049 :change-logs-user-mail)
2050 (patcher-project-option project
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
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)))))))
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))))
2086 (defmacro patcher-map-change-log-extents (&optional buffer &rest body)
2087 ;; Map BODY over all extents marking a ChangeLog contents in BUFFER.
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)
2093 (defmacro patcher-map-source-extents (&optional buffer &rest body)
2094 ;; Map BODY over all extents marking a source contents in BUFFER.
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)
2100 (defun patcher-change-logs (&optional buffer)
2101 ;; Return the list of ChangeLog absolute file names appearing in BUFFER
2102 ;; (current buffer by default).
2104 (patcher-map-change-log-extents buffer
2105 (let ((change-log (extent-property extent 'patcher-change-log)))
2106 (push change-log change-logs)))
2109 (defun patcher-sources (&optional buffer)
2110 ;; Return the list of source absolute file names appearing in BUFFER
2111 ;; (current buffer by default).
2113 (patcher-map-source-extents buffer
2114 (let ((source (extent-property extent 'patcher-source)))
2115 (push source sources)))
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))))
2124 (patcher-globally-declare-boundp
2125 '(name source-diff source-files change-log-diff change-log-files))
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"
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"
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"))
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.
2156 (goto-char patcher-diff-marker)
2157 (let ((font-lock-always-fontify-immediately t)
2158 (pos (with-current-buffer buffer (goto-char (point-min))))
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))))
2171 (defun patcher-generic-diff-cleaner (diff-header &optional beg end)
2172 "Patcher default post-processor for diffs.
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)
2183 (concat "--- \\" (number-to-string old-file-match) "\n"
2184 "+++ \\" (number-to-string new-file-match))))))
2186 (defun patcher-git-intent-to-add (file)
2187 "Signal our intention to add FILE to the Git index (add -N)."
2189 (patcher-call-process (concat "git add -N -- " file))))
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))))
2195 (patcher-call-process (concat "git add -- " file)))))
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
2202 (with-current-buffer buffer
2203 (let ((after-diff-hook (patcher-project-option patcher-project
2205 (when after-diff-hook
2206 (patcher-with-progression "Running after diff hooks"
2208 (mapcar (lambda (func)
2209 (goto-char (point-min))
2210 (funcall func beg end))
2211 after-diff-hook)))))))
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))
2219 (patcher-command patcher-project patcher-diff-command
2220 (when patcher-sources
2221 (append (mapcar #'patcher-file-relative-name
2222 patcher-change-logs)
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)
2231 Error during diff. Please fix the problem and type \
2232 \\[patcher-generate-diff] to try again."))
2233 (patcher-insert-diff buffer)))
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.
2239 (patcher-project-option patcher-project :change-logs-prologue)))
2240 (patcher-with-progression "Inserting ChangeLog contents"
2242 (goto-char patcher-change-logs-marker)
2243 (dolist (change-log (patcher-files-buffers patcher-change-logs 'find))
2245 (patcher-change-log-extent change-log (current-buffer)))
2248 (when (> (length prologue) 0)
2249 (insert (replace-in-string prologue "%f"
2250 (patcher-file-relative-name
2251 (buffer-file-name change-log)))
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))))))
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)))
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)))))
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))
2282 (patcher-run-after-diff-hook buffer)
2283 (when (patcher-parse-region nil nil buffer)
2284 (display-buffer buffer t)
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).
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)))))
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"
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))
2309 (patcher-delete-extent-and-region extent)
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)))))
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
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
2327 (goto-char (extent-start-position extent))
2328 (re-search-forward failed-command-regexp
2329 (extent-end-position extent)
2333 (error (- new-error old-error)))
2335 (set-extent-property extent 'patcher-error t))
2337 (set-extent-property extent 'patcher-error nil)))
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.
2344 (let ((diff-extent (patcher-extent 'patcher-diff))
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)
2370 (set-extent-property (patcher-extent 'patcher-diff)
2371 'patcher-error errors)
2373 Problems during diff. \
2374 Please type \\[patcher-insert-change-logs] to try again.")))))
2376 (defun patcher-insert-diff-prologue (command)
2377 ;; Insert a prologue at the top of the diff in the current Patcher mail
2379 (let ((function (patcher-project-option patcher-project
2380 :diff-prologue-function)))
2382 (let ((extent (patcher-extent 'patcher-diff))
2383 (name (patcher-project-name patcher-project))
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))
2392 (and (stringp command)
2393 (patcher-command patcher-project command))))
2394 (set-extent-property extent 'start-open nil)
2396 (goto-char patcher-diff-marker)
2397 (funcall function (if (symbolp command) command 'mixed)))
2398 (set-extent-property extent 'start-open t)))))
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)
2409 (patcher-run-after-diff-hook buffer)
2410 (when (patcher-parse-region nil nil buffer)
2411 (display-buffer buffer t)
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"
2431 'notice-change-log-hook
2432 (patcher-file-relative-name change-log)))))))))
2434 (defun patcher-change-logs-diff-error ()
2436 Patcher has detected a ChangeLog diff. This can mean two things:
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
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."))
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.
2457 (patcher-delete-extent-and-region (patcher-extent 'patcher-diff)))
2459 ;; We don't do ChangeLogs: just (re)diff the project.
2461 (patcher-diff-base buffer)
2462 (patcher-insert-diff buffer)
2463 (patcher-insert-diff-prologue 'sources)
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
2474 (cond ((or (eq appearance 'verbatim)
2475 (eq appearance 'packed))
2476 (let ((generate-change-logs t)
2478 (patcher-extent 'patcher-change-logs)))
2480 (patcher-with-information
2482 ChangeLog skeletons for this patch have already been generated%s.
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
2507 (patcher-generate-change-logs patcher-diff-marker
2508 (extent-end-position
2512 Please annotate the ChangeLog skeletons, \
2513 and type \\[patcher-insert-change-logs] to %s them."
2514 (if (eq appearance 'verbatim)
2517 ;; not generate-change-logs
2518 (if change-logs-extent
2520 To commit your changes, type \\[patcher-commit-change].")
2522 Please type \\[patcher-insert-change-logs] to %s the ChangeLogs"
2523 (if (eq appearance 'verbatim)
2526 ((eq appearance 'patch)
2527 (let ((generate-change-logs t))
2529 (patcher-with-information "\
2530 ChangeLog skeletons for this patch have already been generated.
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
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)
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)
2560 (patcher-insert-diff-prologue 'mixed))
2563 (patcher-convert-change-log-diffs command)
2564 (patcher-insert-diff-prologue command))
2567 invalid `change-logs-diff-command' option: %s" command))))
2569 To commit your changes, type \\[patcher-commit-change]."))))
2571 (let ((generate-change-logs t))
2573 (patcher-with-information "\
2574 ChangeLog skeletons for this patch have already been generated.
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
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
2592 (patcher-insert-diff buffer)
2593 (patcher-insert-diff-prologue 'sources)
2594 (patcher-generate-change-logs patcher-diff-marker
2595 (extent-end-position
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)
2606 To commit your changes, type \\[patcher-commit-change]."))))
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))))
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.
2642 (patcher-remove-change-logs buffer))
2643 (patcher-insert-diff buffer)
2644 (patcher-insert-diff-prologue 'sources)
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)
2653 (patcher-diff-change-logs
2654 patcher-diff-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)
2662 (patcher-diff-change-logs command)))
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)
2671 ;; Some ChangeLog entries might not be
2672 ;; present, so we must rediff the whole
2676 (patcher-insert-diff-prologue 'mixed))
2677 ;; Otherwise, the ChangeLog entries are in the
2679 (patcher-insert-diff buffer)
2680 (patcher-insert-diff-prologue 'mixed)))
2682 (if (not patcher-sources)
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
2692 (patcher-insert-diff-prologue command)
2693 (patcher-convert-change-log-diffs command)))
2696 invalid `change-logs-diff-command' option: %s" command)))))
2698 (patcher-remove-change-logs buffer)
2699 (patcher-insert-diff buffer)
2700 (patcher-insert-diff-prologue 'sources))
2703 invalid `change-logs-appearance' option: %s"
2706 To commit your changes, type \\[patcher-commit-change]."))
2708 (patcher-error "invalid `change-logs-updating' option: %s"
2712 ;; Patcher minor-mode ========================================================
2714 (defun patcher-insert-change-logs ()
2715 "(Re)Insert ChangeLog entries in the current Patcher mail buffer."
2718 (or (patcher-project-option patcher-project :change-logs-updating)
2719 (patcher-error "This project does not handle ChangeLogs")))
2721 (or (patcher-project-option patcher-project :change-logs-appearance)
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 ? "))))
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 ? "))))
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))
2749 (patcher-diff-change-logs command))
2752 invalid `change-logs-diff-command' option: %s" command)))))))
2753 ((eq appearance 'patch)
2754 (when (or (not (patcher-change-logs))
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)
2763 (patcher-insert-diff-prologue 'mixed))
2766 (patcher-convert-change-log-diffs command)
2767 (patcher-insert-diff-prologue command))
2770 invalid `change-logs-diff-command' option: %s" command))))))
2772 (patcher-error "invalid `change-logs-appearance' option: %s"
2775 (patcher-error "invalid `change-logs-updating' option: %s"
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.
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."
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)
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)))
2801 (setq patcher-logmsg-commit-command
2802 (patcher-project-option patcher-project :commit-command t))
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)))
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.
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."
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)))
2828 (defun patcher-insert-patcher-header ()
2829 ;; Insert a Patcher version header in the message.
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")))))
2840 (defcustom patcher-minor-mode-string " Patch"
2841 "*Patcher minor mode modeline string."
2845 (defcustom patcher-minor-mode-hook nil
2846 "*Hooks to run after setting up Patcher minor mode."
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)
2857 ;; Patcher minor mode keymap.
2860 (make-variable-buffer-local
2861 (defvar patcher-minor-mode nil))
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.
2868 \\{patcher-minor-mode-map}"
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))
2877 'patcher-minor-mode patcher-minor-mode-string patcher-minor-mode-map)
2880 ;; ===========================================================================
2881 ;; Mail preparation routines
2882 ;; ===========================================================================
2884 (patcher-globally-declare-boundp '(message-exit-actions))
2887 (defvar patcher-projects-history nil
2888 ;; History used for prompting patcher projects.
2891 (defvar patcher-subjects-history nil
2892 ;; History used for prompting patcher mail subjects.
2895 (defgroup patcher-mail nil
2896 "Mailing options for Patcher projects."
2899 (defcustom patcher-mail-check-change-logs-insertion 'ask
2900 "*ChangeLogs insertion checking prior to sending a Patcher mail.
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
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)))
2915 (defcustom patcher-mail-check-commit-action 'ask
2916 "*Commit action checking prior to sending a Patcher mail.
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
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)))
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.
2941 ;; Check ChangeLogs insertion:
2943 (patcher-project-option patcher-project :change-logs-updating))
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))
2953 (or (null patcher-mail-check-change-logs-insertion)
2954 (and (eq patcher-mail-check-change-logs-insertion
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)
2963 (or (null patcher-mail-check-change-logs-insertion)
2964 (and (eq patcher-mail-check-change-logs-insertion
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.")))))
2971 (patcher-error "invalid `change-logs-appearance' option: %s"
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)
2979 You did not commit your changes. Proceed with sending anyway ? ")))))
2980 (unless proceed (patcher-error "\
2981 Sending aborted. Please commit your changes first.")))))
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)
2992 (let ((ac (assoc (buffer-file-name b) patcher-change-logs)))
2993 (when (or (not ac) ;; #### ??????
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)))
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))
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.
3027 Please report to <didier@xemacs.org>."
3031 ;; Patcher FakeMail mode ====================================================
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."
3037 (patcher-before-send)
3038 (patcher-after-send)
3039 (kill-buffer (current-buffer)))
3041 (defvar patcher-fakemail-mode-map
3042 (let ((map (make-sparse-keymap)))
3043 (define-key map [(control c) (control c)] 'patcher-fakemail-send)
3046 (defun patcher-fakemail-mode ()
3047 "Sets up Patcher-FakeMail major mode.
3048 Used for editing a fake Patcher mail.
3050 \\{patcher-fakemail-mode-map}"
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))
3059 ;; Interface to the different mailing methods ================================
3061 (patcher-globally-declare-fboundp
3062 '(gnus-alive-p gnus gnus-other-frame gnus-post-news
3063 message-mail message-goto-body))
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)
3074 (user-mail-address (or (patcher-project-option ,project :user-mail)
3075 user-mail-address)))
3077 (put 'patcher-with-mail-parameters 'lisp-indent-function 1)
3080 (defun patcher-mail-compose-mail (project subject)
3081 "Prepare a patch-related mail with the `compose-mail' function.
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.
3086 See also the `mail-user-agent' variable."
3088 (patcher-with-mail-parameters project
3089 (compose-mail (or (patcher-project-option project :to-address)
3090 (read-string "To address: "))
3092 (patcher-install-send-hooks))
3095 (defun patcher-mail-sendmail (project subject)
3096 "Prepare a patch-related mail with the `mail' function.
3097 This method requires the `sendmail' library.
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."
3102 (patcher-with-mail-parameters project
3103 (mail nil (or (patcher-project-option project :to-address)
3104 (read-string "To address: "))
3106 (add-local-hook 'mail-send-hook 'patcher-before-send)
3107 (push '(patcher-after-send) mail-send-actions))
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.
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."
3116 (patcher-with-mail-parameters project
3117 (message-mail (or (patcher-project-option project :to-address)
3118 (read-string "To address: "))
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))
3126 (defcustom patcher-mail-run-gnus 'prompt
3127 "*Whether Patcher should run Gnus.
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.
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)))
3142 (defcustom patcher-mail-run-gnus-other-frame t
3143 "*Whether Patcher should start Gnus in a new frame.
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"
3157 (defun patcher-mail-run-gnus ()
3158 ;; Start a gnus session.
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)
3165 ((not patcher-mail-run-gnus-other-frame)
3169 Invalid value for `patcher-mail-run-gnus-other-frame': "
3170 patcher-mail-run-gnus-other-frame)))))
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').
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)
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)
3192 "The 'gnus mailing method requires a running Gnus session")))
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: ")))
3200 (patcher-with-mail-parameters project
3201 (gnus-post-news 'post gnus-newsgroup-name)))
3202 (when (patcher-goto-subject)
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))
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)))
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
3242 (list (patcher-file-relative-name
3243 project-directory command-directory 'raw))
3246 (patcher-file-relative-name
3247 (expand-file-name file project-directory)
3248 command-directory 'raw))
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)))
3256 ;; Mail generation entry points =============================================
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))
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)
3274 (intern (concat "patcher-mail-"
3276 (patcher-project-option project :mail-method t))))
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)))
3284 (when (patcher-project-option project :change-logs-updating)
3286 (patcher-project-option project :change-logs-appearance)))
3287 (when (and appearance (not (eq appearance 'patch)))
3288 (setq patcher-change-logs-marker (point-marker))
3290 (setq patcher-diff-marker (point-marker))
3291 (patcher-generate-diff override)))
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.
3304 When called interactively, use a prefix (ARG) to override the value of
3305 the diff command to use for this project.
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.
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
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
3323 (let* ((prj (assoc (completing-read "Project: " (append patcher-subprojects
3325 nil t nil 'patcher-projects-history)
3326 (append patcher-subprojects patcher-projects)))
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 " ")
3338 (let* ((default-file (and (buffer-file-name)
3339 (patcher-file-relative-name
3343 ;; If the file is not actually underneath the
3344 ;; project, then don't suggest it as a
3347 (if (string-match "^\\.\\.$\\|^\\.\\.[/\\]"
3349 nil default-file))))
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)))
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.
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
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.
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
3380 (let* ((prj (assoc (completing-read "Project: " (append patcher-subprojects
3382 nil t nil 'patcher-projects-history)
3383 (append patcher-subprojects patcher-projects)))
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)))
3395 ;; Mail adaptation entry points =============================================
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.
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))
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)
3426 (patcher-project-option project :change-logs-appearance)))
3427 (when (and appearance (not (eq appearance 'patch)))
3428 (setq patcher-change-logs-marker (point-marker))
3430 (setq patcher-diff-marker (point-marker))
3431 (patcher-generate-diff))
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.
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."
3443 (list (assoc (completing-read "Project: " (append patcher-subprojects
3445 nil t nil 'patcher-projects-history)
3446 (append patcher-subprojects patcher-projects))))
3447 (patcher-mail-adapt-1 project (patcher-project-option project :files)))
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.
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."
3459 (let* ((prj (assoc (completing-read "Project: " (append patcher-subprojects
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 " ")
3468 (let* ((default-file (and (buffer-file-name)
3469 (patcher-file-relative-name
3473 ;; If the file is not actually underneath the
3474 ;; project, then don't suggest it as a
3477 (if (string-match "^\\.\\.$\\|^\\.\\.[/\\]"
3479 nil default-file))))
3482 default-file nil default-file))))))
3484 (patcher-mail-adapt-1 project files))
3487 ;; Patcher Gnus Summary minor mode ==========================================
3489 (patcher-globally-declare-fboundp
3490 '(gnus-summary-followup gnus-summary-followup-with-original
3491 gnus-summary-reply gnus-summary-reply-with-original))
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
3498 (gnus-summary-followup nil)
3501 'patcher-mail-adapt-subproject
3502 'patcher-mail-adapt)))
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
3510 (gnus-summary-followup-with-original nil)
3513 'patcher-mail-adapt-subproject
3514 'patcher-mail-adapt)))
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
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)
3526 'patcher-mail-adapt-subproject
3527 'patcher-mail-adapt)))
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
3535 (gnus-summary-reply-with-original nil)
3538 'patcher-mail-adapt-subproject
3539 'patcher-mail-adapt)))
3541 (defcustom patcher-gnus-summary-minor-mode-string " Patch"
3542 "*Patcher Gnus Summary minor mode modeline string."
3546 (defcustom patcher-gnus-summary-minor-mode-hook nil
3547 "*Hooks to run after setting up Patcher Gnus Summary minor mode."
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)
3562 ;; Patcher Gnus Summary minor mode keymap.
3565 (make-variable-buffer-local
3566 (defvar patcher-gnus-summary-minor-mode nil))
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.
3573 \\{patcher-gnus-summary-minor-mode-map}"
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))
3581 'patcher-gnus-summary-minor-mode
3582 patcher-gnus-summary-minor-mode-string
3583 patcher-gnus-summary-minor-mode-map)
3586 ;; Patcher Gnus Article minor mode ==========================================
3588 (defcustom patcher-gnus-article-minor-mode-string " Patch"
3589 "*Patcher Gnus Article minor mode modeline string."
3593 (defcustom patcher-gnus-article-minor-mode-hook nil
3594 "*Hooks to run after setting up Patcher Gnus Article minor mode."
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)
3609 ;; Patcher Gnus Article minor mode keymap.
3612 (make-variable-buffer-local
3613 (defvar patcher-gnus-article-minor-mode nil))
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.
3620 \\{patcher-gnus-article-minor-mode-map}"
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))
3628 'patcher-gnus-article-minor-mode
3629 patcher-gnus-article-minor-mode-string
3630 patcher-gnus-article-minor-mode-map)
3633 ;; ==========================================================================
3634 ;; Routines to plug Patcher into external libraries
3635 ;; ==========================================================================
3637 (patcher-globally-declare-boundp
3638 '(gnus-summary-mode-hook gnus-article-mode-hook))
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))))
3652 ;;; patcher.el ends here