;;; xmtn-automate.el --- Interface to monotone's "automate" functionality ;; Copyright (C) 2008, 2009 Stephen Leake ;; Copyright (C) 2006, 2007 Christian M. Ohler ;; Author: Christian M. Ohler ;; Keywords: tools ;; This file is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation; either version 2 of the License, or ;; (at your option) any later version. ;; ;; This file 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 ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with this file; see the file COPYING. If not, write to ;; the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, ;; Boston, MA 02110-1301 USA. ;;; Commentary: ;; This library provides access to monotone's "automate" interface ;; from Emacs Lisp. ;; ;; see http://www.monotone.ca/docs/Automation.html#Automation for ;; details of the monotone automate command. ;; ;; mtn automate allows sending several commands to a single mtn ;; process, and provides the results in a form that is easy to ;; parse. It does some caching between command, and will do more in ;; the future, so this is a significant speed-up over spawning a new ;; subprocess for each command. ;; ;; To allow xmtn-automate to track how long an automate stdio process ;; needs to be kept around, and to store meta data, we introduce the ;; concept of a session. To the programmer using this library, a ;; session is an opaque object that is needed to run automate ;; commands. Each session is associated with a monotone workspace ;; ("root") that the commands will operate on. A session can be ;; obtained using `xmtn-automate-cache-session'. Note that ;; `xmtn-automate-cache-session' doesn't necessarily start a fresh ;; monotone process, if a session with that root already exists. The ;; process must be killed with `xmtn-automate-kill-session'. ;; ;; Once you have a session object, you can use ;; `xmtn-automate-new-command' to send commands to monotone. ;; ;; A COMMAND is a list of strings (the command and its arguments), or ;; a cons of lists of strings. If car COMMAND is a list, car COMMAND is ;; options (without leading "--"), cdr is the command and arguments. ;; ;; `xmtn-automate-new-command' returns a command handle. You use this ;; handle to check the error code of the command and obtain its ;; output. Your Emacs Lisp code can also do other computation while ;; the monotone command runs. Allowing this kind of parallelism and ;; incremental processing of command output is the main reason for ;; introducing command handles. ;; ;; The intention behind this protocol is to allow Emacs Lisp code to ;; process command output incrementally as it arrives instead of ;; waiting until it is complete. However, for xmtn-basic-io, the ;; bookkeeping overhead for this kind of pipelining was excessive -- ;; byte-compiled Emacs Lisp is rather slow. But I didn't try very ;; hard to tune it, either. So I'm not sure whether incremental ;; processing is useful. ;; ;; In the output buffer, the mtn stdio output header (::::) has been processed; ;; only the data is present. ;; There are some notes on the design of xmtn in ;; docs/xmtn-readme.txt. ;;; Code: (eval-and-compile (require 'cl) (require 'parse-time) ;for parse-integer (require 'xmtn-base) (require 'xmtn-run) (require 'xmtn-compat)) (defun xmtn-automate-command-error-code (command) (let ((process (xmtn-automate--session-process (xmtn-automate--command-handle-session command)))) (while (null (xmtn-automate--command-handle-error-code command)) (xmtn--assert-for-effect (accept-process-output process)))) (xmtn-automate--command-handle-error-code command)) (defun xmtn-automate-command-buffer (command) (xmtn-automate--command-handle-buffer command)) (defun xmtn-automate-command-write-marker-position (command) (marker-position (xmtn-automate--command-handle-write-marker command))) (defun xmtn-automate-command-accept-output (command) (let ((previous-write-marker-position (marker-position (xmtn-automate--command-handle-write-marker command)))) (while (and (= (marker-position (xmtn-automate--command-handle-write-marker command)) previous-write-marker-position) (not (xmtn-automate--command-handle-finished-p command))) (xmtn--assert-for-effect (accept-process-output (xmtn-automate--session-process (xmtn-automate--command-handle-session command))))) (> (marker-position (xmtn-automate--command-handle-write-marker command)) previous-write-marker-position))) (defun xmtn-automate-command-finished-p (command) (xmtn-automate--command-handle-finished-p command)) (defun xmtn-automate-command-wait-until-finished (handle) (while (not (xmtn-automate-command-finished-p handle)) (xmtn--assert-for-effect (or (xmtn-automate-command-accept-output handle) (xmtn-automate-command-finished-p handle)))) nil) (defvar xmtn-automate--*sessions* '() "Assoc list of sessions, indexed by uniquified root directory.") (defun xmtn-automate-cache-session (root) "If necessary, create a mtn automate session for workspace ROOT, store it in session cache. Return session." ;; we require an explicit root argument here, rather than relying on ;; default-directory, because one application is to create several ;; sessions for several workspaces, and operate on them as a group ;; (see xmtn-multi-status.el, for example). (let* ((default-directory (dvc-uniquify-file-name root)) (session (xmtn-automate-get-cached-session default-directory))) (or session (progn (setq session (xmtn-automate--make-session default-directory default-directory)) (setq xmtn-automate--*sessions* (acons default-directory session xmtn-automate--*sessions*)) session)))) (defun xmtn-automate-get-cached-session (key) "Return a session from the cache, or nil. KEY is uniquified workspace root." (cdr (assoc key xmtn-automate--*sessions*))) (defun xmtn-automate--command-output-as-string-ignoring-exit-code (handle) (xmtn-automate-command-wait-until-finished handle) (with-current-buffer (xmtn-automate-command-buffer handle) (prog1 (buffer-substring-no-properties (point-min) (point-max)) (xmtn-automate--cleanup-command handle)))) (defun xmtn-automate-command-check-for-and-report-error (handle) (unless (eql (xmtn-automate-command-error-code handle) 0) (error "mtn automate command (arguments %S) reported an error (code %s):\n%s" (xmtn-automate--command-handle-arguments handle) (xmtn-automate-command-error-code handle) (xmtn-automate--command-output-as-string-ignoring-exit-code handle))) nil) (defun xmtn-automate-simple-command-output-string (root command) "Send COMMAND to session for ROOT. Return result as a string." (let* ((session (xmtn-automate-cache-session root)) (command-handle (xmtn-automate--new-command session command nil))) (xmtn-automate-command-check-for-and-report-error command-handle) (xmtn-automate--command-output-as-string-ignoring-exit-code command-handle))) (defun xmtn-automate-simple-command-output-insert-into-buffer (root buffer command) "Send COMMAND to session for ROOT, insert result into BUFFER." (let* ((session (xmtn-automate-cache-session root)) (command-handle (xmtn-automate--new-command session command nil))) (xmtn-automate-command-check-for-and-report-error command-handle) (xmtn-automate-command-wait-until-finished command-handle) (with-current-buffer buffer (insert-buffer-substring-no-properties (xmtn-automate-command-buffer command-handle))) (xmtn-automate--cleanup-command command-handle))) (defun xmtn-automate-command-output-lines (handle) "Return list of lines of output in HANDLE; first line output is first in list." (xmtn-automate-command-check-for-and-report-error handle) (xmtn-automate-command-wait-until-finished handle) (save-excursion (set-buffer (xmtn-automate-command-buffer handle)) (goto-char (point-min)) (let (result) (while (< (point) (point-max)) (setq result (cons (buffer-substring-no-properties (point) (progn (end-of-line) (point))) result)) (forward-line 1)) (xmtn-automate--cleanup-command handle) (nreverse result)))) (defun xmtn-automate-simple-command-output-lines (root command) "Return list of strings containing output of COMMAND, one line per string." (let* ((session (xmtn-automate-cache-session root)) (command-handle (xmtn-automate--new-command session command nil))) (xmtn-automate-command-output-lines command-handle))) (defun xmtn-automate-simple-command-output-line (root command) "Return the one line output from mtn automate as a string. Signals an error if output contains zero lines or more than one line." (let ((lines (xmtn-automate-simple-command-output-lines root command))) (unless (eql (length lines) 1) (error "Expected precisely one line of output from mtn automate, got %s: %s %S" (length lines) xmtn-executable command)) (first lines))) (defun xmtn-automate--set-process-session (process session) (process-put process 'xmtn-automate--session session)) (defun xmtn-automate--process-session (process) (process-get process 'xmtn-automate--session)) (defstruct (xmtn-automate--decoder-state (:constructor xmtn-automate--%make-raw-decoder-state)) (read-marker) (remaining-chars 0) (last-p nil)) (defstruct (xmtn-automate--session (:constructor xmtn-automate--%make-raw-session) (:copier xmtn-automate--copy-session)) (root) (name) (buffer nil) (process nil) (decoder-state) (next-command-number 0) (must-not-kill-counter) (remaining-command-handles) (sent-kill-p) (closed-p nil)) (defstruct (xmtn-automate--command-handle (:constructor xmtn-automate--%make-raw-command-handle)) (arguments) (mtn-command-number) (session-command-number) (session) (buffer) (write-marker) (may-kill-p) (finished-p nil) (error-code nil)) (defun* xmtn-automate--initialize-session (session &key root name) (xmtn--assert-optional (equal root (file-name-as-directory root)) t) (setf (xmtn-automate--session-root session) root (xmtn-automate--session-name session) name (xmtn-automate--session-process session) nil (xmtn-automate--session-closed-p session) nil) nil) (defun xmtn-automate--make-session (root key) (dvc-trace "new session %s" key) (let* ((name (format "xmtn automate session for %s" key))) (let ((session (xmtn-automate--%make-raw-session))) (xmtn-automate--initialize-session session :root root :name name) session))) (defun xmtn-automate--session-send-process-kill (session) (let ((process (xmtn-automate--session-process session))) ;; Stop parser. (setf (xmtn-automate--session-sent-kill-p session) t) (with-current-buffer (xmtn-automate--session-buffer session) (let ((inhibit-read-only t) deactivate-mark) (save-excursion (goto-char (process-mark process)) (insert "\n(killing process)\n") (set-marker (process-mark process) (point))))) ;; Maybe this should really be a sigpipe. But let's not get too ;; fancy (ha!) and non-portable. ;;(signal-process (xmtn-automate--session-process session) 'PIPE) ;; This call to `sit-for' is apparently needed in some situations to ;; make sure the process really gets killed. (sit-for 0) (interrupt-process process)) nil) (defun xmtn-automate--close-session (session) "Kill session process, buffer." (setf (xmtn-automate--session-closed-p session) t) (let ((process (xmtn-automate--session-process session))) (cond ((null process) ;; Process died for some reason - most likely 'mtn not found in ;; path'. Don't warn if buffer hasn't been deleted; that ;; obscures the real error message nil) ((ecase (process-status process) (run nil) (exit t) (signal t)) (unless xmtn-automate--*preserve-buffers-for-debugging* (kill-buffer (xmtn-automate--session-buffer session)))) (t (process-send-eof process) (if (zerop (xmtn-automate--session-must-not-kill-counter session)) (xmtn-automate--session-send-process-kill session) ;; We can't kill the buffer yet. We need to dump mtn's output ;; in there so we can parse it and determine when the critical ;; commands are finished so we can then kill mtn. (dvc-trace "Not killing process %s yet: %s out of %s remaining commands are critical" (process-name process) (xmtn-automate--session-must-not-kill-counter session) (length (xmtn-automate--session-remaining-command-handles session)))) (with-current-buffer (xmtn-automate--session-buffer session) ;; This isn't essential but helps debugging. (rename-buffer (format "*%s: killed session*" (xmtn-automate--session-name session)) t)) (let ((fake-session (xmtn-automate--copy-session session))) (xmtn-automate--set-process-session process fake-session))))) nil) (defun xmtn-automate--start-process (session) (xmtn--check-cached-command-version) (xmtn--assert-optional (not (xmtn-automate--session-closed-p session))) (xmtn--assert-optional (typep session 'xmtn-automate--session)) (let ((name (xmtn-automate--session-name session)) (buffer (xmtn-automate--new-buffer session)) (root (xmtn-automate--session-root session))) (let ((process-connection-type nil) (default-directory root)) (let ((process (apply 'start-process name buffer xmtn-executable "automate" "stdio" xmtn-additional-arguments))) (xmtn-automate--set-process-session process session) (set-process-filter process 'xmtn-automate--process-filter) (set-process-sentinel process 'xmtn-automate--process-sentinel) (xmtn--set-process-query-on-exit-flag process nil) ;; Need binary (or no-conversion or maybe raw-text-unix?) ;; since this is the format in which mtn automate stdio ;; computes the size of the output. (set-process-coding-system process 'binary 'binary) (setf (xmtn-automate--session-process session) process) (setf (xmtn-automate--session-decoder-state session) (xmtn-automate--%make-raw-decoder-state :read-marker (with-current-buffer buffer (xmtn--assert-optional (eql (point-min) (point)) t) (set-marker (make-marker) (point-min))))) (setf (xmtn-automate--session-must-not-kill-counter session) 0) (setf (xmtn-automate--session-remaining-command-handles session) (list)) (setf (xmtn-automate--session-sent-kill-p session) nil) process)))) (defun xmtn-automate--ensure-process (session) "Ensure SESSION has an active process; restart it if it died." (let ((process (xmtn-automate--session-process session))) (when (or (null process) (ecase (process-status process) (run nil) (exit t) (signal t))) (setq process (xmtn-automate--start-process session)) (setf (xmtn-automate--session-process session) process)) (xmtn--assert-optional (buffer-live-p (xmtn-automate--session-buffer session))) process)) (defun xmtn-automate--new-buffer (session) (let* ((buffer-base-name (format " *%s: session*" (xmtn-automate--session-name session))) (buffer (generate-new-buffer buffer-base-name))) (with-current-buffer buffer (buffer-disable-undo) (set-buffer-multibyte nil) (setq buffer-read-only t)) (setf (xmtn-automate--session-buffer session) buffer) buffer)) (defun xmtn-automate--append-encoded-strings (strings) "Encode STRINGS (a list of strings or nil) in automate stdio format, insert into current buffer. Assumes that point is at the end of the buffer." (xmtn--assert-optional (eql (point) (point-max))) (dolist (string strings) (if string (progn (save-excursion (insert string)) (encode-coding-region (point) (point-max) 'xmtn--monotone-normal-form) (insert (number-to-string (- (point-max) (point))) ":") (goto-char (point-max))))) nil) (defun xmtn-automate--send-command-string (session command option-plist session-number) "Send COMMAND and OPTION-PLIST to SESSION." (let* ((buffer-name (format "*%s: input for command %s*" (xmtn-automate--session-name session) session-number)) (buffer nil)) (unwind-protect (progn (when (get-buffer buffer-name) ;; Make sure the buffer is in a clean state. (with-current-buffer buffer-name (let ((inhibit-read-only t)) (erase-buffer)) (fundamental-mode))) (setq buffer (get-buffer-create buffer-name)) (with-current-buffer buffer (buffer-disable-undo) (set-buffer-multibyte t) (setq buffer-read-only t) (let ((inhibit-read-only t)) (when option-plist (insert "o") (xmtn-automate--append-encoded-strings option-plist) (insert "e")) (insert "l") (xmtn-automate--append-encoded-strings command) (insert "e\n")) (dvc-trace "mtn automate: '%s'" (buffer-substring (point-min) (point-max))) (process-send-region (xmtn-automate--session-process session) (point-min) (point-max)))) (when buffer (unless xmtn-automate--*preserve-buffers-for-debugging* (kill-buffer buffer)))))) (defun xmtn-automate--new-command (session command may-kill-p) "Send COMMAND to SESSION." (xmtn-automate--ensure-process session) (let* ((command-number (1- (incf (xmtn-automate--session-next-command-number session)))) (buffer-name (format " *%s: output for command %s*" (xmtn-automate--session-name session) command-number)) (buffer (progn (when (get-buffer buffer-name) ;; Make sure no local variables or mode changes ;; remain from the previous command parser. (with-current-buffer buffer-name (let ((inhibit-read-only t)) (erase-buffer)) (fundamental-mode))) (get-buffer-create buffer-name)))) (if (not (listp (car command))) (xmtn-automate--send-command-string session command '() command-number) (xmtn-automate--send-command-string session (cdr command) (car command) command-number)) (with-current-buffer buffer (buffer-disable-undo) (set-buffer-multibyte nil) (setq buffer-read-only t) (xmtn--assert-optional (and (eql (point) (point-min)) (eql (point) (point-max)))) (let ((handle (xmtn-automate--%make-raw-command-handle :session session :arguments command :session-command-number command-number :may-kill-p may-kill-p :buffer buffer :write-marker (set-marker (make-marker) (point))))) (setf (xmtn-automate--session-remaining-command-handles session) (nconc (xmtn-automate--session-remaining-command-handles session) (list handle))) (when (not may-kill-p) (incf (xmtn-automate--session-must-not-kill-counter session)) (xmtn--set-process-query-on-exit-flag (xmtn-automate--session-process session) t)) handle)))) (defun xmtn-automate--cleanup-command (handle) (unless xmtn-automate--*preserve-buffers-for-debugging* (kill-buffer (xmtn-automate--command-handle-buffer handle)))) (defsubst xmtn-automate--process-new-output--copy (session) (let* ((session-buffer (xmtn-automate--session-buffer session)) (state (xmtn-automate--session-decoder-state session)) (read-marker (xmtn-automate--decoder-state-read-marker state)) (command (first (xmtn-automate--session-remaining-command-handles session))) (command-output-buffer (xmtn-automate--command-handle-buffer command)) (write-marker (xmtn-automate--command-handle-write-marker command))) (xmtn--assert-optional (not (xmtn-automate--session-sent-kill-p session))) (with-current-buffer session-buffer (let* ((end (min (+ read-marker (xmtn-automate--decoder-state-remaining-chars state)) (point-max))) (chars-to-read (- end read-marker))) (cond ((= chars-to-read 0) nil) ((> chars-to-read 0) (if (not (buffer-live-p command-output-buffer)) ;; Buffer has already been killed, just discard input. (progn) (with-current-buffer command-output-buffer (save-excursion (goto-char write-marker) (let ((inhibit-read-only t) deactivate-mark) (insert-buffer-substring-no-properties session-buffer read-marker end)) (set-marker write-marker (point)))) ;;(xmtn--debug-mark-text-processed session-buffer read-marker end nil) ) (set-marker read-marker end) (decf (xmtn-automate--decoder-state-remaining-chars state) chars-to-read) t) (t (xmtn--assert-nil)))))) ;; Return value matters! ) (defun xmtn--debug-mark-text-processed (buffer start end bold-p) (xmtn--assert-optional (< start end) t) (with-current-buffer buffer (let ((inhibit-read-only t)) (if bold-p (xmtn--assert-for-effect (add-text-properties start end '(face (:strike-through t :weight semi-bold)))) (xmtn--assert-for-effect (add-text-properties start end '(face (:strike-through t)))))))) (defsubst xmtn-automate--process-new-output (session new-string) (let* ((session-buffer (xmtn-automate--session-buffer session)) (state (xmtn-automate--session-decoder-state session)) (read-marker (xmtn-automate--decoder-state-read-marker state)) (write-marker (process-mark (xmtn-automate--session-process session))) (tag 'check-for-more)) (with-current-buffer session-buffer ;; Why oh why doesn't (require 'cl) provide tagbody... (loop for command = (first (xmtn-automate--session-remaining-command-handles session)) do (xmtn--assert-optional (or (eql tag 'exit-loop) (not (xmtn-automate--session-sent-kill-p session)))) (ecase tag (check-for-more (xmtn--assert-optional (<= read-marker write-marker) t) (if (= read-marker write-marker) (setq tag 'exit-loop) (setq tag 'again))) (again (cond ((> (xmtn-automate--decoder-state-remaining-chars state) 0) (if (xmtn-automate--process-new-output--copy session) (setq tag 'again) (setq tag 'check-for-more))) ((and (= (xmtn-automate--decoder-state-remaining-chars state) 0) (xmtn-automate--decoder-state-last-p state)) (xmtn--assert-optional command) (setf (xmtn-automate--command-handle-finished-p command) t) (with-no-warnings ;; discard result (pop (xmtn-automate--session-remaining-command-handles session))) (setq tag 'check-for-more) (when (not (xmtn-automate--command-handle-may-kill-p command)) (when (zerop (decf (xmtn-automate--session-must-not-kill-counter session))) (xmtn--set-process-query-on-exit-flag (xmtn-automate--session-process session) nil) (when (xmtn-automate--session-closed-p session) (xmtn-automate--session-send-process-kill session) (setq tag 'exit-loop)))) (setf (xmtn-automate--decoder-state-last-p state) nil)) ((and (= (xmtn-automate--decoder-state-remaining-chars state) 0) (not (xmtn-automate--decoder-state-last-p state))) (unless command (error "Unexpected output from mtn: %s" new-string)) (save-excursion (goto-char read-marker) (cond ((looking-at "\\([0-9]+\\):\\([012]\\):\\([lm]\\):\\([0-9]+\\):") (let ((command-number (parse-integer (match-string 1))) (error-code (parse-integer (match-string 2))) (last-p (cond ((string= (match-string 3) "l") t) ((string= (match-string 3) "m") nil) (t (xmtn--assert-nil)))) (size (parse-integer (match-string 4)))) (xmtn--assert-optional (typep command-number '(integer 0 *)) t) (xmtn--assert-optional (typep error-code '(member 0 1 2)) t) (xmtn--assert-optional (typep size '(integer 0 *)) t) (xmtn--assert-optional (eql command-number (xmtn-automate--command-handle-mtn-command-number command))) (setf (xmtn-automate--command-handle-error-code command) error-code) (setf (xmtn-automate--decoder-state-remaining-chars state) size) (setf (xmtn-automate--decoder-state-last-p state) last-p) ;;(xmtn--debug-mark-text-processed session-buffer ;; read-marker ;; (match-end 0) ;; t) (set-marker read-marker (match-end 0))) (setq tag 'again)) ;; This is just a simple heuristic, there are many ;; kinds of invalid input that it doesn't detect. ;; FIXME: This can errorneously be triggered by ;; warnings that mtn prints on stderr; but Emacs ;; interleaves stdout and stderr (see (elisp) ;; Output from Processes) with no way to ;; distinguish between them. We'll probably have ;; to spawn mtn inside a shell that redirects ;; stderr to a file. But I don't think that's ;; possible in a portable way... ((looking-at "[^0-9]") (error "Invalid output from mtn: %s" (buffer-substring-no-properties (point) (point-max)))) (t (xmtn--assert-optional command) (setq tag 'exit-loop))))) (t (xmtn--assert-nil)))) (exit-loop (return)))))) nil) (defvar xmtn-automate--*preserve-buffers-for-debugging* nil) (defun xmtn-automate--process-sentinel (process event-string) (let ((status (process-status process)) (session (xmtn-automate--process-session process))) (let ((buffer (xmtn-automate--session-buffer session))) (when (buffer-live-p buffer) (with-current-buffer buffer (let ((inhibit-read-only t) deactivate-mark) (save-excursion ;; This seems to fail in XEmacs when running the test ;; `file-diff'. I don't know why. (xmtn--assert-optional (marker-position (process-mark process)) t) (goto-char (process-mark process)) (insert (format "\n(process exited: %S)\n" (if (eql (aref event-string (1- (length event-string))) ?\n) (subseq event-string 0 (1- (length event-string))) event-string))) (set-marker (process-mark process) (point)))))) (flet ((reclaim-buffer () (unless xmtn-automate--*preserve-buffers-for-debugging* ;; Maybe it's not such a good idea to kill the buffer ;; from here since that will run `kill-buffer-hook', ;; and the functions in there might not be prepared to ;; run inside a sentinel. But let's wait until someone ;; actually encounters this problem. (kill-buffer buffer) ))) (ecase status (exit (xmtn--assert-optional (eql (process-exit-status process) 0) t) (reclaim-buffer)) (signal (if (xmtn-automate--session-sent-kill-p session) (reclaim-buffer) (message "Process %s died due to signal" (process-name process)) (when (not (zerop (xmtn-automate--session-must-not-kill-counter session))) (lwarn 'xmtn ':error "Process %s died due to signal during a critical operation" (process-name process)))))))))) (defun xmtn-automate--process-filter (process input-string) (let ((session (xmtn-automate--process-session process))) (let ((buffer (xmtn-automate--session-buffer session))) (xmtn--assert-optional (eql (process-buffer process) buffer)) (xmtn--assert-optional (buffer-live-p buffer)) (with-current-buffer buffer (let* ((mark (process-mark process)) (move-point-p (= (point) mark))) (save-excursion (goto-char mark) (let ((inhibit-read-only t) deactivate-mark) (insert input-string)) (set-marker mark (point))) (when move-point-p (goto-char mark)))) ;;(with-local-quit ; For debugging. ;; Emacs receives a message "mtn: operation canceled: Interrupt" ;; from mtn after we kill it. Ignore such "input". (unless (xmtn-automate--session-sent-kill-p session) (xmtn-automate--process-new-output session input-string)) ;;) ))) (defun xmtn--map-parsed-certs (xmtn--root xmtn--revision-hash-id xmtn--thunk) (lexical-let ((root xmtn--root) (revision-hash-id xmtn--revision-hash-id) (thunk xmtn--thunk)) (xmtn--with-automate-command-output-basic-io-parser (xmtn--next-stanza root `("certs" ,revision-hash-id)) (loop for xmtn--stanza = (funcall xmtn--next-stanza) while xmtn--stanza do (xmtn-match xmtn--stanza ((("key" (string $xmtn--key)) ("signature" (string $xmtn--signature)) ("name" (string $xmtn--name)) ("value" (string $xmtn--value)) ("trust" (string $xmtn--trust))) (setq xmtn--signature (xmtn-match xmtn--signature ("ok" 'ok) ("bad" 'bad) ("unknown" 'unknown))) (let ((xmtn--trusted (xmtn-match xmtn--trust ("trusted" t) ("untrusted" nil)))) (macrolet ((decodef (var) `(setq ,var (decode-coding-string ,var 'xmtn--monotone-normal-form)))) (decodef xmtn--key) (decodef xmtn--name) ;; I'm not sure this is correct. The documentation ;; mentions a cert_is_binary hook, but it doesn't ;; exist; and even if it did, we would have no way of ;; calling it from here. But, since cert values are ;; always passed on the command line, and command ;; line arguments are converted to utf-8, I suspect ;; certs will also always be in utf-8. (decodef xmtn--value)) (funcall thunk xmtn--key xmtn--signature xmtn--name xmtn--value xmtn--trusted)))))))) (defun xmtn--list-parsed-certs (root revision-hash-id) "Return a list of the contents of each cert attached to REVISION-HASH-ID. Each element of the list is a list; key, signature, name, value, trust." (lexical-let ((accu '())) (xmtn--map-parsed-certs root revision-hash-id (lambda (key signature name value trusted) (push (list key signature name value trusted) accu))) (setq accu (nreverse accu)) accu)) (defun xmtn--heads (root branch) ;; apparently stdio automate doesn't default arguments properly; ;; this fails if branch is not passed to mtn. (xmtn-automate-simple-command-output-lines root (list "heads" (or branch (xmtn--tree-default-branch root))))) (defun xmtn--tree-default-branch (root) (xmtn-automate-simple-command-output-line root `("get_option" "branch"))) (defun xmtn-automate-local-changes (work) "Summary of status for WORK; 'ok if no changes, 'need-commit if changes." (message "checking %s for local changes" work) (let ((default-directory work)) (let ((result (xmtn-automate-simple-command-output-string default-directory (list (list "no-unchanged" "no-ignored") "inventory")))) (if (> (length result) 0) 'need-commit 'ok)))) (provide 'xmtn-automate) ;;; xmtn-automate.el ends here