Commits

Anonymous committed b9d570b

Patcher 3.5

Comments (0)

Files changed (4)

+2003-07-29  Didier Verna  <didier@xemacs.org>
+
+	* Patcher 3.5 is released.
+
 2003-05-18  Norbert Koch  <viteno@xemacs.org>
 
 	* Makefile (VERSION): XEmacs package 1.55 released.
 
 ;; Patcher is distributed in the hope that it will be useful,
 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
-;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 ;; GNU General Public License for more details.
 
 ;; You should have received a copy of the GNU General Public License
 
 ;; Suggestions for further improvements:
 
+
+;; #### Implement a default theme for XEmacs developpers / committers.
+
+;; #### Investigate on the notion of adding new files (it's different across
+;; RCSes).
+
+;; #### If the user answers `no' to the confirm commit question, it should be
+;; possible to edit manually the computed commit command.
+
+;; #### The subject-related strings could benefit from almost all %
+;; constructs.
+
 ;; #### Provide a way to attach patches instead of inserting them as plain
 ;; text.
 
 ;; different one if the user don't want to start Gnus.
 ;;
 ;; #### Consider doing a better job of handling overlapping patcher
-;; instances.  e.g. use a different extent property for each instance,
+;; instances. e.g. use a different extent property for each instance,
 ;; and keep a usage count for the ChangeLog files so that they're only
 ;; killed when no instance uses them any more.
 ;;
 ;;
 ;; #### Have a control window to clarify the progress of everything.
 ;; Make it perhaps a small buffer, above the mail message/ChangeLog
-;; buffer.  It shows what steps have been completed, what haven't, and
-;; what to do.  It should have buttons in it for the various actions.
+;; buffer. It shows what steps have been completed, what haven't, and
+;; what to do. It should have buttons in it for the various actions.
 ;; One button is for include-changelogs, one for commit, one for send
-;; the mail, and one for execute the commit.  These should be enabled
-;; or grayed out appropriately.  It should also have buttons that show
+;; the mail, and one for execute the commit. These should be enabled
+;; or grayed out appropriately. It should also have buttons that show
 ;; the associated changelogs; clicking on a button puts that changelog
-;; buffer in the main window below the control window.  By each
+;; buffer in the main window below the control window. By each
 ;; changelog button should be an indication of whether the changelog
-;; has been modified so far.  The control window should stay around as
+;; has been modified so far. The control window should stay around as
 ;; much as it can during the whole process (e.g. use
 ;; set-buffer-dedicated-p or something), so that it's always there to
-;; drive the whole process.  One corollary is that you don't actually
+;; drive the whole process. One corollary is that you don't actually
 ;; have to switch to the mail buffer to (e.g.) execute
 ;; include-changelogs -- you just click on the control window, and it
-;; does it automatically.  also, when you execute include-changelogs,
+;; does it automatically. also, when you execute include-changelogs,
 ;; it can issue a warning if not all changelogs have been visited, and
-;; prompt you to make sure you want to continue.  Similarly, you can
+;; prompt you to make sure you want to continue. Similarly, you can
 ;; run include-changelogs more than once even if it succeeded the
 ;; first time (e.g. for some reason you didn't like the result and
-;; deleted it), but it will prompt "are you sure?".  there could also
+;; deleted it), but it will prompt "are you sure?". There could also
 ;; be an "undo include-changelogs", if you make a mistake after doing
 ;; include-changelogs and realize you want to go back and fix the
 ;; problem and do include-changelogs again.
 
 
+
+;; Internal notes:
+
+;; - See why the ChangeLogs are represented in absolute path.
+
+
+
 ;; Thanks to these people for their suggestions, testing and contributions:
 
+;; Adrian Aichner      <adrian@xemacs.org>,
 ;; Ben Wing            <ben@xemacs.org>,
-;; Adrian Aichner      <adrian@xemacs.org>,
+;; Karl Pflasterer     <sigurd@12move.de>
+;; Malcolm Purvis      <malcolmpurvis@optushome.com.au>
+;; Norbert Koch        <nk@viteno.net>
+;; Raphael Poss        <poss_r@epita.fr>
+;; Stephen J. Turnbull <stephen@xemacs.org>
 ;; Steve Youngs        <youngs@xemacs.org>,
-;; Stephen J. Turnbull <stephen@xemacs.org>
-;; Norbert Koch        <nk@viteno.net>
-;; Malcolm Purvis      <malcolmpurvis@optushome.com.au>
-;; Karl Pflasterer     <sigurd@12move.de>
 
 
 ;;; Code:
     (setq symbol (eval symbol))
     (if (not (consp symbol))
 	(setq symbol (list symbol)))
