1 ;;; patcher-instance.el --- Process instantiation
3 ;; Copyright (C) 2008, 2009, 2010, 2011 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: Sat Feb 13 22:43:33 2010
9 ;; Last Revision: Thu Jan 12 22:04:43 2012
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 3,
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.
38 (eval-when-compile (require 'patcher-cutil))
39 (require 'patcher-util)
40 (require 'patcher-project)
44 ;; ==========================================================================
46 ;; ==========================================================================
48 (patcher-define-error 'status
49 "Patcher status error.")
54 ;; ==========================================================================
55 ;; Project instantiation
56 ;; ==========================================================================
58 (defstruct (patcher-project (:constructor patcher-make-project))
67 ;; The name of the temporary file in which to store the log message. The
68 ;; reason why the logmsg-buffer does not visit this file (hence the need for
69 ;; this variable) is that we want the logmsg buffer's name to be readable,
70 ;; and the user doesn't care about the name of the temporary file.
72 ;; The diff command to use. This string is not supposed to include the files
73 ;; to which the command applies. Only the command itself. This variable is
74 ;; needed because the user has the ability to override the project's diff
75 ;; command by giving a prefix to `patcher-generate-diff'.
77 ;; The commit command to use. This variable is needed because the user has
78 ;; the ability to override the project's commit command by giving a prefix
79 ;; to `patcher-logmsg-commit'.
81 ;; The project's base directory. This variable is used to anchor ephemeral
82 ;; ChangeLogs, and is needed because a subproject may modify the original
83 ;; project's value for it.
85 ;; The project's command directory. This variable is needed for supporting
86 ;; relocatable projects.
88 ;; List of files/directories command-line specification for the diff
89 ;; command. This variable is needed because a temporary subproject may
90 ;; modify the original project's value for it.
92 ;; List of ChangeLog absolute file names.
94 ;; List of source absolute file names.
98 ;; Accessors ================================================================
100 (put 'patcher-project-option 'lisp-indent-function 1)
101 (defun* patcher-project-option
102 (project option &optional non-nil
103 &aux (name (patcher-project-name project))
104 (opt (patcher-descriptor-option
105 (patcher-project-descriptor name) option 0)))
106 ;; Return the value of OPTION in PROJECT, or fall back to the value of
107 ;; `patcher-default-OPTION'. If NON-NIL, check that the retreived value is
111 (or (symbol-value (intern-soft (concat "patcher-default-"
113 (symbol-name option) 1))))
115 (patcher-error "Project %s: option %s is null" name option)))))
117 (defun patcher-project-command-name (project)
118 ;; Return the value of the :name option in PROJECT, or PROJECT's name.
119 (or (patcher-project-option project :name)
120 (patcher-project-name project)))
122 (defun patcher-project-files (project)
123 ;; Return the sorted list of PROJECT's files.
124 ;; The files are returned relative to PROJECT's command directory.
125 ;; Files include both sources and ChangeLogs, unless ChangeLogs are
126 ;; ephemeral in PROJECT.
127 (let ((dir (patcher-project-command-directory project)))
129 (mapcar (lambda (file)
130 (patcher-file-relative-name file dir))
131 (if (eq (patcher-project-option project :change-logs-status)
133 (patcher-project-sources project)
134 (append (patcher-project-change-logs project)
135 (patcher-project-sources project)))))))
137 (defun patcher-project-sources-string (project)
138 ;; Return a string of all PROJECT's source files.
139 (patcher-files-string (patcher-project-sources project)))
141 (defun patcher-project-change-logs-string (project)
142 ;; Return a string of all PROJECT's ChangeLog files.
143 (patcher-files-string (patcher-project-change-logs project)))
146 ;; Instantiation ============================================================
148 ;; #### NOTE: this variable is set in every auxiliary buffer (see below).
149 ;; Technically, it is not required in process buffers; only in buffers where
150 ;; interactive functions might be called without a project as argument. This
151 ;; variable is not set in reference buffers (see also below) because they
152 ;; might be shared by different instances of patcher. In ChangeLog files for
153 ;; instance, we have extents providing the necessary information about every
155 (make-variable-buffer-local
156 (defvar patcher-project nil
157 ;; Instance of the PATCHER-PROJECT structure describing the current
161 (defvar patcher-subject-history nil)
163 (defun* patcher-prompt-subject
165 &aux (subject (read-string "Subject: "
166 (let ((s (patcher-project-option project :subject)))
167 (unless (zerop (length s))
168 (patcher-substitute-name project s)))
169 'patcher-subject-history)))
170 ;; Prompt for, and return a subject for PROJECT.
171 ;; Also create the 'patcher-subject extent in the string.
172 (set-extent-properties (make-extent 0 (length subject) subject)
178 (defvar patcher-directory-history nil)
181 ;; #### NOTE: supporting subprojects is in fact rather complicated because of
182 ;; the various ways the supported RCSes behave. The situation is as follows:
184 ;; 1/ At first glance, it may seem simpler to always execute commands from the
185 ;; (super)project's base directory, turning a :subdirectory into a
186 ;; specification. However, this would make things difficult for the user of a
187 ;; Mercurial repository with plenty of submodules (e.g. the XEmacs packages).
188 ;; Indeed, it would then be necessary to define one distinct project for every
189 ;; submodule because the commands need to be executed locally in every
190 ;; submodule. Patcher could perhaps support this by parsing .hgsub and
191 ;; dynamically create all subprojects as regular projects but we're not quite
192 ;; there yet (this brings the more general question of submodules that exist
193 ;; in various forms in different RCSes). So at least for the time being, we
194 ;; choose to work directly in the specified subdirectory, if any. This,
195 ;; however, causes additional problems.
197 ;; 2/ PRCS can't work in subdirectories. Only from the toplevel. Hence the
198 ;; :command-directory option which cancels the above policy.
200 ;; 3/ Some RCSes like CVS output diff paths relative to the current directory
201 ;; by default. Some others like Git don't (Git needs --relative, so we need
202 ;; to be careful in themes). In Mercurial, there doesn't seem to be a way to
203 ;; get relative paths and that is currently a problem.
205 ;; 4/ Then, there is the commit command problem. While Git diff --relative
206 ;; restricts to the current directory, there's no corresponding behavior for
207 ;; the commit command. For instance, if you git commit -a from a subdirctory,
208 ;; even the changes outside the current directory will be committed. Because
209 ;; of that, we can't work with empty specifications in subdirectories (we need
210 ;; at least to use ".").
212 ;; All in all, this is why the function below is such a mess.
214 (defun* patcher-prompt-project
216 &aux (project (patcher-make-project :name (patcher-prompt-name)))
218 (patcher-descriptor-directory
219 (patcher-project-descriptor (patcher-project-name project))))
220 (subdirectory (patcher-project-option project :subdirectory))
222 (patcher-project-option project :command-directory))
223 (files (patcher-project-option project :files))
224 default-working-directory working-directory)
225 ;; Create and return a new project by prompting for it.
226 ;; Always prompt for the project's name and subject.
227 ;; When RELOCATE, offer to use an alternate directory.
228 ;; When OVERRIDE, offer to work on a temporary subproject.
229 ;; Note that it is possible to both relocate and override.
231 ;; Initialize non problematic slots.
232 (setf (patcher-project-subject project) (patcher-prompt-subject project))
233 (setf (patcher-project-diff-command project)
234 (patcher-project-option project :diff-command t))
235 (setf (patcher-project-commit-command project)
236 (patcher-project-option project :commit-command t))
238 ;; Handle relocation. SUBDIRECTORY, COMMAND-DIRECTORY and FILES are all
239 ;; supposed to be relative paths so only BASE-DIRECTORY needs to be modified
241 (when (or relocate (null base-directory))
245 (format (if base-directory
246 "Relocate %s under: "
248 (file-name-as-directory subdirectory))
249 (if base-directory "Relocate to: " "Project location: "))
250 ;; #### NOTE: I think it's nice to start from ../ because many
251 ;; people keep clones of the same project under one directory. It
252 ;; might however not be so intuitive that if you don't type
253 ;; anything, you will get the original directory, not the one
254 ;; that's printed in the minibuffer.
256 (file-name-as-directory (expand-file-name ".." base-directory)))
257 base-directory t nil patcher-directory-history))
259 ;; Check that the relocation is valid: we need both a valid subdirectory
260 ;; and command (super) directory.
262 (unless (file-exists-p (expand-file-name subdirectory base-directory))
263 (patcher-error "Unable to relocate subdirectory %s under %s"
264 subdirectory base-directory)))
265 (when command-directory
266 (unless (file-exists-p (expand-file-name command-directory
268 (patcher-error "Unable to relocate command directory %s under %s"
269 command-directory base-directory))))
270 (setf (patcher-project-base-directory project) base-directory)
272 ;; Compute the default working directory (that is, before a potential
273 ;; override) and the actual one which may be overridden.
274 (setq default-working-directory
277 (expand-file-name subdirectory base-directory)
279 (setq working-directory
282 (read-directory-name "Subdirectory: "
283 (file-name-as-directory
284 default-working-directory)
285 default-working-directory
287 default-working-directory))
289 ;; #### WARNING: this filtering is very complicated to do because of the
290 ;; possibility to use wildcards. The code below doesn't really work so for
291 ;; the time being, I'm backing this out and just cancelling the :files
294 ;;; ;; If we have overridden the working directory, we need to filter out the
295 ;;; ;; :files that do not belong to that new subdirectory.
296 ;;; (unless (string= default-working-directory working-directory)
297 ;;; ;; #### FIXME: this will break if someone dares to define a subproject
298 ;;; ;; with files starting with ../ and other clever tricks. Let's just hope
299 ;;; ;; nobody does that.
300 ;;; (setq files (loop with subdir = (directory-file-name
301 ;;; (patcher-file-relative-name
302 ;;; working-directory
303 ;;; default-working-directory))
304 ;;; with length = (length subdir)
305 ;;; for file in files
306 ;;; if (string= subdir (directory-file-name file))
307 ;;; collect "*" and collect ".*"
309 ;;; when (and (> (length file) (1+ length))
311 ;;; (substring file 0 length))
312 ;;; (char= ?/ (aref file length)))
313 ;;; collect (substring file (1+ length)))))
315 (unless (string= default-working-directory working-directory)
318 ;; Handle a request for overriding the :files.
321 ;; #### FIXME: this will break when a filename contains spaces!
323 (let ((default-directory
324 (file-name-as-directory working-directory)))
326 (let ((files-string (mapconcat #'identity files " ")))
327 (read-shell-command "Files: "
328 (concat files-string " "))))
329 (let* ((default-file (when (buffer-file-name)
330 (patcher-file-relative-name
334 ;; If the file is not actually underneath the
335 ;; project, then don't suggest it as a possibility.
337 (if (string-match "^\\.\\.$\\|^\\.\\.[/\\]"
341 (read-shell-command "Files: " default-file)))))))
343 ;; If we are working in a subdirectory with no specification (no :files
344 ;; option provided), then we need to make an explicit specification of ".".
345 ;; See point #4 in the comment above this function.
346 (when (and (or (not (string= default-working-directory working-directory))
349 (setq files (list ".")))
351 ;; Initialize the project's command directory and files specification.
352 ;; When a command directory is given, we further need to relativize all the
353 ;; :files to this directory.
354 (cond (command-directory
355 (setq command-directory
357 (expand-file-name command-directory base-directory)))
358 (setf (patcher-project-command-directory project) command-directory)
359 (setf (patcher-project-specification project)
363 (patcher-file-relative-name
364 (expand-file-name file working-directory)
367 (unless (string= command-directory working-directory)
368 (list (patcher-file-relative-name
369 working-directory command-directory))))))
371 (setf (patcher-project-command-directory project) working-directory)
372 (setf (patcher-project-specification project) files)))
374 ;; Finally, initialize the process buffer. This needs to wait until the
375 ;; command directory is properly initialized.
376 ;; unclean forward reference
377 (patcher-setup-auxiliary-buffer project
378 (setf (patcher-project-process-buffer project)
379 (generate-new-buffer " *Patcher Process*")))
383 (defun patcher-project-interactive (prefix)
384 ;; Create and return a new project based on PREFIX requests in a list.
385 ;; See `patcher-mail' for the semantics of PREFIX.
386 (list (patcher-prompt-project (member prefix '(1 -1))
387 (and prefix (not (eq prefix 1))))))
393 ;; ==========================================================================
394 ;; Project-related utilities
395 ;; ==========================================================================
397 (put 'patcher-substitute-name 'lisp-indent-function 1)
398 (defun* patcher-substitute-name
399 (project str &aux (command-name (patcher-project-command-name project))
400 (name (patcher-project-name project)))
401 ;; Replace a %n in STR with PROJECT's command name.
402 ;; Replace a %N in STR with PROJECT's name.
403 (let (case-fold-search)
404 (replace-in-string (replace-in-string str "%N" name) "%n" command-name)))
406 (defun* patcher-prefixed-subject
407 (project &optional old-subject
408 &aux (subject-prefix (patcher-project-option project
410 (subject (patcher-project-subject project)))
411 ;; Return PROJECT's prefixed subject, possibly adapting OLD-SUBJECT.
412 (unless (zerop (length subject-prefix))
413 (setq subject-prefix (patcher-substitute-name project subject-prefix))
414 (set-extent-properties (make-extent 0 (length subject-prefix)
418 patcher-subject-prefix t)))
419 (unless (zerop (length old-subject))
421 (if (zerop (length subject))
423 (let (case-fold-search)
426 (patcher-project-option project :subject-rewrite-format)
429 (concat subject-prefix
430 (unless (or (zerop (length subject-prefix))
431 (zerop (length subject)))
435 (defun patcher-change-subject (project)
436 ;; Read a new subject for PROJECT and propagate it to relevant buffers.
437 (setf (patcher-project-subject project)
438 (read-string "New subject: " (patcher-project-subject project)))
439 (dolist (buffer (list (patcher-project-mail-buffer project)
440 (patcher-project-logmsg-buffer project)))
442 (with-current-buffer buffer
443 (patcher-within-extent (extent 'subject)
444 (insert (patcher-project-subject project))
445 (delete-region (point) (extent-end-position extent)))))))
447 (put 'patcher-command 'lisp-indent-function 1)
448 (defun patcher-command (project command &optional files)
449 ;; Build a Patcher command from COMMAND that applies to FILES.
450 ;; This involves %n, %N, %f, %!f and %?f substitutions,
451 ;; as well as :pre-command handling.
452 ;; Special value t for FILES means use all ChangeLog (unless ephemeral) and
453 ;; source files explicitely.
456 (setq files (if (patcher-project-specification project)
457 (patcher-project-files project)
459 (setq command (patcher-substitute-name project command))
460 (setq command (replace-in-string command "%!f{\\(.*?\\)}"
461 (if files "" "\\1")))
462 (setq command (replace-in-string command "%\\?f{\\(.*?\\)}"
463 (if files "\\1" "")))
464 (setq command (replace-in-string command "%f"
466 (mapconcat #'identity files " ")
469 (let ((precmd (patcher-project-option project :pre-command)))
470 (unless (zerop (length precmd))
471 (setq command (concat precmd " " command))))
472 (setq command (replace-in-string command "[ \t]+" " " t))
476 ;; Auxiliary buffers ========================================================
478 ;; Auxiliary buffers are used by Patcher to perform specific tasks. They are
479 ;; not shared across Patcher instances, may be lazily generated and behave
480 ;; like singletons. Patcher process, log message and commit command buffers
481 ;; are auxiliary buffers.
483 (put 'patcher-setup-auxiliary-buffer 'lisp-indent-function 1)
484 (defun patcher-setup-auxiliary-buffer (project buffer)
485 ;; Setup BUFFER as auxiliary for PROJECT.
486 (with-current-buffer buffer
487 (setq patcher-project project)
488 (cd (patcher-project-command-directory project))))
491 ;; Reference buffers ========================================================
493 ;; Reference buffers are used by Patcher to get project-related information.
494 ;; They may be shared across patcher instances and may exist independently
495 ;; from patcher. Source and ChangeLog buffers are reference buffers.
497 (make-variable-buffer-local
498 (defvar patcher-references nil
499 ;; List of Patcher instances referencing this buffer.
500 ;; Automatically becomes buffer-local when set.
501 ;; - If this list contains the special value t, it means that the buffer
502 ;; existed before any instance of Patcher required it, so it will never be
503 ;; killed (by Patcher).
504 ;; - Otherwise, Patcher may kill this buffer when there are no more
508 (put 'patcher-reference-buffer 'lisp-indent-function 1)
509 (defun patcher-reference-buffer (project buffer)
510 ;; Add a reference to PROJECT in BUFFER.
511 ;; Return non-nil if the reference didn't exist before.
512 (with-current-buffer buffer
513 (unless (member* project patcher-references :test #'eq)
514 (push project patcher-references))))
516 (put 'patcher-unreference-buffer 'lisp-indent-function 2)
517 (defun patcher-unreference-buffer (project buffer &optional kill force-save)
518 ;; Remove the reference to PROJECT from BUFFER.
519 ;; If KILL and PROJECT was the last reference in BUFFER, authorize Patcher
520 ;; to kill BUFFER. If FORCE-SAVE, force saving before killing.
521 ;; Return two values: whether PROJECT was the last Patcher reference in
522 ;; BUFFER, and whether BUFFER was killed.
523 (with-current-buffer buffer
524 (assert (member* project patcher-references :test #'eq))
525 (setq patcher-references (delete* project patcher-references :test #'eq))
526 (let ((lastp (or (null patcher-references)
527 (equal patcher-references '(t))))
529 (when (and (null patcher-references) kill)
530 (patcher-save-buffer buffer force-save)
533 (values lastp killp))))
536 ;; Project cleanup ==========================================================
538 (defun patcher-delete-project (project &optional kill-mail-buffer)
539 ;; Delete PROJECT and remove all dependencies.
540 (declare-fboundp (patcher-unlink-sources project))
541 (declare-fboundp (patcher-unlink-change-logs project))
542 (kill-buffer (patcher-project-process-buffer project))
543 (when (patcher-project-logmsg-buffer project)
544 (kill-buffer (patcher-project-logmsg-buffer project)))
545 (when (patcher-project-cmtcmd-buffer project)
546 (kill-buffer (patcher-project-cmtcmd-buffer project)))
547 (when (patcher-project-mail-buffer project)
549 (kill-buffer (patcher-project-mail-buffer project))
550 (with-current-buffer (patcher-project-mail-buffer project)
551 (setq patcher-project nil)))))
553 (defun patcher-kill-project (project)
555 (when (yes-or-no-p "Really abort the project? ")
556 (unless (or (eq (patcher-project-option project :change-logs-status)
558 (patcher-project-committed-p project))
559 (with-fboundp '(patcher-generated-change-logs
560 patcher-ungenerate-change-logs)
561 (let ((change-log-buffers
562 (patcher-generated-change-logs patcher-project)))
563 (when change-log-buffers
564 (case (patcher-with-message
566 Some ChangeLog skeletons for this project have been generated.
567 The relevant ChangeLog files are the following: %s.
569 Before killing the project, please answer the question below to
570 remove all skeletons (y), keep them all (n) or choose interactively (i)."
571 (patcher-buffers-string change-log-buffers))
572 (patcher-read-char "Remove ChangeLog skeletons? " "yni"))
573 (?y (patcher-ungenerate-change-logs patcher-project
575 (?i (patcher-ungenerate-change-logs patcher-project
578 :prompt "Remove this skeleton? ")))))))
579 (patcher-delete-project project 'kill-mail-buffer)))
582 ;; Project commands ==========================================================
584 (put 'patcher-call-command 'common-lisp-indent-function 2)
585 (defun* patcher-call-command
586 (project command &key (erase t) progression ignore-exit-status)
587 ;; Call COMMAND for PROJECT.
588 ;; Output goes to process buffer.
589 ;; If ERASE (the default), erase BUFFER first.
590 ;; Return two values delimiting the process output's region.
591 ;; Throw a PATCHER-PROCESS error for non-zero exit status or a
592 ;; failed-command-regexp option match in the process output.
593 (with-current-buffer (patcher-project-process-buffer project)
597 (end (patcher-call-process command progression ignore-exit-status))
598 (failed-command-regexp (patcher-project-option project
599 :failed-command-regexp)))
600 (when failed-command-regexp
601 (patcher-with-progression "Checking command output"
603 (when (re-search-forward failed-command-regexp end t)
604 (patcher-error 'process command))))
608 ;; Project commit ===========================================================
610 (patcher-define-error 'committed
611 "Patcher committed error."
614 (defun patcher-detect-committed-project (project)
615 ;; Detect an already committed PROJECT.
616 ;; Throw a committed error when detected.
617 (when (patcher-project-committed-p project)
618 (patcher-error 'committed)))
621 (patcher-define-error 'undiffable
622 "Patcher undiffable error."
625 (defun patcher-detect-undiffable-project (project)
626 ;; Detect an undiffable PROJECT.
627 ;; Throw an undiffable error when detected.
628 (when (patcher-project-committed-p project)
629 (patcher-error 'undiffable)))
632 (patcher-define-error 'commit
633 "Patcher commit error."
636 ;; #### NOTE: this function assumes that PROJECT is committable because it is
637 ;; too low-level to perform the check. Other, upper-level functions will
638 ;; perform the check.
639 (defun patcher-commit-project (project command)
640 ;; Commit PROJECT with commit COMMAND.
641 (patcher-condition-case nil
642 (patcher-call-command project command
643 :progression "Committing changes")
645 (patcher-error 'commit command)))
646 (setf (patcher-project-committed-p project) t)
647 ;; Record the successful commit in the mail message.
648 (set-window-configuration (patcher-project-window-configuration project))
649 (with-current-buffer (patcher-project-mail-buffer project)
651 ;; Possibly change the subject:
652 (let ((subject-committed-prefix
653 (patcher-project-option project :subject-committed-prefix)))
654 (when subject-committed-prefix
655 (setq subject-committed-prefix
656 (patcher-substitute-name project subject-committed-prefix))
657 (patcher-within-extent (extent 'subject-prefix)
658 (insert subject-committed-prefix)
659 (delete-region (point) (extent-end-position extent)))
660 (goto-char (extent-end-position (patcher-extent 'subject-prefix)))
661 (when (looking-at "\\S-")
663 ;; Insert the `committed' notice:
664 (goto-char (point-min))
665 (when (re-search-forward
666 (concat "^" (regexp-quote mail-header-separator))
669 (let ((notice (patcher-project-option patcher-project
671 (unless (zerop (length notice))
672 (insert notice "\n")))))))
675 (provide 'patcher-instance)
677 ;;; patcher-instance.el ends here