-    ;; Another hack.  This works because the autoload environment is
+    ;; Another hack. This works because the autoload environment is
     ;; currently used ONLY to suppress warnings, and the actual
     ;; autoload definition is not used. (NOTE: With this definition,
     ;; we will get spurious "multiple autoloads for %s" warnings if we
 ;; ===========================================================================
 
 ;; $Format: "(defconst patcher-prcs-major-version \"$ProjectMajorVersion$\")"$
-(defconst patcher-prcs-major-version "version-3-4")
+(defconst patcher-prcs-major-version "version-3-5")
 ;; $Format: "(defconst patcher-prcs-minor-version \"$ProjectMinorVersion$\")"$
-(defconst patcher-prcs-minor-version "2")
+(defconst patcher-prcs-minor-version "1")
 (defconst patcher-version
   (let ((level patcher-prcs-minor-version)
 	major minor status)
 
 
 ;; ===========================================================================
-;; Internal utilities
+;; General utilities
 ;; ===========================================================================
 
-;; #### NOTE: this is currently useless.
-(defvar patcher-instances nil
-  ;; A list of all alive instances of Patcher (an instance is dead after the
-  ;; mail has been sent. Each element is of the form '(BUFFER_NAME . BUFFER).
-  )
-
-(defconst patcher-change-log-entry-start-regexp
-  "^[0-9]\\{4,4\\}-[0-9]\\{2,2\\}-[0-9]\\{2,2\\} "
-  ;; Regexp matching the beginning of a ChangeLog entry
-  )
-
-;; Buffer local variables ====================================================
-
-;; The following variables get local values in various Patcher buffers (mail
-;; buffer, process output buffer etc).
-
-(make-variable-buffer-local
- (defvar patcher-project nil
-   ;; Patcher project related to the current patch. This is also set in
-   ;; auxiliary buffers.
-   ))
-
-(make-variable-buffer-local
- (defvar patcher-mail-buffer nil
-   ;; Mail buffer corresponding to Patcher auxiliary buffers.
-   ))
-
-
-;; Utility functions =========================================================
-
-;;(defun patcher-keyword-value (keyword values)
-;;  ;; Return the value of KEYWORD from a (KEY VAL ...) list. VAL may be omitted
-;;  ;; in the list, in which case t is returned.
-;;  (let ((vals values)
-;;	key val)
-;;    (catch 'found
-;;      (while (setq key (pop vals))
-;;	(setq val (or (not (car vals))
-;;		      (if (keywordp (car vals)) t (pop vals))))
-;;	(and (eq keyword key)
-;;	     (throw 'found val))))
-;;    ))
-
 (defsubst patcher-error (msg &rest args)
   ;; Like `patcher-message, but triggers a Patcher error instead.
   (error (substitute-command-keys (apply 'format msg args))))
   ;; wrap BODY in "msg..." / "msg...done" messages.
   ;; Return the value of BODY execution.
   `(prog2
-       (patcher-message (concat ,msg "..."))
+       (patcher-message (concat ,msg "... please wait."))
        (progn ,@body)
-     (patcher-message (concat ,msg "...done"))))
+     (patcher-message (concat ,msg "... done."))))
 (put 'patcher-with-progression 'lisp-indent-function 1)
 
 
    '("buffer" "buffers" "save")))
 
 
-(defsubst patcher-call-process (command &optional buffer)
-  ;; Call a shell process which executes COMMAND (a string) with output to
-  ;; BUFFER (the current buffer by default).
-  (apply 'call-process shell-file-name nil (or buffer t) nil
-	 shell-command-switch (list command)))
-
-(defun patcher-command (command files)
-  ;; Replace the %f in the string COMMAND with the string FILES (if any), or
-  ;; append it to the end of COMMAND.
-  (or files (setq files ""))
-  ;; force Unix syntax
-  (setq files (replace-in-string files "\\\\" "/"))
-  (cond ((string-match "%f" command)
-	 (replace-in-string (replace-in-string command "%f" files)
-			    "[ \t]+$" ""))
-	((> (length files) 0)
-	 (concat command " " files))
-	(t command)))
-
-(defsubst patcher-extent (property &optional value buffer)
-  ;; Get the extent that has PROPERTY set to VALUE (t if not given) in BUFFER
-  ;; (current buffer if nil).
-  (car (mapcar-extents #'identity nil buffer nil nil nil
-		       property (or value t))))
-
-(defun patcher-process-output-buffer (&optional mail-buffer)
-  ;; Get a process output buffer for the current Patcher MAIL-BUFFER (current
-  ;; buffer by default), and prepare it. We can reuse an already existing one
-  ;; because auxiliary buffers are currently used only in one Lisp shot, so
-  ;; there's no risk of Patcher instances overlapping.
-  (let ((project patcher-project)
-	(directory default-directory)
-	(buffer (get-buffer-create " *Patcher Process Output*")))
-    (or mail-buffer (setq mail-buffer (current-buffer)))
-    (with-current-buffer buffer
-      (cd directory)
-      (setq patcher-project project)
-      (setq patcher-mail-buffer mail-buffer)
-      (erase-buffer))
-    buffer))
-
-
 
 ;; ===========================================================================
 ;; Projects description
 ;; ===========================================================================
 
 (defgroup patcher nil
-  "Patch mailing utilities.")
+  "Automatic archive-base project maintenance.")
 
 (defgroup patcher-default nil
-  "Default settings for Patcher projects."
+  "Default settings for Patcher project options."
   :group 'patcher)
 
+(defcustom patcher-default-name nil
+  "*Default name for Patcher projects.
+
+This project option (a string) exists to let you define different Patcher
+projects (hence with different names) sharing a common name for the
+underlying diff and commit commands.  If set, it will be used rather than
+the real project's name."
+  :group 'patcher-default
+  :type '(choice (const :tag "Patcher name" nil)
+		 (string :tag "Other name")))
+
 (defcustom patcher-default-mail-method 'compose-mail
   "*Default method used by Patcher to prepare a mail.
+
 Currently, there are four built-in methods: 'compose-mail \(the default),
 'sendmail, 'message, 'gnus and 'fake.  Please refer to the corresponding
-`patcher-mail-*' function for a description of each method.  You can also
-define your own method, say `foo'.  In that case, you *must* provide a
-function named `patcher-mail-foo' which takes two arguments: a project
-descriptor and a string containing the subject of the message.  This
-function must prepare a mail buffer.  If you want to do this, please see
-how it's done for the built-in methods."
+`patcher-mail-*' function for a description of each method.
+
+You can also define your own method, say `foo'.  In that case, you *must*
+provide a function named `patcher-mail-foo' which takes two arguments: a
+project descriptor and a string containing the subject of the message.
+This function must prepare a mail buffer.  If you want to do this, please
+see how it's done for the built-in methods."
   :group 'patcher-default
   :type '(radio (const compose-mail)
 		(const sendmail)
 
 (defcustom patcher-default-subject-prefix "[PATCH]"
   "*Default prefix for the subject of Patcher mails.
-A %n occurring in this string will be replaced with the project's name.
+
+The following string transformations are performed:
+- %n: the value of the :name project option if set, or the project's name
+      in the Patcher sense.
+- %N: the project's name in the Patcher sense.
+
 A space will be inserted between the prefix and the rest of the subject,
 as appropriate.  This part of the subject is never prompted for.  See
-also the variables `patcher-default-subject'
-and `patcher-default-subject-committed-prefix'.
-
-NOTE: In order to comply with the current policy, XEmacs reviewers are
-encouraged to notify their auto-approval privilege by using a value of
-\"[PATCH APPROVE]\" for their `subject-prefix' project option."
+also `patcher-default-subject' and
+`patcher-default-subject-committed-prefix'."
   :group 'patcher-default
-  :type '(choice (const :tag "None" nil)
-		 string))
+  :type 'string)
 
 (defcustom patcher-default-subject-committed-prefix "[COMMIT]"
   "*Default prefix for the subject of Patcher mails.
-This string is used rather than the one specified by the variable
-`patcher-default-subject-prefix' when you have committed your patch
-before sending the message.  A %n occurring in this string will be
-replaced with the project's name.  A space will be inserted between
-the prefix and the rest of the subject, as appropriate.  This part of
-the subject is never prompted for.  See also the variables
-`patcher-default-subject-prefix' and `patcher-default-subject'.
 
-A value of nil means don't change the subject.
-
-NOTE: In order to comply with the current policy, XEmacs reviewers are
-encouraged to notify their commit action by using a value of
-\"[PATCH APPROVE COMMIT]\" for their `subject-committed-prefix' project
-option."
+Same as `patcher-default-subject-prefix', but for committed patches. If nil,
+keep the normal subject prefix."
   :group 'patcher-default
   :type '(choice (const :tag "Don't change" nil)
 		 string))
 
-(defcustom patcher-default-subject nil
-  "*Default subject to use when sending a Patcher mail.
-A %n occurring in this string will be replaced with the project's name.
-Please note that this variable (and its corresponding project option) is
-used *only* to provide a default value for prompted subjects.  Subjects
-are *always* prompted for.
+(defcustom patcher-default-subject ""
+  "*Default subject for Patcher mails.
 
-See also the variables `patcher-default-subject-prefix' and
-`patcher-default-subject-committed-prefix' which are not subject to
+The following string transformations are performed:
+- %n: the value of the :name project option if set, or the project's name
+      in the Patcher sense.
+- %N: the project's name in the Patcher sense.
+
+Please note that this is used *only* to provide a default value for prompted
+subjects.  Subjects are *always* prompted for.
+
+See also `patcher-default-subject-prefix' and
+`patcher-default-subject-committed-prefix', which are not subject to
 prompting."
   :group 'patcher-default
-  :type '(choice (const :tag "None" nil)
-		 string))
+  :type 'string)
 
-(defcustom patcher-default-mail-prologue nil
+(defcustom patcher-default-mail-prologue ""
   "*Default string to insert at the beginning of every Patcher mail."
   :group 'patcher-default
-  :type '(choice (const :tag "None" nil)
-		 string))
+  :type 'string)
 
 (defcustom patcher-default-change-logs-updating 'automatic
   "*Controls the way ChangeLog fields are updated.
+
 Possible values and their meaning are:
-- 'automatic: (the default) Patcher generates ChangeLog skeletons
+- 'automatic: \(the default) Patcher generates ChangeLog skeletons
    automatically based on the created diff (you then have to fill up the
    entries as needed).
 - 'manual: you are supposed to have updated the ChangeLog files by hand,
 
 (defcustom patcher-default-change-logs-user-name nil
   "*User full name for generated ChangeLog entries.
+
 If nil, let `patch-to-change-log' decide what to use.
 Otherwise, it should be a string."
   :group 'patcher-default
   :type '(choice (const :tag "Default" nil)
-		 string))
+		 (string :tag "Other name")))
 
 (defcustom patcher-default-change-logs-user-mail nil
   "*User mail address for generated ChangeLog entries.
+
 If nil, let `patch-to-change-log' decide what to use.
 Otherwise, it should be a string."
   :group 'patcher-default
   :type '(choice (const :tag "Default" nil)
-		 string))
+		 (string :tag "Other mail")))
 
 (defcustom patcher-default-change-logs-appearance 'verbatim
   "*Controls the appearance of ChangeLog entries in Patcher mails.
+
 The values currently supported are:
 - 'verbatim \(the default): ChangeLog entries appear simply as text above
    the patch.  A short line mentioning the ChangeLog file they belong to
    the patch itself.
 -  nil: ChangeLog entries don't appear at all.
 
-See also the variable `patcher-default-change-logs-diff-command'."
+See also the `patcher-default-change-logs-diff-command' user option."
   :group 'patcher-default
   :type '(radio (const :tag "Verbatim" verbatim)
 		(const :tag "Diff, packed together" packed)
 
 (defcustom patcher-default-change-logs-prologue "%f addition:"
   "*Default string to insert just before ChangeLogs in every Patcher mail.
+
 This applies when ChangeLog additions appear verbatim in the message.
-A %f occurring in this string will be replaced with the ChangeLog file
-name (relative to the project's directory)."
+A %f occurring in this string will be replaced with the ChangeLog file name
+(relative to the project's directory)."
   :group 'patcher-default
-  :type '(choice (const :tag "None" nil)
-		 string))
+  :type 'string)
 
 (defcustom patcher-default-diff-prologue-function
   'patcher-default-diff-prologue
   "*Function used to insert a prologue before each diff output.
+
 Insertion must occur at current point in current buffer.
 This function should take one argument indicating the kind of diff:
 - a value of 'sources indicates a source diff only,
 		 (const :tag "None" nil)
 		 (symbol :tag "Other")))
 
+(defcustom patcher-default-command-directory nil
+  "*Default command directory for Patcher projects.
+
+This directory (a string) can be relative to the project's directory.
+All diff and commit commands are executed from this directory if set.
+Otherwise, the project's directory is used."
+  :group 'patcher-default
+  :type  '(choice (const :tag "Same directory" nil)
+		  (string :tag "Other directory")))
+
+
+(defcustom patcher-default-pre-command ""
+  "*Default string to prefix patcher commands with.
+
+This is where you would put things like \"runsocks\"."
+  :group 'patcher-default
+  :type 'string)
+
 (defcustom patcher-default-diff-command "cvs -q diff -u %f"
   "*Default method used by Patcher to generate a patch.
-A %f occurring in this string will be replaced with the files affected by
-the patch.  These files can be specified by using `patcher-mail-subproject'
-instead of `patcher-mail' to prepare the patch.  Otherwise, the %f will
-simply be removed."
+
+The following string transformations are performed:
+- %n: the value of the :name project option if set, or the project's name
+      in the Patcher sense.
+- %N: the project's name in the Patcher sense.
+- %f: the files affected by the patch.  These files can be specified by
+      using `patcher-mail-subproject' instead of `patcher-mail' to prepare
+      the patch.  Otherwise, the %f will simply be removed."
   :group 'patcher-default
   :type 'string)
 
 (defcustom patcher-default-after-diff-hook nil
   "*Hook run on the output of a Patcher diff comand.
+
 The functions in this hook should operate on the current buffer and take
 two optional arguments limiting the processing to a buffer region
 (in the absence of arguments, the whole buffer should be processed).
   :type 'hook)
 
 (defcustom patcher-default-diff-line-filter "\\? .*"
-  "*Default line filter to pass Patcher diff's through.
+  "*Default line filter to pass Patcher diffs through.
+
 When inserting a diff in Patcher mails, lines matching this regexp will
 be excluded.
 
-Note: the regexp must match the whole line. Don't add beginning and end
+Note: the regexp must match the whole line.  Don't add beginning and end
 of line markers to it, Patcher will do this for you.
 
 By default, local files unknown to CVS are filtered out."
 
 (defcustom patcher-default-change-logs-diff-command "cvs -q diff -U 0 %f"
   "*Command to use to generate ChangeLog diffs.
+
 This value is used when the ChangeLog appearance is either 'packed or
 'patch (see the variable `patcher-default-change-logs-appearance').
+
 If set to 'diff, use the same command as for the rest of the patch.
-Otherwise, it should be a string.  A `%f' occurring in this string will be
-replaced with the ChangeLog filenames.
+Otherwise, it should be a string.
+
+The following string transformations are performed:
+- %n: the value of the :name project option if set, or the project's name
+      in the Patcher sense.
+- %N: the project's name in the Patcher sense.
+- %f: the ChangeLog filenames.
 
 Note: it is highly recommended to remove the context from ChangeLog diffs
 because they often fail to apply correctly."
 
 (defcustom patcher-default-commit-privilege nil
   "*Default value for Patcher commit privilege status.
+
 If you have the privilege to commit patches yourself, you should set
 this option to t."
   :group 'patcher-default
 
 (defcustom patcher-default-commit-command "cvs commit -F %s %f"
   "*Default method used by Patcher to commit a patch.
-- A %s occurring in the command will be replaced with the name of a file
-  containing the commit log message.
-- A %S occuring in the command will be replaced with the commit log message
-  itself.
-- A %f occurring in the command will be replaced with the files affected by
-  the patch.  These files can be specified by using
-  `patcher-mail-subproject' instead of `patcher-mail' to prepare the patch.
-   Otherwise, the %f will simply be removed."
+
+The following string transformations are performed:
+- %n: the value of the :name project option if set, or the project's name
+      in the Patcher sense.
+- %N: the project's name in the Patcher sense.
+- %s: the name of a file containing the commit log message.
+- %S: the commit log message itself (quoted to prevent shell expansion).
+- %f: the files affected by the patch.  These files can be specified by using
+     `patcher-mail-subproject' instead of `patcher-mail' to prepare the patch.
+      Otherwise, the %f will simply be removed."
   :group 'patcher-default
   :type 'string)
 
   "NOTE: This patch has been committed."
   "*Notice added to a mail when the patch is committed before sending."
   :group 'patcher-default
-  :type '(choice (const :tag "None" nil)
-		 string))
+  :type 'string)
 
 (defcustom patcher-default-failed-command-regexp "^cvs \\[[^]]* aborted\\]"
   "*Default regular expression for matching the result of a failed command.
+
 Commands in question are the diff and the commit one."
   :group 'patcher-default
   :type 'regexp)
 
 (defcustom patcher-default-log-message-items '(subject)
   "*Elements used to initialize a Patcher commit log message.
+
 This is nil, or a list of the following items:
 - 'subject: the subject of the corresponding Patcher mail (sans the prefix),
 - 'compressed-change-logs: the compressed ChangeLog entries for the current
 (defcustom patcher-default-change-logs-separator
   "-------------------- ChangeLog entries follow: --------------------"
   "*ChangeLog entries separator for Patcher commit log messages.
+
 Either nil, or a string which will be inserted in a line of its own.
+
 See also the function `patcher-logmsg-insert-change-logs'."
   :group 'patcher-default
-  :type '(choice (const :tag "None" nil)
-		 string))
+  :type 'string)
 
 (defcustom patcher-default-edit-log-message t
   "*Whether Patcher lets you edit the commit log message.
+
 If nil, Patcher will directly use the initialization value \(see
 `patcher-default-init-log-message')."
   :group 'patcher-default
 
 (defcustom patcher-default-kill-source-files-after-sending t
   "*Whether to kill source files after sending the mail.
+
 This is effective only when sources files have not been killed already
 \(see the variable `patcher-default-kill-source-files-after-diffing').
 
 
 (defcustom patcher-default-kill-source-files-after-diffing t
   "*Whether to kill source files after building the ChangeLog skeletons.
+
 These files are loaded temporarily by `patch-to-change-log'.  If this
 variable is non nil, `patch-to-change-log' will be instructed to remove
 them when they are not needed anymore.
+
 See also the variable `patcher-default-kill-source-files-after-sending'."
   :group 'patcher-default
   :type 'boolean)
 
 (defcustom patcher-default-to-address "xemacs-patches@xemacs.org"
   "*Default email address to use when sending a Patcher mail.
+
 This variable is used by all mail methods except the 'gnus one \(see
 `patcher-default-mail-method').  If not defined, it is prompted for."
   :group 'patcher-default
   :type 'string)
 
-(defcustom patcher-default-gnus-group nil
+(defcustom patcher-default-gnus-group ""
   "*Default Gnus group to use when sending a Patcher mail.
+
 This variable is used only in the 'gnus mail method \(see
 `patcher-default-mail-method').  The mail sending process will behave as if
 you had typed `C-u a' in the group buffer on that Gnus group.  If not
   :group 'patcher-default
   :type 'string)
 
+(defcustom patcher-default-themes nil
+  "*Default themes to use in Patcher projects.
+
+This is a list of theme names (symbols) that must be defined in the
+`patcher-themes' user option."
+  :group 'patcher-default
+  ;; #### NOTE: ideally, this type should be computed automatically, depending
+  ;; on the defined themes. This arises the interesting question of custom
+  ;; dynamic types. Without them, it's a complex thing to do.
+  :type '(repeat (symbol :tag "Theme name")))
+
 
 ;; Defining these const avoids coding special cases for the :inheritance,
-;; :subdirectory and :files (sub)project option in the accessor functions.
+;; :subdirectory, :files and :command-directory (sub)project option in the
+;; accessor functions.
 (defconst patcher-default-inheritance nil)
 (defconst patcher-default-subdirectory nil)
 (defconst patcher-default-files nil)
 
-
 (defconst patcher-project-options-custom-type
-  '((list :inline t :tag "Mail method"
+  '((list :inline t :tag "Project name"
+	  :format "%{%t%}: %v"
+	  (const :tag "" :value :name)
+	  (choice (const :tag "Patcher name" nil)
+		  (string :tag "Other name")))
+    (list :inline t :tag "Mail method"
 	  :format "%{%t%}: %v"
 	  (const :tag "" :value :mail-method)
 	  (choice (const compose-mail)
     (list :inline t :tag "Subject prefix"
 	  :format "%{%t%}: %v"
 	  (const  :tag "" :value :subject-prefix)
-	  (choice (const :tag "None" nil)
-		  string))
+	  string)
     (list :inline t :tag "Subject committed prefix"
 	  :format "%{%t%}: %v"
 	  (const  :tag "" :value :subject-committed-prefix)
     (list :inline t :tag "Subject"
 	  :format "%{%t%}: %v"
 	  (const  :tag "" :value :subject)
-	  (choice (const :tag "None")
-		  string))
+	  string)
     (list :inline t :tag "Mail Prologue"
 	  :format "%{%t%}: %v"
 	  (const :tag "" :value :mail-prologue)
-	  (choice (const :tag "None" nil)
-		  string))
+	  string)
     (list :inline t :tag "ChangeLogs updating"
 	  :format "%{%t%}: %v"
 	  (const :tag "" :value :change-logs-updating)
 	  :format "%{%t%}: %v"
 	  (const :tag "" :value :change-logs-user-name)
 	  (choice (const :tag "Default" nil)
-		  string))
+		  (string :tag "Other name")))
     (list :inline t :tag "ChangeLogs user mail"
 	  :format "%{%t%}: %v"
 	  (const :tag "" :value :change-logs-user-mail)
 	  (choice (const :tag "Default" nil)
-		  string))
+		  (string :tag "Other mail")))
     (list :inline t :tag "ChangeLogs appearance"
 	  :format "%{%t%}: %v"
 	  (const :tag "" :value :change-logs-appearance)
     (list :inline t :tag "ChangeLogs prologue"
 	  :format "%{%t%}: %v"
 	  (const :tag "" :value :change-logs-prologue)
-	  (choice (const :tag "None" nil)
-		  string))
+	  string)
     (list :inline t :tag "Diff prologue function"
 	  :format "%{%t%}: %v"
 	  (const :tag "" :value :diff-prologue-function)
 	  (choice (const :tag "Default" patcher-default-diff-prologue)
 		  (const :tag "None" nil)
 		  (symbol :tag "Other")))
+    (list :inline t :tag "Command directory"
+	  :format "%{%t%}: %v"
+	  (const :tag "" :value :command-directory)
+	  (choice (const :tag "Same directory" nil)
+		  (string :tag "Other directory")))
+    (list :inline t :tag "Pre command"
+	  :format "%{%t%}: %v"
+	  (const :tag "" :value :pre-command)
+	  string)
     (list :inline t :tag "Diff command"
 	  :format "%{%t%}: %v"
 	  (const :tag "" :value :diff-command)
     (list :inline t :tag "Committed notice"
 	  :format "%{%t%}: %v"
 	  (const :tag "" :value :committed-notice)
-	  (choice (const :tag "None" nil)
-		  string))
+	  string)
     (list :inline t :tag "Failed command regexp"
 	  :format "%{%t%}: %v"
 	  (const :tag "" :value :failed-command-regexp)
     (list :inline t :tag "ChangeLogs separator"
 	  :format "%{%t%}: %v"
 	  (const :tag "" :value :change-logs-separator)
-	  (choice (const :tag "None" nil)
-		  string))
+	  string)
     (list :inline t :tag "Edit log message"
 	  :format "%{%t%}: %v"
 	  (const :tag "" :value :edit-log-message)
     (list :inline t :tag "Gnus Group"
 	  :format "%{%t%}: %v"
 	  (const  :tag "" :value :gnus-group)
-	  string))
+	  string)
+    (list :inline t :tag "Themes"
+	  :format "%{%t%}: %v"
+	  (const :tag "" :value :themes)
+	  ;; #### NOTE: ideally, this type should be computed automatically,
+	  ;; depending on the defined themes. This arises the interesting
+	  ;; question of custom dynamic types. Without them, it's a complex
+	  ;; thing to do.
+	  (repeat (symbol :tag "Theme name"))))
   ;; This is currently useless, and would cause problems in the custom type:
   ;; it will match the inheritance field in patcher-projects before the
   ;; corresponding custom type definition.
   ;; `patcher-projects' and `patcher-subprojects'.
   )
 
+
+(defgroup patcher-themes nil
+  "Theme settings for Patcher projects."
+  :group 'patcher)
+
+(defcustom patcher-themes '()
+  "*List of themes to use in Patcher projects.
+
+Each element looks like \(NAME :OPTION VALUE ...).  NAME is the theme
+name (a symbol).  The remainder of the list is the same as in project
+descriptors (see `patcher-projects').
+
+See also `patcher-max-theme-depth'."
+  :group 'patcher-themes
+  :type `(repeat
+	  (group (symbol :tag "Theme name")
+		 ;; #### NOTE: we could be tempted to add an `inheritance'
+		 ;; mechanism for themes, just like for projects. However,
+		 ;; don't forget that a theme can contain other themes because
+		 ;; themes belong to `patcher-project-options-custom-type'.
+		 (repeat :inline t :tag "Options"
+			 (choice :inline t :value (:mail-method compose-mail)
+				 ,@patcher-project-options-custom-type))
+		 ))
+  )
+
+
+(defgroup patcher-projects nil
+  "Project settings for Patcher."
+  :group 'patcher)
+
 (defcustom patcher-projects '()
-  "*List of project descriptors used by `patcher-mail'.
-Each project descriptor looks like \(NAME DIR OPTIONS...):
-- NAME is the project's name \(it serves to identify the project),
-- DIR  is the top level directory where the project's sources live,
+  "*List of project descriptors.
+
+Each project descriptor looks like \(NAME DIR :OPTION VALUE ...).
+- NAME is the project's name \(a string).
+- DIR is the project's root directory (a string).
 
 The remainder of the project descriptor is composed of \"project options\"
-\(keywords and associated values).  Keywords correspond to the variables
-`patcher-default-*'.  When Patcher needs a keyword value, it tries to find
-it at different places:
-- first, it looks for the keyword in the project options.
-- if that fails, it tries to find it in the project options of the projects
-  appearing in the `:inheritance' option of the current project.
-- if that fails, it falls back to the corresponding `patcher-default-*'
-variable.
-
-As an exception, the `:inheritance' keyword does not have a corresponding
-`patcher-default-inheritance' variable."
-  :group 'patcher
+\(keyword / value pairs).  When Patcher needs a project option, it tries
+to find it at different places:
+- First, it looks for it in the project descriptor itself.
+- If that fails, it tries to find it in the project themes, if any.
+- If that fails, it tries to find it in the inherited projects, if any.
+- If that fails, it falls back to the corresponding `patcher-default-*'
+  user option."
+  :group 'patcher-projects
   :type `(repeat
 	  (group (string :tag "Project")
 		 (directory :tag "Project directory")
   )
 
 (defcustom patcher-subprojects '()
-  "*List of subproject descriptors used by `patcher-mail'.
+  "*List of Patcher subproject descriptors.
+
 Subproject descriptors are similar to project descriptors \(see the
 variable `patcher-projects') with a few exceptions:
 
-- instead of the project directory field DIR, you specify the name of the
+- Instead of the project directory field DIR, you specify the name of the
   project this subproject is based on.
-- two project options are available in addition to the standard ones:
-  - :subdirectory lets you specify a subdirectory \(of the original
+- Two project options are available in addition to the standard ones:
+  - :subdirectory lets you specify a subdirectory \(of the parent
      project's directory) in which the whole subproject resides.  There is
-     no corresponding `patcher-default-subdirectory' variable.
+     no corresponding `patcher-default-subdirectory' fallback..
   - :files lets you specify a list of files or directories composing the
-     subproject. Each file specification can contain wildcards.  If a
+     subproject.  Each file specification can contain wildcards.  If a
      :subdirectory option is given, these files or directories should be
-     relative to this subdirectory. Otherwise, they should be relative to
+     relative to this subdirectory.  Otherwise, they should be relative to
      the base project's directory.  There is no corresponding
      `patcher-default-files' variable.
   Note that a subproject with neither a :subdirectory nor a :files option
   behaves exactly like the corresponding base project.
-- subprojects don't have an :inheritance mechanism. Instead, they
+- Subprojects don't have an :inheritance mechanism.  Instead, they
   implicitly inherit from their base project \(which in turn can inherit
   from other projects).
 
 `patcher-mail', *not* `patcher-mail-subproject'.  Using the former will
 directly use the set of files and/or directory you have specified.  Using
 the latter will also let you modify this set."
-  :group 'patcher
+  :group 'patcher-projects
   :type `(repeat
 	  (group (string :tag "Subproject")
 		 (string :tag "Of project")
 
 ;; Project descriptors Accessors =============================================
 
-(defsubst patcher-project-name (project) (nth 0 project))
+;; #### NOTE: the accessors routines don't handle the case where the same
+;; option is given several times. Only the first one is used. This currently
+;; would have any sensible meaning anyway.
+
+(defsubst patcher-project-patcher-name (project)
+  (nth 0 project))
+
+(defsubst patcher-subproject-p (project)
+  ;; Return non nil if PROJECT is defined in `patcher-subprojects'.
+  (member project patcher-subprojects))
+
+(defcustom patcher-max-theme-depth 8
+  "*Maximum nesting level in Patcher themes.
+
+This option is a guard against infinite loops that might occur for wrong
+settings of Patcher themes (as themes can contain themes)."
+  :group 'patcher-themes
+  :type 'integer)
+
+(defun patcher-themes-option (themes option level)
+  ;; Look for an option in a list of themes. Note that themes can have the
+  ;; :themes option set. The themes tree (it shouldn't be a graph) is
+  ;; traversed in depth first.
+  (let (theme value)
+    (while (and (not value) (setq theme (pop themes)))
+      (setq theme (assoc theme patcher-themes))
+      (or theme (patcher-error "`%s': no such theme" theme))
+      (let ((theme-options (cdr theme)))
+	(setq value (member option theme-options))
+	(unless value
+	  (let ((subthemes (member :themes theme-options)))
+	    (when (> level patcher-max-theme-depth)
+	      (patcher-error
+	       "Theme `%s': maximum nesting level of themes exceeded.
+Either you have an infinite loop in your theme's :themes option, or you should
+increase the value of `patcher-max-theme-depth'"
+	       (car theme)))
+	    (setq value
+		  (patcher-themes-option (cadr subthemes) option (1+ level)))
+	    ))
+	))
+    value))
+
+(defcustom patcher-max-inheritance-depth 8
+  "*Maximum nesting level in Patcher projects.
+
+This option is a guard against infinite loops that might occur for wrong
+settings of Patcher projects (as projects can inherit projects)."
+  :group 'patcher-projects
+  :type 'integer)
+
+(defun patcher-project-option-1 (project option level)
+  ;; Try to find an option in the project descriptor, otherwise, try in each
+  ;; project from the project's inheritance list.
+  ;; The whole option form is returned: '(:stuff value)
+  (when (> level patcher-max-inheritance-depth)
+    (patcher-error "Project `%s': maximum nesting level of projects exceeded.
+Either you have an infinite loop in your project's inheritance, or you should
+increase the value of `patcher-max-inheritance-depth'"
+		   (patcher-project-patcher-name project)))
+  (let* ((is-subproject (patcher-subproject-p project))
+	 (options (cddr project))
+	 (value (member option options)))
+    ;; Try to find the option in themes.
+    (unless value
+      (let ((themes (member :themes options)))
+	(when themes
+	  (setq value (patcher-themes-option (cadr themes) option 0)))
+	))
+    ;; Try to find the option in inherited projects. Note that inherited
+    ;; projects can have their :inherit option set in turn. The inheritance
+    ;; tree (it shouldn't be a graph) is traverse in depth first.
+    (unless value
+      (let ((projs (if is-subproject
+		       (list (nth 1 project))
+		     (cadr (member :inheritance options))))
+	    proj)
+	(when projs
+	  (while (and (not value) (setq proj (pop projs)))
+	    ;; #### FIXME: what happens if we inherit from something like a
+	    ;; subproject which is unrelated to the current project ?
+	    (setq value (patcher-project-option-1 (assoc proj patcher-projects)
+						  option (1+ level)))))
+	))
+    ;; Now some checkings.
+    (when (and (eq option :files) value)
+      (if is-subproject
+	  ;; Return the files as a string, not as the original list.
+	  (setq value (list :files (mapconcat #'identity (cadr value) " ")))
+	;; #### NOTE: we don't normally check other user-level errors (like,
+	;; only projects can have an :inheritance option above). However, that
+	;; case is special: we have some blind calls to
+	;; `patcher-project-option' that could get an illegal :files options
+	;; from illegal projects. These calls are supposed to return `nil' as a
+	;; result, so we perform the checking.
+	(patcher-error "Project `%s': only subprojects can have a :file option"
+		       (patcher-project-patcher-name project))
+	(setq value nil)))
+    value))
+
+(defun patcher-project-option (project option)
+  ;; Returns either a project's option, or the patcher-default-* value.
+  (let ((opt (patcher-project-option-1 project option 0)))
+    (if opt
+	(cadr opt)
+      (symbol-value
+       (intern-soft
+	(concat "patcher-default-" (substring (symbol-name option) 1))))
+      )))
+(put 'patcher-project-option 'lisp-indent-function 1)
+
+(defsubst patcher-project-name (project)
+  (let ((name (patcher-project-option project :name)))
+    (or name (patcher-project-patcher-name project))
+    ))
 
 (defun patcher-project-directory (project)
   ;; Returns the project directory of PROJECT, possibly expanded as a project
   ;; subdir if PROJECT is a subproject.
-  (if (member project patcher-subprojects)
+  (if (patcher-subproject-p project)
       (let ((prj (assoc (nth 1 project) patcher-projects)))
 	(unless prj
 	  (patcher-error "Can't find base project for subproject `%s'"
-			 (patcher-project-name project)))
+			 (patcher-project-patcher-name project)))
 	(let ((subdir (patcher-project-option project :subdirectory)))
 	  (if subdir
 	      (expand-file-name subdir (patcher-project-directory prj))
     ;; else: (member project patcher-projects)
     (nth 1 project)))
 
-(defun patcher-project-option-1 (project option)
-  ;; Try to find an option in the project descriptor, otherwise, try in each
-  ;; project from the project's inheritance list.
-  ;; The whole option form is returned: '(:stuff value)
-  (let* ((is-subproject (member project patcher-subprojects))
-	 (options (cddr project))
-	 (value (member option options)))
-    (unless value
-      (let ((projs (cadr (member :inheritance options)))
-	    proj)
-	;; If PROJECT is a subproject, we use only the base project as an
-	;; inheritance list.
-	(when is-subproject
-	  (if projs
-	      (warn "Option :inheritance in subproject `%s' will be unused"
-		    (patcher-project-name project)))
-	  (setq projs (list (nth 1 project))))
-	(when projs
-	  (while (and (not value) (setq proj (pop projs)))
-	    (setq value (patcher-project-option-1 (assoc proj patcher-projects)
-						  option))))
-	))
-    ;; Now some checkings.
-    (when (and (eq option :files) value)
-      (if is-subproject
-	  ;; Return the files as a string, not as the original list.
-	  (setq value (list :files (mapconcat #'identity (cadr value) " ")))
-	;; Projects shouldn't have a :file option.
-	(warn "Option :file in project `%s' will be unused"
-	      (patcher-project-name project))
-	(setq value nil)))
-    value))
 
-(defun patcher-project-option (project option)
-  ;; Returns either a project's option, or the patcher-default-* value.
-  (let ((opt (patcher-project-option-1 project option)))
-    (if opt
-	(cadr opt)
-      (symbol-value
-       (intern-soft
-	(concat "patcher-default-" (substring (symbol-name option) 1))))
-      )))
-(put 'patcher-project-option 'lisp-indent-function 1)
+;; ===========================================================================
+;; Internal utilities
+;; ===========================================================================
+
+;; #### NOTE: this is currently useless.
+(defvar patcher-instances nil
+  ;; A list of all alive instances of Patcher (an instance is dead after the
+  ;; mail has been sent. Each element is of the form '(BUFFER_NAME . BUFFER).
+  )
+
+(defconst patcher-change-log-entry-start-regexp
+  "^[0-9]\\{4,4\\}-[0-9]\\{2,2\\}-[0-9]\\{2,2\\} "
+  ;; Regexp matching the beginning of a ChangeLog entry
+  )
+
+;; Buffer local variables ====================================================
+
+;; The following variables get local values in various Patcher buffers (mail
+;; buffer, process output buffer etc).
+
+(make-variable-buffer-local
+ (defvar patcher-project nil
+   ;; Patcher project related to the current patch. This is also set in
+   ;; auxiliary buffers.
+   ))
+
+(make-variable-buffer-local
+ (defvar patcher-mail-buffer nil
+   ;; Mail buffer corresponding to Patcher auxiliary buffers.
+   ))
+
+
+;; Utility functions =========================================================
+
+;;(defun patcher-keyword-value (keyword values)
+;;  ;; Return the value of KEYWORD from a (KEY VAL ...) list. VAL may be omitted
+;;  ;; in the list, in which case t is returned.
+;;  (let ((vals values)
+;;	key val)
+;;    (catch 'found
+;;      (while (setq key (pop vals))
+;;	(setq val (or (not (car vals))
+;;		      (if (keywordp (car vals)) t (pop vals))))
+;;	(and (eq keyword key)
+;;	     (throw 'found val))))
+;;    ))
+
+
+(defsubst patcher-substitute-name (project str)
+  ;; Replace a %N in string STR with the current project's name.
+  ;; Replace a %n in string STR with the value of :name, if set, and with the
+  ;; current project's name otherwise.
+  (let ((name (patcher-project-name project))
+	(patcher-name (patcher-project-patcher-name project))
+	case-fold-search)
+    (replace-in-string (replace-in-string str "%N" patcher-name)
+		       "%n" name)
+    ))
+
+(defun patcher-command (project command &optional files)
+  ;; Build a Patcher command from COMMAND that applies to FILES.
+  ;; This involves %n and %f substitution, and :pre-command handling.
+
+  ;; 1/ %n substitution:
+  (setq command (patcher-substitute-name project command))
+
+  ;; 2/ Force Unix syntax, and replace a %f in COMMAND with the files
+  ;; (if any) converted to a string, or append it to the end of STR.
+  (setq files (if files
+		  (replace-in-string (mapconcat #'identity files " ")
+				     "\\\\" "/")
+		""))
+  (cond ((string-match "%f" command)
+	 (setq command (replace-in-string
+			(replace-in-string command "%f" files)
+			"[ \t]+$" "")))
+	((> (length files) 0)
+	 (setq command (concat command " " files)))
+	)
+
+  ;; 3/ Prepend the :pre-command option to COMMAND, if any.
+  (let ((precmd (patcher-project-option project :pre-command)))
+    (when (> (length precmd) 0)
+      (setq command (concat precmd " " command))
+      ))
+  command)
+
+(defsubst patcher-call-process (command &optional buffer)
+  ;; Call a shell process which executes COMMAND (a string) with output to
+  ;; BUFFER (the current buffer by default).
+  (apply 'call-process shell-file-name nil (or buffer t) nil
+	 shell-command-switch (list command)))
+
+(defsubst patcher-extent (property &optional value buffer)
+  ;; Get the extent that has PROPERTY set to VALUE (t if not given) in BUFFER
+  ;; (current buffer if nil).
+  (car (mapcar-extents #'identity nil buffer nil nil nil
+		       property (or value t))))
+
+(defun patcher-process-output-buffer (&optional mail-buffer)
+  ;; Get a process output buffer for the current Patcher MAIL-BUFFER (current
+  ;; buffer by default), and prepare it. We can reuse an already existing one
+  ;; because auxiliary buffers are currently used only in one Lisp shot, so
+  ;; there's no risk of Patcher instances overlapping.
+  (let ((project patcher-project)
+	(directory default-directory)
+	(buffer (get-buffer-create " *Patcher Process Output*")))
+    (or mail-buffer (setq mail-buffer (current-buffer)))
+    (with-current-buffer buffer
+      (cd directory)
+      (setq patcher-project project)
+      (setq patcher-mail-buffer mail-buffer)
+      (erase-buffer))
+    buffer))
 
 
 
     (goto-char (point-min))
     (let ((prologue (patcher-project-option patcher-project
 		      :change-logs-prologue)))
-      (and prologue
-	   (> (length prologue) 0)
-	   (setq prologue (concat
-			   "^"
-			   (replace-in-string
-			    (regexp-quote prologue) "%f" ".+")
-			   "$"))
-	   (delete-matching-lines prologue)))
+      (when (> (length prologue) 0)
+	(setq prologue (concat
+			"^"
+			(replace-in-string
+			 (regexp-quote prologue) "%f" ".+")
+			"$"))
+	(delete-matching-lines prologue)))
     (delete-matching-lines patcher-change-log-entry-start-regexp)
     ;; Now compress the change log specs into just files, so that mostly just
     ;; the annotations are left.
 		(setq subject
 		      (buffer-substring (point) (point-at-eol))))))
 	  )))
-    (insert subject "\n\n")
+    (let ((doit (> (length subject) 0)))
+      (when doit (insert subject))
+      doit)
     ))
 
 (defun patcher-logmsg-insert-change-logs (&optional separator)
 ChangeLogs separator string defined by the :change-logs-separator project
 option."
   (interactive "P")
+  (unless (point-at-bol)
+    (insert "\n"))
   (when separator
     (setq separator (patcher-project-option patcher-project
 		      :change-logs-separator))
-    (and separator (> (length separator) 0)
-	 (insert "\n\n" separator "\n\n")))
+    (when (> (length separator) 0)
+      (insert separator "\n\n")))
   (let ((prologue (patcher-project-option patcher-project
 		    :change-logs-prologue)))
     (dolist (change-log (patcher-files-buffers
 			 (symbol-value-in-buffer 'patcher-change-logs
 						 patcher-mail-buffer)
 			 'find))
-      (insert "\n")
-      (and prologue (> (length prologue) 0)
-	   (insert (replace-in-string prologue "%f"
-				      (patcher-file-relative-name
-				       (buffer-file-name change-log)))
-		   "\n\n"))
+      (when (> (length prologue) 0)
+	(insert (replace-in-string prologue "%f"
+				   (patcher-file-relative-name
+				    (buffer-file-name change-log)))
+		    "\n\n"))
       (insert (extent-string
+	       ;; #### NOTE: there is an empty line at the end of this extent.
 	       (patcher-change-log-extent change-log patcher-mail-buffer))))
-    ))
+    )
+  ;; -2 is because 1/ there's a spurious empty line inserted after the last
+  ;; element, and 2/ because we don't want automatic newlines at the end of
+  ;; the buffer, in case log messages are used as strings (for instance with
+  ;; PRCS).
+  (delete-char -2))
 
 (defun patcher-logmsg-insert-compressed-change-logs ()
   "Insert compressed ChangeLog entries in the current Patcher LogMsg buffer."
 				       patcher-logmsg-commit-command)))
     (let* ((command
 	    (patcher-command
+	     patcher-project
 	     (replace-in-string
 	      (let (case-fold-search)
 		(replace-in-string patcher-logmsg-commit-command
-				   "%S" (buffer-substring) t))
+				   "%S" (shell-quote-argument
+					 (buffer-substring)) t))
 	      "%s" patcher-logmsg-file-name t)
-	     (if sources
-		 (concat (patcher-files-string change-logs)
-			 " "
-			 sources)
-	       ""))))
+	     (when sources
+	       (append (mapcar #'patcher-file-relative-name change-logs)
+		       sources)))))
       ;; Maybe display the commit command, and make sure the user agrees.
       (when (or (not confirm-commits)
 		(save-window-excursion
 		    (extent (patcher-extent 'patcher-subject-prefix)))
 		(when subject-committed-prefix
 		  (setq subject-committed-prefix
-			(replace-in-string subject-committed-prefix "%n"
-					   (patcher-project-name
-					    patcher-project)))
+			(patcher-substitute-name patcher-project
+						 subject-committed-prefix))
 		  (when extent
 		    (goto-char (extent-start-position extent))
 		    (delete-region (point) (extent-end-position extent)))
 	      (forward-line 1)
 	      (let ((notice (patcher-project-option patcher-project
 			      :committed-notice)))
-		(and notice (> (length notice) 0)
-		     (insert notice "\n"))))
+		(when (> (length notice) 0)
+		  (insert notice "\n"))))
 	    ))
 	;; Bury the log message (see above).  Remove the log message window
 	;; and display the output buffer.
   (let ((items (patcher-project-option patcher-project :log-message-items))
 	(edit-log-message (patcher-project-option patcher-project
 			    :edit-log-message))
-	separator)
+	inserted)
     (dolist (item items)
       (cond ((eq item 'subject)
-	     (patcher-logmsg-insert-subject)
-	     (setq separator t))
+	     (when inserted (insert "\n\n"))
+	     (setq inserted (patcher-logmsg-insert-subject)))
 	    ((eq item 'compressed-change-logs)
+	     (when inserted (insert "\n\n"))
 	     (patcher-logmsg-insert-compressed-change-logs)
-	     (setq separator t))
+	     (setq inserted t))
 	    ((eq item 'change-logs)
-	     (patcher-logmsg-insert-change-logs separator))
+	     (when inserted (insert "\n\n"))
+	     (patcher-logmsg-insert-change-logs inserted))
 	    (t
-	     (patcher-error "invalid log message item: %s" item)))
-      (insert "\n"))
+	     (patcher-error "invalid log message item: %s" item))))
     (goto-char (point-min))
     (if edit-log-message
 	(patcher-message "\
 
 (make-variable-buffer-local
  (defvar patcher-sources nil
-   ;; String containing files/directories command-line specification for the
-   ;; diff command (it will be appended to it). This variable is needed
-   ;; because the user has the ability to override the project's files by
-   ;; calling `patcher-mail-subproject' instead of `patcher-mail'.
+   ;; List of files/directories command-line specification for the diff
+   ;; command. This variable is needed because the user has the ability to
+   ;; override the project's files by calling `patcher-mail-subproject'
+   ;; instead of `patcher-mail'.
    ))
 
 (make-variable-buffer-local
   ;; succeeded, and create the patcher-diff extent.
   (patcher-save-buffers (patcher-files-buffers patcher-change-logs))
   (let ((command
-	 (patcher-command patcher-diff-command
-			  (if patcher-sources
-			      (concat (patcher-files-string
-				       patcher-change-logs)
-				      " "
-				      patcher-sources)
-			    "")))
+	 (patcher-command patcher-project patcher-diff-command
+			  (when patcher-sources
+			    (append (mapcar #'patcher-file-relative-name
+					    patcher-change-logs)
+				    patcher-sources))))
 	(buffer (patcher-process-output-buffer)))
     (patcher-with-progression "Generating global diff"
       (patcher-call-process command buffer))
     (when (patcher-parse-region nil nil buffer)
       (display-buffer buffer t)
       (patcher-error "\
-Error during diff. Please fix the problem and type \
+Error during diff.  Please fix the problem and type \
 \\[patcher-generate-diff] to try again."))
     (patcher-insert-diff buffer)
     ))
 	  (let ((extent
 		 (patcher-change-log-extent change-log (current-buffer)))
 		(beg (point)))
-	    ;; Force forward slashes (for native Windows). -- APA
 	    (insert "\n")
-	    (and prologue (> (length prologue) 0)
-		 (insert (replace-in-string prologue "%f"
-					    (patcher-file-relative-name
-					     (buffer-file-name change-log)))
-			 "\n\n"))
+	    (when (> (length prologue) 0)
+	      (insert (replace-in-string prologue "%f"
+					 (patcher-file-relative-name
+					  (buffer-file-name change-log)))
+		      "\n\n"))
 	    (insert (extent-string extent))
 	    (set-extent-properties (make-extent beg (point))
 	      `(start-open t patcher-change-log
     (when function
       (let ((name (patcher-project-name patcher-project))
 	    (change-log-files (patcher-files-string patcher-change-logs))
-	    (change-log-diff (patcher-command command "")))
+	    (change-log-diff (patcher-command patcher-project command)))
 	(funcall function 'change-logs))
       )))
 
   (let ((buffer (patcher-process-output-buffer)))
     (patcher-with-progression "Generating the ChangeLogs diff"
       (patcher-call-process
-       (patcher-command command (patcher-files-string patcher-change-logs))
+       (patcher-command patcher-project command
+			(patcher-files-string patcher-change-logs))
        buffer))
     (patcher-run-after-diff-hook buffer)
     (when (patcher-parse-region nil nil buffer)
       (display-buffer buffer t)
       (patcher-error "\
-Error during diff. Please fix the problem and type \
+Error during diff.  Please fix the problem and type \
 \\[patcher-insert-change-logs] to try again."))
     ;; #### FIXME: maybe check that all changelogs are diff'ed (meaning the
     ;; user has not forgotten to update one of them).
   (save-excursion
     (let ((diff-extent (patcher-extent 'patcher-diff))
 	  (errors 0)
-	  change-log cmd beg end)
+	  change-log beg end)
       ;; #### Don't forget to start-close the diff extent !! A ChangeLog could
       ;; appear at the beginning of the diff.
       (set-extent-property diff-extent 'start-open nil)
 	  ;; instead of deleting and recreating it, map(car)-extents goes into
 	  ;; an infinite loop, on all extents over and over again.
 	  (setq change-log (extent-property extent 'patcher-change-log))
-	  (setq cmd (patcher-command command
-				     (patcher-file-relative-name change-log)))
 	  (goto-char (extent-start-position extent))
 	  (setq beg (point-marker))
 	  (patcher-delete-extent-and-region extent)
-	  (patcher-call-process cmd)
+	  (patcher-call-process
+	   (patcher-command patcher-project command
+			    (patcher-file-relative-name change-log)))
 	  (setq end (point-marker))
 	  (patcher-run-after-diff-hook (current-buffer) beg end)
 	  (setq extent (make-extent beg end))
     (when function
       (let ((extent (patcher-extent 'patcher-diff))
 	    (name (patcher-project-name patcher-project))
-	    (source-diff (patcher-command patcher-diff-command ""))
+	    (source-diff
+	     (patcher-command patcher-project patcher-diff-command))
+	    ;; #### NOTE: maybe passing a list instead of a string would be
+	    ;; better. I won't break backward compatibility though, at least
+	    ;; not before a major release.
 	    (source-files (patcher-files-string (patcher-sources)))
 	    (change-log-files (patcher-files-string patcher-change-logs))
-	    (change-log-diff (and (stringp command)
-				  (patcher-command command ""))))
+	    (change-log-diff
+	     (and (stringp command)
+		  (patcher-command patcher-project command))))
 	(set-extent-property extent 'start-open nil)
 	(save-excursion
 	  (goto-char patcher-diff-marker)
   ;; be deduced from the variable `patcher-sources', even when set, because it
   ;; might contain directory specifications).
   (patcher-with-progression "Diff'ing the project"
-    (patcher-call-process (patcher-command patcher-diff-command
-					   patcher-sources)
-			  buffer))
+    (patcher-call-process
+     (patcher-command patcher-project patcher-diff-command patcher-sources)
+     buffer))
   (patcher-run-after-diff-hook buffer)
   (when (patcher-parse-region nil nil buffer)
     (display-buffer buffer t)
 
 (defun patcher-change-logs-diff-error ()
   (patcher-error "\
-Patcher has detected a ChangeLog diff. This can mean two things:
+Patcher has detected a ChangeLog diff.  This can mean two things:
 
 - your project might be out of date (someone else has modified the ChangeLog
-  file in the meantime. You should then update your project before running
+  file in the meantime.  You should then update your project before running
   Patcher.
 
-- you have spurious ChangeLog entries. This happens for instance when you have
+- you have spurious ChangeLog entries.  This happens for instance when you have
   filled the ChangeLogs files manually, but Patcher is supposed to do so (the
-  :change-log-updating project option is 'automatic). You should then clean up
+  :change-log-updating project option is 'automatic).  You should then clean up
   your ChangeLog file before running Patcher."))
 
 (defun patcher-generate-diff-1 ()
 			(not (equal (patcher-change-logs buffer)
 				    patcher-change-logs)))
 	       (patcher-error "\
-Some ChangeLog files are not updated. \
+Some ChangeLog files are not updated.  \
 Please update them before running Patcher."))
 	     (cond ((eq appearance 'verbatim)
 		    ;; #### NOTE: when ChangeLog entries are part of the diff,
 		      (patcher-error "Please generate the diff first."))
 		  (let* ((extent (patcher-extent 'patcher-change-logs))
 			 (do-it (or (not extent) (y-or-n-p "\
-ChangeLog entries already inserted. Replace ? "))))
+ChangeLog entries already inserted.  Replace ? "))))
 		    (when do-it
 		      (patcher-delete-extent-and-region extent)
 		      (patcher-insert-change-logs-verbatim)))
 		      (patcher-error "Please generate the diff first."))
 		  (let* ((extent (patcher-extent 'patcher-change-logs))
 			 (do-it (or (not extent) (y-or-n-p "\
-ChangeLog entries already inserted. Replace ? "))))
+ChangeLog entries already inserted.  Replace ? "))))
 		    (when do-it
 		      (patcher-delete-extent-and-region extent)
 		      (let ((command (patcher-project-option patcher-project
 		 ((eq appearance 'patch)
 		  (when (or (not (patcher-change-logs))
 			    (y-or-n-p "\
-ChangeLog entries already inserted. Replace ? "))
+ChangeLog entries already inserted.  Replace ? "))
 		    (patcher-delete-extent-and-region
 		     (patcher-extent 'patcher-diff))
 		    (let ((command (patcher-project-option patcher-project
 			  (and (eq patcher-mail-check-change-logs-insertion
 				   'ask)
 			       (y-or-n-p "\
-You did not insert the ChangeLog entries. Proceed with sending anyway ? ")))))
+You did not insert the ChangeLog entries.  Proceed with sending anyway ? ")))))
 		 (unless proceed (patcher-error "\
-Sending aborted. Please insert the ChangeLogs first."))))
+Sending aborted.  Please insert the ChangeLogs first."))))
 	     )
 	    ((eq appearance 'patch)
 	     (unless (patcher-change-logs)
 			  (and (eq patcher-mail-check-change-logs-insertion
 				   'ask)
 			       (y-or-n-p "\
-You did not insert the ChangeLog entries. Proceed with sending anyway ? ")))))
+You did not insert the ChangeLog entries.  Proceed with sending anyway ? ")))))
 		 (unless proceed(patcher-error "\
-Sending aborted. Please insert the ChangeLogs first."))))
+Sending aborted.  Please insert the ChangeLogs first."))))
 	     )
 	    (t
 	     (patcher-error "invalid `change-logs-appearance' option: %s"
     (let ((proceed (or (null patcher-mail-check-commit-action)
 		       (and (eq patcher-mail-check-commit-action 'ask)
 			    (y-or-n-p "\
-You did not commit your changes. Proceed with sending anyway ? ")))))
+You did not commit your changes.  Proceed with sending anyway ? ")))))
       (unless proceed (patcher-error "\
-Sending aborted. Please commit your changes first.")))
+Sending aborted.  Please commit your changes first.")))
     ))
 
 (defun patcher-after-send (&optional unused)
 
 (defun patcher-mail-compose-mail (project subject)
   "Prepare a patch-related mail with the `compose-mail' function.
-See also the `mail-user-agent' variable.
 
 This function uses the `:to-address' project option to determine the email
-address for sending the message.  Otherwise, the address is prompted for."
+address for sending the message.  Otherwise, the address is prompted for.
+
+See also the `mail-user-agent' variable."
   (compose-mail (or (patcher-project-option project :to-address)
 		    (read-string "To address: "))
 		subject nil nil nil nil '((patcher-after-send)))
 
 (defcustom patcher-mail-run-gnus 'prompt
   "*Whether Patcher should run Gnus.
+
 The 'gnus mailing method of Patcher needs a running Gnus session.
 If Gnus is not running at the time it is needed, Patcher can start
 it (or not) depending on this variable:
 
 (defcustom patcher-mail-run-gnus-other-frame t
   "*Whether Patcher should start Gnus in a new frame.
+
 This is used in case Patcher has to start Gnus by itself \(see the
 variable `patcher-mail-run-gnus').  Possible values are:
 - nil:     start Gnus in the current frame,
 	  ((eq patcher-mail-run-gnus t)
 	   (patcher-mail-run-gnus))
 	  ((eq patcher-mail-run-gnus 'prompt)
-	   (if (y-or-n-p "Gnus is not currently running. Start it ? ")
+	   (if (y-or-n-p "Gnus is not currently running.  Start it ? ")
 	       (patcher-mail-run-gnus)
 	     (patcher-error
 	      "The 'gnus mailing method requires a running Gnus session")))
 (defun patcher-mail-fake (project subject)
   "Prepare a patch-related fake mail.
 Use this function if you want to do all that Patcher can do, apart from
-sending a real mail. This function generates a fake mail buffer which acts
+sending a real mail.  This function generates a fake mail buffer which acts
 as a standard Patcher mail buffer, apart from the fact that when you type
 \\<patcher-fakemail-mode-map>\\[patcher-fakemail-send] in it, it doesn't
 really send a mail, but just clean things up."
   (let ((buffer (generate-new-buffer "*Patcher Fake Mail*")))
     (switch-to-buffer buffer)
+    ;; #### NOTE: Patcher asks for a subject even with the fakemail method,
+    ;; which is arguable. However, even with a fake mail, one could require
+    ;; log message initialization from a fake subject. We could do something
+    ;; more clever here.
+    (insert "Subject: " subject "\n")
     (patcher-fakemail-mode)
     ))
 
   (let ((subject-prefix (patcher-project-option project :subject-prefix))
 	extent)
     ;; Construct the subject, maybe with an extent marking the prefix:
-    (when (and subject-prefix (> (length subject-prefix) 0))
-      (setq subject-prefix
-	    (replace-in-string subject-prefix "%n"
-			       (patcher-project-name project)))
+    (when (> (length subject-prefix) 0)
+      (setq subject-prefix (patcher-substitute-name project subject-prefix))
       (setq extent (make-extent 0 (length subject-prefix) subject-prefix))
       (set-extent-properties extent
 	'(detachable nil duplicable t patcher-subject-prefix t)))
      project subject))
   (push (cons (buffer-name) (current-buffer)) patcher-instances)
   (patcher-minor-mode t)
-  (cd (patcher-project-directory project))
   (setq patcher-project project)
-  (setq patcher-sources files)
+  (let ((command-directory
+	 (patcher-project-option patcher-project :command-directory)))
+    (if command-directory
+	(let ((project-directory (patcher-project-directory project)))
+	  (setq command-directory
+		(expand-file-name command-directory project-directory))
+	  (cd command-directory)
+	  (when files (setq files (split-string files)))
+	  (setq patcher-sources
+		(if (not files)
+		    (list (patcher-file-relative-name
+			   project-directory command-directory 'raw))
+		  (mapcar
+		   (lambda (file)
+		     (patcher-file-relative-name
+		      (expand-file-name file project-directory)
+		      command-directory 'raw))
+		   files)))
+	  )
+      ;; else: (null command-directory)
+      (cd (patcher-project-directory project))
+      ))
   (setq patcher-diff-command (patcher-project-option project :diff-command))
   (let ((mail-prologue (patcher-project-option project :mail-prologue)))
-    (and mail-prologue (> (length mail-prologue) 0)
-	 (insert "\n" mail-prologue)))
+    (when (> (length mail-prologue) 0)
+      (insert "\n" mail-prologue)))
   (save-excursion
     (insert "\n\n")
     (when (patcher-project-option project :change-logs-updating)
 	  (sbj (read-string
 		"Subject: "
 		(let ((s (patcher-project-option prj :subject)))
-		  (and s
-		       (> (length s) 0)
-		       (replace-in-string s "%n"
-					  (patcher-project-name prj))))
+		  (when (> (length s) 0)
+		    (patcher-substitute-name prj s)))
 		patcher-subjects-history))
 	  (dir (patcher-project-directory prj))
 	  (fls (let ((default-directory (file-name-as-directory dir)))
 		 (or (let ((f (patcher-project-option prj :files)))
-		       (and f (read-shell-command "Files: "
-						  (concat
-						   (mapconcat 'identity f " ")
-						   " ")
+		       (and f (read-shell-command "Files: " (concat f " ")
 						  nil f)))
 		     (let* ((default-file (and (buffer-file-name)
 					       (patcher-file-relative-name
 						(buffer-file-name)
-						dir t)))
+						dir 'raw)))
 			    (default-file
 			      ;; If the file is not actually underneath the
 			      ;; project, then don't suggest it as a
 	  (sbj (read-string
 		"Subject: "
 		(let ((s (patcher-project-option prj :subject)))
-		  (and s
-		       (> (length s) 0)
-		       (replace-in-string s "%n"
-					  (patcher-project-name prj))))
+		  (when (> (length s) 0)
+		    (patcher-substitute-name prj s)))
 		patcher-subjects-history)))
      (list prj sbj current-prefix-arg)))
-  (let ((files (patcher-project-option project :files)))
-    (and files (setq files (mapconcat 'identify files " ")))
-    (patcher-mail-1 project subject files (and (interactive-p) arg))
-    ))
+  (patcher-mail-1 project subject (patcher-project-option project :files)
+		  (and (interactive-p) arg))
+  )
 
 (provide 'patcher)
 
+2003-07-29  Didier Verna  <didier@xemacs.org>
+
+	* Patcher 3.5 is released.
+
 2003-04-23  Didier Verna  <didier@xemacs.org>
 
 	* Patcher 3.4.1 is released.

texi/patcher.texi

 
 @c Contents management by FCM version 0.1-b2.
 
+@c #### TODO:
+@c - Add a section on how to setup Patcher for PRCS.
+
 
 @c %** start of header
 @setfilename patcher.info
 @c ====================================================================
 @c Definitions
 @c ====================================================================
-@set VERSION 3.4
-@set COPYRIGHT_DATE 1999-2003
+@set VERSION 3.5
+@set COPYRIGHT_DATE 1999, 2000, 2001, 2002, 2003
 
 
 @c ====================================================================
 * Requirements::     What you need to get Patcher running
 * Quick Start::      For the brave and the impatient
 * User Manual::      A step-by-step guide to using Patcher
-* Reference Manual:: Hiding the information you're looking for
 * Variables Index::
 * Functions Index::
 * Keystrokes Index::
 usually involves repeatedly the same few steps, some of which can be
 tedious: you work on your local copy of the project; once you're
 satisfied with your changes, you create a patch by diffing your local
-copy against the project's archive; maybe you then construct the
-ChangeLog entries. Finally, you propose your changes by sending a mail
-to the developers list with your patch and the ChangeLog entries
-included, hoping that your proposition will be accepted. If you're one
-of the maintainers, you will still probably send the message to the
-list, simply announcing the modification, and immediately commit the
-patch with an informative log message.
+copy against the project's archive; then (or progressively), you
+construct the ChangeLog entries. Finally, you propose your changes by
+sending a mail to the developers list with your patch and the ChangeLog
+entries included, hoping that your proposition will be accepted. If
+you're one of the maintainers, you will still probably send the message
+to the list, simply announcing the modification, and immediately commit
+the patch with an informative log message.
 
 Patcher is an XEmacs package designed to automate this process. Patcher
 can't work on the project for you. However, as soon as you give it some
 repository, create ChangeLog entries, prepare a mail announcing the
 changes, and even commit the patch for you with a informative log
 message. All of this is done in just a few keystrokes. Additionally,
-Patcher can (when that's possible) perform some sanity checks, like
-verifying that your local copy is up-to-date, that you did not forget
-some ChangeLog entries, that the commit operation went well and so on.
+Patcher can perform some sanity checks, like verifying that your local
+copy is up-to-date, that you did not forget some ChangeLog entries, that
+the commit operation went well and so on.
 
 If you're brave and impatient, and want to start using the basics of
 Patcher as soon as possible, see @ref{Quick Start}. It is recommended to
 read it anyway, since it gives an overview of how Patcher works. If you
-know the basics and now want a more detailed guide, see @ref{User
-Manual}. If you're an advanced user and need a particular piece of
-information, see @ref{Reference Manual}.
+know the basics and want a more detailed guide, see @ref{User Manual}.
 
 Enjoy using Patcher !
 
 @comment  node-name,  next,  previous,  up
 @chapter Distribution
 
-Patcher is available either as a stand-alone package, or as part of the
+Patcher is available either as a standalone package, or as part of the
 standard @file{xemacs-devel} XEmacs package.
 
-You can get @file{xemacs-devel} it in three different ways:
+You can get @file{xemacs-devel} in three different ways:
 @itemize @bullet
 @item
 You can install @file{xemacs-devel} from within XEmacs via the package
 unpack it where you put the other packages.
 
 @item
-@file{xemacs-devel} sources are available from XEmacs's CVS repository.
-The module name is @samp{xemacs-devel}. See
+@file{xemacs-devel} sources are available from the XEmacs CVS
+repository. The module name is @samp{xemacs-devel}. See
 @uref{http://www.xemacs.org/Develop/cvsaccess.html} for more information
-about XEmacs's CVS repository.
+about the XEmacs CVS repository.
 @end itemize
 
 
-I also provide Patcher as a stand-alone package. This is mainly for my
+I also provide Patcher as a standalone package. This is mainly for my
 personal usage (when developing and debugging new versions) but you
 might want to use it if you're not interested in the rest of
-@file{xemacs-devel}. The stand-alone version of Patcher can be found at
+@file{xemacs-devel}. The standalone version of Patcher can be found at
 @uref{http://www.lrde.epita.fr/~didier/comp/development/software.php}.
 You will also find different inlined versions of this documentation at
 that place.
 works with earlier versions of XEmacs, but I'm sure it does @strong{not}
 work with GNU Emacs (see below).
 
-Patcher might also has some other requirements, depending on how you use
+Patcher might also have some other requirements, depending on how you use
 it:
 
 @itemize @bullet
 
 @item
 If you want to send mails from Patcher (@pxref{Mail Methods}), you will
-need a mail agent. Patcher currently supports @file{compose-mail}, and
-@file{sendmail}, @file{message} as well as @file{Gnus} natively.
+need a mail user agent. Patcher currently supports @file{compose-mail},
+@file{sendmail}, @file{message} and @file{Gnus} natively.
 @end itemize
 
 
 @comment  node-name,  next,  previous,  up
 @chapter Quick Start
 
-In this chapter, we will quickly setup the basic Patcher configuration
-for hacking on XEmacs. Adapt the example as you wish. Let's make some
+In this chapter, we quickly setup the basic Patcher configuration for
+hacking on XEmacs. Adapt the example as you wish. Let's make some
 assumptions first:
 
 @itemize @bullet
 @comment  node-name,  next,  previous,  up
 @section Setting up Patcher
 
-The first thing to do is instruct patcher to handle your "XEmacs"
-project. Put this in your .emacs:
+The first thing to do is to make patcher aware of your "XEmacs" project.
+Put this in your @file{.emacs} file:
 
 @lisp
 (setq patcher-projects
 @end lisp
 
 @findex patcher-mail
-Calling this function will first prompt you (with completion) for a
-project name (the first element of each project descriptor, remember ?).
-We currently only have an "XEmacs" project, so hitting @kbd{TAB} will
-directly fill the minibuffer in with the only possibility. Then, you're
-prompted for a subject line that will be used in the mail. Say something
-sensible.
+First, you're prompted (with completion) for a project name (the first
+element of each project descriptor, remember ?). We currently only have
+an "XEmacs" project, so hitting @kbd{TAB} will directly fill the
+minibuffer in with this only choice. Then, you're prompted for a subject
+line that will be used in the mail. Say something sensible.
 
 Three operations are now executed in turn:
 
 @enumerate
 @item
+@vindex patcher-default-to-address
 @vindex :to-address
 Patcher prepares a mail buffer. The message will be sent to the address
 you specified with the @code{:to-address} project option, and the
 @item
 Patcher now builds the patch. The command used to do this is @samp{cvs
 -q diff -u} (this is also a project option). Upon successful completion
-of this command (we will assume that's indeed the case), the patch is
+of this command (we assume that's indeed the case), the patch is
 inserted into the mail buffer. Some information about the patch is
 provided just above it (the command used, the files affected and so on).
 
 @findex patcher-commit-change
 @kindex C-c C-p c
 In order to start the commit process, simply type @samp{C-c C-p c}. This
-calls the function @code{patcher-commit-change}. Welcome to a new
-buffer, the @code{*Patcher Log Message*} buffer. This buffer lets you
-edit the log message that will accompany your commit. Note that the
-message is initialized with the subject line of your mail. This is also
-a project option.
+calls the function @code{patcher-commit-change}. Congratulations. You've
+just been transported to a new buffer, the @code{*Patcher Log Message*}
+buffer. This buffer lets you edit the log message that will accompany
+your commit. Note that the message is initialized with the subject line
+of your mail. This is also a project option.
 
 @findex patcher-logmsg-commit
 @kindex C-c C-c
 @c ====================================================================
 @c User Manual
 @c ====================================================================
-@node User Manual, Reference Manual, Quick Start, Top
+@node User Manual, Variables Index, Quick Start, Top
 @comment  node-name,  next,  previous,  up
 @chapter User Manual
 
 This chapter provides a step-by-step guide to using Patcher. Everything
-there is to know about Patcher is here, but the features are introduced
-progressively.
+there is to know about Patcher is here, though the features are
+introduced progressively.
 
 All user options that are going to be presented in this manual can be
 found in the @code{patcher} customization group, or a subgroup of it.
 * ChangeLogs Handling::  How Patcher behaves with respect to ChangeLogs
 * Project Check In::     Committing your changes from Patcher
 * Mail Sending::         Sending the message and cleaning up the place
-* Error Handling::       Tracking when commands fail
-* Subprojects Handling:: Defining permanent subprojects
+* More On Commands::     Error handling and other generalities
+* More On Subprojects::  Defining permanent subprojects
 @end menu
 
 @ignore
 @section Project Descriptors
 
 @vindex patcher-projects
-Projects specifications are stored in the @code{patcher-projects} user
-option. This variable is actually a list of @dfn{project descriptors}. Each
-project descriptor has the following form: @samp{(@var{NAME} @var{DIR}
+Projects specifications are stored in @code{patcher-projects}. This user
+option is actually a list of @dfn{project descriptors}. Each project
+descriptor has the following form: @samp{(@var{NAME} @var{DIR}
 @var{:OPTION} @var{VALUE} @dots{})}
 
+@vindex patcher-default-to-address
 @vindex :to-address
 @var{NAME} is a string naming your project, @var{DIR} is a string
 specifying the directory in which it resides. The remainder of a project
-descriptor is a sequence of zero or more option/value pairs. All option
-names start with a colon. The type of the value depends on the
-corresponding option. For example, there is a project option named
-@code{:to-address}, whose value should be a string giving the email
-address to which you want to send Patcher messages.
+descriptor is a sequence of zero or more option/value pairs, that we
+call @dfn{project options}. All option names start with a colon. The
+type of a value depends on the corresponding option. For example, there
+is a project option named @code{:to-address}, whose value should be a
+string giving the email address to which you want to send Patcher
+messages.
+
+When Patcher needs the value for a particular project option, it looks
+for it directly in the project descriptor, but also in other places.
+This process is described below.
+
+@menu
+* Themes::                 Collections of options
+* Project inheritance::    Getting options from other projects
+* Fallbacks::              Default values for project options
+* Retrieval::              The process of getting an option's value
+* Inheritance or theme ?:: The proper way to factor out option values
+@end menu
+
+@node Themes, Project inheritance, , Project Descriptors
+@comment  node-name,  next,  previous,  up
+@subsection Themes
+
+If you have several projects sharing the same option set, you might want
+to setup a theme. Themes are named collections of project options.
+
+@vindex patcher-themes
+Themes are stored in the @code{patcher-themes} user option. This option
+is a list of themes. Each theme has the following form:
+@samp{(@var{NAME} @var{:OPTION} @var{VALUE} @dots{})}.
+
+@var{NAME} is the theme's name (a symbol). The remainder of the list is
+a sequence of zero or more option/value pairs, just like in project
+descriptors.
+
+@vindex patcher-default-themes
+@vindex :themes
+In order to use a theme in a given project, a @code{:themes} project
+option is provided. It is a list of theme names (symbols). Use this
+option in your project descriptor, and the project will implicitly
+inherit all options from the corresponding theme.
+
+One important note: as @code{:themes} is a project option, it can appear
+in a theme. In other words, themes can inherit from other themes. When
+Patcher tries to retrieve an option, the themes tree is traversed in
+depth first.
+
+@vindex patcher-max-theme-depth
+Because themes can contain themes, a bogus setting might lead to an
+infinite loop (a cycle in a theme graph). To prevent this, the
+@code{patcher-max-theme-depth} user option is provided. It represents
+the expected maximum theme nesting level, and defaults to 8.
+
+
+@node Project inheritance, Fallbacks, Themes, Project Descriptors
+@comment  node-name,  next,  previous,  up
+@subsection Project inheritance
+
+When two projects are very similar, you might want to use the project
+inheritance mechanism described below.
+
+@vindex :inheritance
+There is a special project option called @code{:inheritance}. This
+option must be a list of project names (strings). The inheritance of a
+project defines a list of projects from which to inherit options.
+
+One important note: inherited projects might have their own
+@code{:inheritance} option set to other projects in turn. In other
+words, the project inheritance can be more than one level deep. When
+Patcher tries to retrieve an option, the inheritance tree is traversed
+in depth first.
+
+@vindex patcher-max-inheritance-depth
+Because inherited projects can inherit from projects, a bogus setting
+might lead to an infinite loop (a cycle in a project graph). To prevent
+this, the @code{patcher-max-inheritance-depth} user option is provided.
+It represents the expected maximum project inheritance level, and
+defaults to 8.
+
+The @code{:inheritance} project option is somewhat special in the sense
+that it can't appear in a theme. We will encounter other exceptions
+later in this manual.
+
+
+@node Fallbacks, Retrieval, Project inheritance, Project Descriptors
+@comment  node-name,  next,  previous,  up
+@subsection Fallbacks
 
 @vindex patcher-default-to-address
 @vindex :to-address
-For each existing project option, Patcher also has a fallback user
+For each existing project option, Patcher also has a @dfn{fallback} user
 option with a default value that would be shared among all (or some of)
-your projects. The name of the fallback variable is obtained by
-replacing the colon in the project option's name with the prefix
-@code{patcher-default-}. For example, the variable corresponding to the
+your projects. The name of the fallback is obtained by replacing the
+colon in the project option's name with the prefix
+@code{patcher-default-}. For example, the fallback corresponding to the
 @code{:to-address} project option is named
 @code{patcher-default-to-address}.
 
+The @code{:inheritance} project option is also special in the sense that
+it doesn't have a corresponding fallback. We will encounter other
+exceptions later in this manual.
+
+In the remainder of this manual, we will rarely mention the fallbacks
+again. When we introduce a new project option, just remember that it
+always has a corresponding fallback (well, not always, as you just
+discovered).
+
+
+@node Retrieval, Inheritance or theme ?, Fallbacks, Project Descriptors
+@comment  node-name,  next,  previous,  up
+@subsection Retrieval
+
+When Patcher needs the value of a particular project option, it looks
+for it in the following manner:
+
+@itemize @bullet
+@item
+First, it looks directly in the project descriptor to see if the option
+is given.
+
+@item
+@vindex patcher-default-themes
+@vindex :themes
+@vindex patcher-themes
+If that fails, it next tries the given themes, if any. This involves
+recursively traversing the project's themes tree. Options successfully
+retrieved in themes are said to be @dfn{themed}.
+
+@item
 @vindex :inheritance
-There is a special project option called @code{:inheritance}. This
-option must be a list of strings which correspond to project names. The
-inheritance of a project defines a list of projects from which to
-inherit options. Note that this project option is special in the sense
-that it doesn't have a corresponding fallback variable (that would be
-meaningless).
+If that still fails, it then tries the inherited projects, if any. This
+involves recursively traversing the project's inheritance tree. Options
+successfully retrieved in inherited projects are said to be
+@dfn{inherited}. Note that in turn, such options could have been
+actually themed in the inherited project.
 
-When Patcher needs the value of a particular option, it first looks in
-the project descriptor to see if the option is given. If that fails, it
-recursively looks in the options of all inherited projects. If that
-still fails, it finally falls back to the value given in the
-corresponding fallback variable.
+@item
+If that fails again, it finally falls back to the value given in the
+corresponding fallback (if it exists). In such a case, the option is
+said to be @dfn{fallbacked}.
+@end itemize
 
-Note that a value of @code{nil} for a project option @strong{is} a real
-value. It does not mean that the option is unset. As a consequence, if
-Patcher finds a project option with a value of @code{nil}, it will use
-it, even if the fallback variable as a non @code{nil} value. This
-provides you with a way to annihilate default values for some projects.
+Note that a value of @code{nil} for a project option @strong{is an
+actual value}. It is not equivalent to an option being unset. As a
+consequence, if Patcher finds a project option with a value of
+@code{nil} somewhere, it will use it and stop the search, even if a non
+nil value could be retrieved later from a theme, an inherited project or
+a fallback. This provides you with a way to annihilate themed,
+inherited or fallbacked options.
 
-In the remainder of this manual, we will rarely mention the fallback
-variables again. When we introduce a new project option, just remember
-that it always has a corresponding fallback.
+
+@node Inheritance or theme ?, , Retrieval, Project Descriptors
+@comment  node-name,  next,  previous,  up
+@subsection Inheritance or theme ?
+
+At that point, you might be wondering why the themes and inheritance
+concepts were both designed, since they actually perform very similar
+tasks. Good question. Here is a good answer.
+
+Projects might share options for different reasons. For example, my
+"XEmacs" (source) and "XEmacs Packages" projects share many options
+(@code{To:} address, @code{From:} address, diff and commit commands and
+so on) because they both relate to XEmacs. On the other hand I have
+personal but totally unrelated projects that share the same commands
+because they are all handled through a local PRCS archive.
+
+In other words, you should rather use the inheritance mechanism when
+projects relate to each other, and the theme mechanism for settings that
+are orthogonal the projects they apply to.
+
 
 
 @c Mail Preparation ===================================================
 @vindex :mail-method
 Since there are different mail packages working in XEmacs, Patcher
 supports different methods for preparing messages. You can specify the
-method you prefer thanks to the @code{:mail-method} project option. The
-value must be a symbol.
+method you prefer in the @code{:mail-method} project option. The value
+must be a symbol.
 
 @menu
 * Standard Mail Methods:: Patcher supports standard mail packages