1;;; mercurial.el --- Emacs support for the Mercurial distributed SCM
3;; Copyright (C) 2005, 2006 Bryan O'Sullivan
5;; Author: Bryan O'Sullivan <bos@serpentine.com>
7;; mercurial.el is free software; you can redistribute it and/or
8;; modify it under the terms of version 2 of the GNU General Public
9;; License as published by the Free Software Foundation.
11;; mercurial.el is distributed in the hope that it will be useful, but
12;; WITHOUT ANY WARRANTY; without even the implied warranty of
13;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14;; General Public License for more details.
16;; You should have received a copy of the GNU General Public License
17;; along with mercurial.el, GNU Emacs, or XEmacs; see the file COPYING
18;; (`C-h C-l'). If not, write to the Free Software Foundation, Inc.,
19;; 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
23;; mercurial.el builds upon Emacs's VC mode to provide flexible
24;; integration with the Mercurial distributed SCM tool.
26;; To get going as quickly as possible, load mercurial.el into Emacs and
27;; type `C-c h h'; this runs hg-help-overview, which prints a helpful
30;; Much of the inspiration for mercurial.el comes from Rajesh
31;; Vaidheeswarran's excellent p4.el, which does an admirably thorough
32;; job for the commercial Perforce SCM product. In fact, substantial
33;; chunks of code are adapted from p4.el.
35;; This code has been developed under XEmacs 21.5, and may not work as
36;; well under GNU Emacs (albeit tested under 21.4). Patches to
37;; enhance the portability of this code, fix bugs, and add features
40;; As of version 22.3, GNU Emacs's VC mode has direct support for
41;; Mercurial, so this package may not prove as useful there.
43;; Please send problem reports and suggestions to bos@serpentine.com.
48(eval-when-compile (require 'cl))
54(defmacro hg-feature-cond (&rest clauses)
55 "Test CLAUSES for feature at compile time.
56Each clause is (FEATURE BODY...)."
58 (let ((feature (car x))
60 (when (or (eq feature t)
62 (return (cons 'progn body))))))
65;;; XEmacs has view-less, while GNU Emacs has view. Joy.
68 (xemacs (require 'view-less))
72;;; Variables accessible through the custom system.
74(defgroup mercurial nil
75 "Mercurial distributed SCM."
79 (or (executable-find "hg")
80 (dolist (path '("~/bin/hg" "/usr/bin/hg" "/usr/local/bin/hg"))
81 (when (file-executable-p path)
83 "The path to Mercurial's hg executable."
84 :type '(file :must-match t)
87(defcustom hg-mode-hook nil
88 "Hook run when a buffer enters hg-mode."
92(defcustom hg-commit-mode-hook nil
93 "Hook run when a buffer is created to prepare a commit."
97(defcustom hg-pre-commit-hook nil
98 "Hook run before a commit is performed.
99If you want to prevent the commit from proceeding, raise an error."
103(defcustom hg-log-mode-hook nil
104 "Hook run after a buffer is filled with log information."
108(defcustom hg-global-prefix "\C-ch"
109 "The global prefix for Mercurial keymap bindings."
113(defcustom hg-commit-allow-empty-message nil
114 "Whether to allow changes to be committed with empty descriptions."
118(defcustom hg-commit-allow-empty-file-list nil
119 "Whether to allow changes to be committed without any modified files."
123(defcustom hg-rev-completion-limit 100
124 "The maximum number of revisions that hg-read-rev will offer to complete.
125This affects memory usage and performance when prompting for revisions
126in a repository with a lot of history."
130(defcustom hg-log-limit 50
131 "The maximum number of revisions that hg-log will display."
135(defcustom hg-update-modeline t
136 "Whether to update the modeline with the status of a file after every save.
137Set this to nil on platforms with poor process management, such as Windows."
141(defcustom hg-incoming-repository "default"
142 "The repository from which changes are pulled from by default.
143This should be a symbolic repository name, since it is used for all
144repository-related commands."
148(defcustom hg-outgoing-repository "default-push"
149 "The repository to which changes are pushed to by default.
150This should be a symbolic repository name, since it is used for all
151repository-related commands."
159 "Is this file managed by Mercurial?")
160(make-variable-buffer-local 'hg-mode)
161(put 'hg-mode 'permanent-local t)
163(defvar hg-status nil)
164(make-variable-buffer-local 'hg-status)
165(put 'hg-status 'permanent-local t)
167(defvar hg-prev-buffer nil)
168(make-variable-buffer-local 'hg-prev-buffer)
169(put 'hg-prev-buffer 'permanent-local t)
172(make-variable-buffer-local 'hg-root)
173(put 'hg-root 'permanent-local t)
175(defvar hg-view-mode nil)
176(make-variable-buffer-local 'hg-view-mode)
177(put 'hg-view-mode 'permanent-local t)
179(defvar hg-view-file-name nil)
180(make-variable-buffer-local 'hg-view-file-name)
181(put 'hg-view-file-name 'permanent-local t)
183(defvar hg-output-buffer-name "*Hg*"
184 "The name to use for Mercurial output buffers.")
186(defvar hg-file-history nil)
187(defvar hg-repo-history nil)
188(defvar hg-rev-history nil)
189(defvar hg-repo-completion-table nil) ; shut up warnings
194(defconst hg-commit-message-start
195 "--- Enter your commit message. Type `C-c C-c' to commit. ---\n")
197(defconst hg-commit-message-end
198 "--- Files in bold will be committed. Click to toggle selection. ---\n")
200(defconst hg-state-alist
212 (let ((map (make-sparse-keymap)))
213 (hg-feature-cond (xemacs (set-keymap-name map 'hg-prefix-map))) ; XEmacs
214 (set-keymap-parent map vc-prefix-map)
215 (define-key map "=" 'hg-diff)
216 (define-key map "c" 'hg-undo)
217 (define-key map "g" 'hg-annotate)
218 (define-key map "i" 'hg-add)
219 (define-key map "l" 'hg-log)
220 (define-key map "n" 'hg-commit-start)
221 ;; (define-key map "r" 'hg-update)
222 (define-key map "u" 'hg-revert-buffer)
223 (define-key map "~" 'hg-version-other-window)
225 "This keymap overrides some default vc-mode bindings.")
228 (let ((map (make-sparse-keymap)))
229 (define-key map "\C-xv" hg-prefix-map)
232(add-minor-mode 'hg-mode 'hg-mode hg-mode-map)
238 (let ((map (make-sparse-keymap)))
239 (define-key map "," 'hg-incoming)
240 (define-key map "." 'hg-outgoing)
241 (define-key map "<" 'hg-pull)
242 (define-key map "=" 'hg-diff-repo)
243 (define-key map ">" 'hg-push)
244 (define-key map "?" 'hg-help-overview)
245 (define-key map "A" 'hg-addremove)
246 (define-key map "U" 'hg-revert)
247 (define-key map "a" 'hg-add)
248 (define-key map "c" 'hg-commit-start)
249 (define-key map "f" 'hg-forget)
250 (define-key map "h" 'hg-help-overview)
251 (define-key map "i" 'hg-init)
252 (define-key map "l" 'hg-log-repo)
253 (define-key map "r" 'hg-root)
254 (define-key map "s" 'hg-status)
255 (define-key map "u" 'hg-update)
258(global-set-key hg-global-prefix hg-global-map)
262(defvar hg-view-mode-map
263 (let ((map (make-sparse-keymap)))
264 (hg-feature-cond (xemacs (set-keymap-name map 'hg-view-mode-map))) ; XEmacs
265 (define-key map (hg-feature-cond (xemacs [button2])
267 'hg-buffer-mouse-clicked)
270(add-minor-mode 'hg-view-mode "" hg-view-mode-map)
273;;; Commit mode keymaps.
275(defvar hg-commit-mode-map
276 (let ((map (make-sparse-keymap)))
277 (define-key map "\C-c\C-c" 'hg-commit-finish)
278 (define-key map "\C-c\C-k" 'hg-commit-kill)
279 (define-key map "\C-xv=" 'hg-diff-repo)
282(defvar hg-commit-mode-file-map
283 (let ((map (make-sparse-keymap)))
284 (define-key map (hg-feature-cond (xemacs [button2])
286 'hg-commit-mouse-clicked)
287 (define-key map " " 'hg-commit-toggle-file)
288 (define-key map "\r" 'hg-commit-toggle-file)
292;;; Convenience functions.
294(defsubst hg-binary ()
297 (error "No `hg' executable found!")))
299(defsubst hg-replace-in-string (str regexp newtext &optional literal)
300 "Replace all matches in STR for REGEXP with NEWTEXT string.
301Return the new string. Optional LITERAL non-nil means do a literal
304This function bridges yet another pointless impedance gap between
305XEmacs and GNU Emacs."
307 (xemacs (replace-in-string str regexp newtext literal))
308 (t (replace-regexp-in-string regexp newtext str nil literal))))
310(defsubst hg-strip (str)
311 "Strip leading and trailing blank lines from a string."
312 (hg-replace-in-string (hg-replace-in-string str "[\r\n][ \t\r\n]*\\'" "")
313 "\\`[ \t\r\n]*[\r\n]" ""))
315(defsubst hg-chomp (str)
316 "Strip trailing newlines from a string."
317 (hg-replace-in-string str "[\r\n]+\\'" ""))
319(defun hg-run-command (command &rest args)
320 "Run the shell command COMMAND, returning (EXIT-CODE . COMMAND-OUTPUT).
321The list ARGS contains a list of arguments to pass to the command."
324 (with-output-to-string
328 (apply 'call-process command nil t nil args))))))
329 (cons exit-code output)))
331(defun hg-run (command &rest args)
332 "Run the Mercurial command COMMAND, returning (EXIT-CODE . COMMAND-OUTPUT)."
333 (apply 'hg-run-command (hg-binary) command args))
335(defun hg-run0 (command &rest args)
336 "Run the Mercurial command COMMAND, returning its output.
337If the command does not exit with a zero status code, raise an error."
338 (let ((res (apply 'hg-run-command (hg-binary) command args)))
339 (if (not (eq (car res) 0))
340 (error "Mercurial command failed %s - exit code %s"
345(defmacro hg-do-across-repo (path &rest body)
346 (let ((root-name (make-symbol "root-"))
347 (buf-name (make-symbol "buf-")))
348 `(let ((,root-name (hg-root ,path)))
350 (dolist (,buf-name (buffer-list))
351 (set-buffer ,buf-name)
352 (when (and hg-status (equal (hg-root buffer-file-name) ,root-name))
355(put 'hg-do-across-repo 'lisp-indent-function 1)
357(defun hg-sync-buffers (path)
358 "Sync buffers visiting PATH with their on-disk copies.
359If PATH is not being visited, but is under the repository root, sync
360all buffers visiting files in the repository."
361 (let ((buf (find-buffer-visiting path)))
363 (with-current-buffer buf
365 (hg-do-across-repo path
368(defun hg-buffer-commands (pnt)
369 "Use the properties of a character to do something sensible."
371 (let ((rev (get-char-property pnt 'rev))
372 (file (get-char-property pnt 'file)))
375 (find-file-other-window file))
377 (hg-diff hg-view-file-name rev rev))
378 ((message "I don't know how to do that yet")))))
380(defsubst hg-event-point (event)
381 "Return the character position of the mouse event EVENT."
382 (hg-feature-cond (xemacs (event-point event))
383 (t (posn-point (event-start event)))))
385(defsubst hg-event-window (event)
386 "Return the window over which mouse event EVENT occurred."
387 (hg-feature-cond (xemacs (event-window event))
388 (t (posn-window (event-start event)))))
390(defun hg-buffer-mouse-clicked (event)
391 "Translate the mouse clicks in a HG log buffer to character events.
392These are then handed off to `hg-buffer-commands'.
394Handle frickin' frackin' gratuitous event-related incompatibilities."
396 (select-window (hg-event-window event))
397 (hg-buffer-commands (hg-event-point event)))
399(defsubst hg-abbrev-file-name (file)
400 "Portable wrapper around abbreviate-file-name."
401 (hg-feature-cond (xemacs (abbreviate-file-name file t))
402 (t (abbreviate-file-name file))))
404(defun hg-read-file-name (&optional prompt default)
405 "Read a file or directory name, or a pattern, to use with a command."
407 (while hg-prev-buffer
408 (set-buffer hg-prev-buffer))
409 (let ((path (or default
411 (expand-file-name default-directory))))
412 (if (or (not path) current-prefix-arg)
414 (eval (list* 'read-file-name
415 (format "File, directory or pattern%s: "
417 (and path (file-name-directory path))
419 (and path (file-name-nondirectory path))
421 (xemacs (cons (quote 'hg-file-history) nil))
425(defun hg-read-number (&optional prompt default)
426 "Read a integer value."
428 (if (or (not default) current-prefix-arg)
430 (eval (list* 'read-string
432 (if default (cons (format "%d" default) nil) nil))))
435(defun hg-read-config ()
436 "Return an alist of (key . value) pairs of Mercurial config data.
437Each key is of the form (section . name)."
439 (dolist (line (split-string (hg-chomp (hg-run0 "debugconfig")) "\n") items)
440 (string-match "^\\([^=]*\\)=\\(.*\\)" line)
441 (let* ((left (substring line (match-beginning 1) (match-end 1)))
442 (right (substring line (match-beginning 2) (match-end 2)))
443 (key (split-string left "\\."))
444 (value (hg-replace-in-string right "\\\\n" "\n" t)))
445 (setq items (cons (cons (cons (car key) (cadr key)) value) items))))))
447(defun hg-config-section (section config)
448 "Return an alist of (name . value) pairs for SECTION of CONFIG."
450 (dolist (item config items)
451 (when (equal (caar item) section)
452 (setq items (cons (cons (cdar item) (cdr item)) items))))))
454(defun hg-string-starts-with (sub str)
455 "Indicate whether string STR starts with the substring or character SUB."
456 (if (not (stringp sub))
457 (and (> (length str) 0) (equal (elt str 0) sub))
458 (let ((sub-len (length sub)))
459 (and (<= sub-len (length str))
460 (string= sub (substring str 0 sub-len))))))
462(defun hg-complete-repo (string predicate all)
463 "Attempt to complete a repository name.
464We complete on either symbolic names from Mercurial's config or real
465directory names from the file system. We do not penalise URLs."
467 (all-completions string hg-repo-completion-table predicate)
468 (try-completion string hg-repo-completion-table predicate))
469 (let* ((str (expand-file-name string))
470 (dir (file-name-directory str))
471 (file (file-name-nondirectory str)))
474 (dolist (name (delete "./" (file-name-all-completions file dir))
476 (let ((path (concat dir name)))
477 (when (file-directory-p path)
478 (setq completions (cons name completions))))))
479 (let ((comp (file-name-completion file dir)))
481 (hg-abbrev-file-name (concat dir comp))))))))
483(defun hg-read-repo-name (&optional prompt initial-contents default)
484 "Read the location of a repository."
486 (while hg-prev-buffer
487 (set-buffer hg-prev-buffer))
488 (let (hg-repo-completion-table)
489 (if current-prefix-arg
491 (dolist (path (hg-config-section "paths" (hg-read-config)))
492 (setq hg-repo-completion-table
493 (cons (cons (car path) t) hg-repo-completion-table))
494 (unless (hg-string-starts-with (hg-feature-cond
495 (xemacs directory-sep-char)
498 (setq hg-repo-completion-table
499 (cons (cons (cdr path) t) hg-repo-completion-table))))
500 (completing-read (format "Repository%s: " (or prompt ""))
509(defun hg-read-rev (&optional prompt default)
510 "Read a revision or tag, offering completions."
512 (while hg-prev-buffer
513 (set-buffer hg-prev-buffer))
514 (let ((rev (or default "tip")))
515 (if current-prefix-arg
516 (let ((revs (split-string
518 (hg-run0 "-q" "log" "-l"
519 (format "%d" hg-rev-completion-limit)))
521 (dolist (line (split-string (hg-chomp (hg-run0 "tags")) "\n"))
522 (setq revs (cons (car (split-string line "\\s-")) revs)))
523 (completing-read (format "Revision%s (%s): "
526 (mapcar (lambda (x) (cons x x)) revs)
534(defun hg-parents-for-mode-line (root)
535 "Format the parents of the working directory for the mode line."
536 (let ((parents (split-string (hg-chomp
537 (hg-run0 "--cwd" root "parents" "--template"
539 (mapconcat 'identity parents "+")))
541(defun hg-buffers-visiting-repo (&optional path)
542 "Return a list of buffers visiting the repository containing PATH."
543 (let ((root-name (hg-root (or path (buffer-file-name))))
546 (dolist (buf (buffer-list) bufs)
548 (let ((name (buffer-file-name)))
549 (when (and hg-status name (equal (hg-root name) root-name))
550 (setq bufs (cons buf bufs))))))))
552(defun hg-update-mode-lines (path)
553 "Update the mode lines of all buffers visiting the same repository as PATH."
554 (let* ((root (hg-root path))
555 (parents (hg-parents-for-mode-line root)))
557 (dolist (info (hg-path-status
562 (substring (buffer-file-name buf) (length root))))
563 (hg-buffers-visiting-repo root))))
564 (let* ((name (car info))
566 (buf (find-buffer-visiting (concat root name))))
569 (hg-mode-line-internal status parents)))))))
574(defun hg-exit-view-mode (buf)
575 "Exit from hg-view-mode.
576We delete the current window if entering hg-view-mode split the
578 (when (and (eq buf (current-buffer))
579 (> (length (window-list)) 1))
581 (when (buffer-live-p buf)
584(defun hg-view-mode (prev-buffer &optional file-name)
585 (goto-char (point-min))
586 (set-buffer-modified-p nil)
588 (hg-feature-cond (xemacs (view-minor-mode prev-buffer 'hg-exit-view-mode))
589 (t (view-mode-enter nil 'hg-exit-view-mode)))
590 (setq hg-view-mode t)
591 (setq truncate-lines t)
593 (setq hg-view-file-name
594 (hg-abbrev-file-name file-name))))
596(defun hg-file-status (file)
597 "Return status of FILE, or nil if FILE does not exist or is unmanaged."
598 (let* ((s (hg-run "status" file))
602 (let ((state (and (>= (length output) 2)
603 (= (aref output 1) ? )
604 (assq (aref output 0) hg-state-alist))))
609(defun hg-path-status (root paths)
610 "Return status of PATHS in repo ROOT as an alist.
611Each entry is a pair (FILE-NAME . STATUS)."
612 (let ((s (apply 'hg-run "--cwd" root "status" "-marduc" paths))
614 (dolist (entry (split-string (hg-chomp (cdr s)) "\n") (nreverse result))
616 (cond ((= (aref entry 1) ? )
617 (setq state (assq (aref entry 0) hg-state-alist)
618 name (substring entry 2)))
619 ((string-match "\\(.*\\): " entry)
620 (setq name (match-string 1 entry))))
621 (setq result (cons (cons name state) result))))))
623(defmacro hg-view-output (args &rest body)
624 "Execute BODY in a clean buffer, then quickly display that buffer.
625If the buffer contains one line, its contents are displayed in the
626minibuffer. Otherwise, the buffer is displayed in view-mode.
627ARGS is of the form (BUFFER-NAME &optional FILE), where BUFFER-NAME is
628the name of the buffer to create, and FILE is the name of the file
630 (let ((prev-buf (make-symbol "prev-buf-"))
631 (v-b-name (car args))
632 (v-m-rest (cdr args)))
633 `(let ((view-buf-name ,v-b-name)
634 (,prev-buf (current-buffer)))
635 (get-buffer-create view-buf-name)
636 (kill-buffer view-buf-name)
637 (get-buffer-create view-buf-name)
638 (set-buffer view-buf-name)
641 (case (count-lines (point-min) (point-max))
643 (kill-buffer view-buf-name)
644 (message "(No output)"))
646 (let ((msg (hg-chomp (buffer-substring (point-min) (point-max)))))
647 (kill-buffer view-buf-name)
650 (pop-to-buffer view-buf-name)
651 (setq hg-prev-buffer ,prev-buf)
652 (hg-view-mode ,prev-buf ,@v-m-rest))))))
654(put 'hg-view-output 'lisp-indent-function 1)
656;;; Context save and restore across revert and other operations.
658(defun hg-position-context (pos)
659 "Return information to help find the given position again."
660 (let* ((end (min (point-max) (+ pos 98))))
662 (buffer-substring (max (point-min) (- pos 2)) end)
665(defun hg-buffer-context ()
666 "Return information to help restore a user's editing context.
667This is useful across reverts and merges, where a context is likely
668to have moved a little, but not really changed."
669 (let ((point-context (hg-position-context (point)))
670 (mark-context (let ((mark (mark-marker)))
672 ;; make sure active mark
674 (marker-position mark)
675 (hg-position-context mark)))))
676 (list point-context mark-context)))
678(defun hg-find-context (ctx)
679 "Attempt to find a context in the given buffer.
680Always returns a valid, hopefully sane, position."
681 (let ((pos (nth 0 ctx))
685 (goto-char (max (point-min) (- pos 15000)))
686 (if (and (not (equal str ""))
687 (search-forward str nil t))
689 (max pos (point-min))))))
691(defun hg-restore-context (ctx)
692 "Attempt to restore the user's editing context."
693 (let ((point-context (nth 0 ctx))
694 (mark-context (nth 1 ctx)))
695 (goto-char (hg-find-context point-context))
697 (set-mark (hg-find-context mark-context)))))
702(defun hg-mode-line-internal (status parents)
703 (setq hg-status status
704 hg-mode (and status (concat " Hg:"
711 (modified . "m"))))))))
713(defun hg-mode-line (&optional force)
714 "Update the modeline with the current status of a file.
715An update occurs if optional argument FORCE is non-nil,
716hg-update-modeline is non-nil, or we have not yet checked the state of
718 (let ((root (hg-root)))
719 (when (and root (or force hg-update-modeline (not hg-mode)))
720 (let ((status (hg-file-status buffer-file-name))
721 (parents (hg-parents-for-mode-line root)))
722 (hg-mode-line-internal status parents)
725(defun hg-mode (&optional toggle)
726 "Minor mode for Mercurial distributed SCM integration.
728The Mercurial mode user interface is based on that of VC mode, so if
729you're already familiar with VC, the same keybindings and functions
732Below is a list of many common SCM tasks. In the list, `G/L\'
733indicates whether a key binding is global (G) to a repository or
734local (L) to a file. Many commands take a prefix argument.
736SCM Task G/L Key Binding Command Name
737-------- --- ----------- ------------
738Help overview (what you are reading) G C-c h h hg-help-overview
740Tell Mercurial to manage a file G C-c h a hg-add
741Commit changes to current file only L C-x v n hg-commit-start
742Undo changes to file since commit L C-x v u hg-revert-buffer
744Diff file vs last checkin L C-x v = hg-diff
746View file change history L C-x v l hg-log
747View annotated file L C-x v a hg-annotate
749Diff repo vs last checkin G C-c h = hg-diff-repo
750View status of files in repo G C-c h s hg-status
751Commit all changes G C-c h c hg-commit-start
753Undo all changes since last commit G C-c h U hg-revert
754View repo change history G C-c h l hg-log-repo
756See changes that can be pulled G C-c h , hg-incoming
757Pull changes G C-c h < hg-pull
758Update working directory after pull G C-c h u hg-update
759See changes that can be pushed G C-c h . hg-outgoing
760Push changes G C-c h > hg-push"
761 (unless vc-make-backup-files
762 (set (make-local-variable 'backup-inhibited) t))
763 (run-hooks 'hg-mode-hook))
765(defun hg-find-file-hook ()
770(add-hook 'find-file-hooks 'hg-find-file-hook)
772(defun hg-after-save-hook ()
774 (let ((old-status hg-status))
776 (if (and (not old-status) hg-status)
779(add-hook 'after-save-hook 'hg-after-save-hook)
782;;; User interface functions.
784(defun hg-help-overview ()
785 "This is an overview of the Mercurial SCM mode for Emacs.
787You can find the source code, license (GPL v2), and credits for this
788code by typing `M-x find-library mercurial RET'."
790 (hg-view-output ("Mercurial Help Overview")
791 (insert (documentation 'hg-help-overview))
793 (insert (documentation 'hg-mode))
796 (delete-region pos (point)))
797 (let ((hg-root-dir (hg-root)))
798 (if (not hg-root-dir)
799 (error "error: %s: directory is not part of a Mercurial repository."
803(defun hg-fix-paths ()
804 "Fix paths reported by some Mercurial commands."
806 (goto-char (point-min))
807 (while (re-search-forward " \\.\\.." nil t)
808 (replace-match " " nil nil))))
811 "Add PATH to the Mercurial repository on the next commit.
812With a prefix argument, prompt for the path to add."
813 (interactive (list (hg-read-file-name " to add")))
814 (let ((buf (current-buffer))
815 (update (equal buffer-file-name path)))
816 (hg-view-output (hg-output-buffer-name)
817 (apply 'call-process (hg-binary) nil t nil (list "add" path))
819 (goto-char (point-min))
822 (unless vc-make-backup-files
823 (set (make-local-variable 'backup-inhibited) t))
824 (with-current-buffer buf
827(defun hg-addremove ()
829 (error "not implemented"))
833 (error "not implemented"))
835(defun hg-commit-toggle-file (pos)
836 "Toggle whether or not the file at POS will be committed."
841 (inhibit-read-only t)
844 (setq bol (+ (point) 4))
845 (setq face (get-text-property bol 'face))
849 (remove-text-properties bol (point) '(face nil))
850 (message "%s will not be committed"
851 (buffer-substring bol (point))))
852 (add-text-properties bol (point) '(face bold))
853 (message "%s will be committed"
854 (buffer-substring bol (point)))))))
856(defun hg-commit-mouse-clicked (event)
857 "Toggle whether or not the file at POS will be committed."
859 (hg-commit-toggle-file (hg-event-point event)))
861(defun hg-commit-kill ()
862 "Kill the commit currently being prepared."
864 (when (or (not (buffer-modified-p)) (y-or-n-p "Really kill this commit? "))
865 (let ((buf hg-prev-buffer))
867 (switch-to-buffer buf))))
869(defun hg-commit-finish ()
870 "Finish preparing a commit, and perform the actual commit.
871The hook hg-pre-commit-hook is run before anything else is done. If
872the commit message is empty and hg-commit-allow-empty-message is nil,
873an error is raised. If the list of files to commit is empty and
874hg-commit-allow-empty-file-list is nil, an error is raised."
876 (let ((root hg-root))
878 (run-hooks 'hg-pre-commit-hook)
879 (goto-char (point-min))
880 (search-forward hg-commit-message-start)
882 (let ((start (point)))
883 (goto-char (point-max))
884 (search-backward hg-commit-message-end)
885 (setq message (hg-strip (buffer-substring start (point)))))
886 (when (and (= (length message) 0)
887 (not hg-commit-allow-empty-message))
888 (error "Cannot proceed - commit message is empty"))
891 (while (< (point) (point-max))
892 (let ((pos (+ (point) 4)))
894 (when (eq (get-text-property pos 'face) 'bold)
896 (setq files (cons (buffer-substring pos (point)) files))))
898 (when (and (= (length files) 0)
899 (not hg-commit-allow-empty-file-list))
900 (error "Cannot proceed - no files to commit"))
901 (setq message (concat message "\n"))
902 (apply 'hg-run0 "--cwd" hg-root "commit" "-m" message files))
903 (let ((buf hg-prev-buffer))
905 (switch-to-buffer buf))
906 (hg-update-mode-lines root))))
908(defun hg-commit-mode ()
909 "Mode for describing a commit of changes to a Mercurial repository.
910This involves two actions: describing the changes with a commit
911message, and choosing the files to commit.
913To describe the commit, simply type some text in the designated area.
915By default, all modified, added and removed files are selected for
916committing. Files that will be committed are displayed in bold face\;
917those that will not are displayed in normal face.
919To toggle whether a file will be committed, move the cursor over a
920particular file and hit space or return. Alternatively, middle click
925\\[hg-commit-finish] proceed with commit
926\\[hg-commit-kill] kill commit
928\\[hg-diff-repo] view diff of pending changes"
930 (use-local-map hg-commit-mode-map)
931 (set-syntax-table text-mode-syntax-table)
932 (setq local-abbrev-table text-mode-abbrev-table
933 major-mode 'hg-commit-mode
934 mode-name "Hg-Commit")
935 (set-buffer-modified-p nil)
936 (setq buffer-undo-list nil)
937 (run-hooks 'text-mode-hook 'hg-commit-mode-hook))
939(defun hg-commit-start ()
940 "Prepare a commit of changes to the repository containing the current file."
942 (while hg-prev-buffer
943 (set-buffer hg-prev-buffer))
944 (let ((root (hg-root))
945 (prev-buffer (current-buffer))
948 (error "Cannot commit outside a repository!"))
949 (hg-sync-buffers root)
950 (setq modified-files (hg-chomp (hg-run0 "--cwd" root "status" "-arm")))
951 (when (and (= (length modified-files) 0)
952 (not hg-commit-allow-empty-file-list))
953 (error "No pending changes to commit"))
954 (let* ((buf-name (format "*Mercurial: Commit %s*" root)))
955 (pop-to-buffer (get-buffer-create buf-name))
956 (when (= (point-min) (point-max))
957 (set (make-local-variable 'hg-root) root)
958 (setq hg-prev-buffer prev-buffer)
961 (insert hg-commit-message-end)
962 (add-text-properties bol (point) '(face bold-italic)))
963 (let ((file-area (point)))
964 (insert modified-files)
965 (goto-char file-area)
966 (while (< (point) (point-max))
971 (add-text-properties (+ bol 4) (point)
972 '(face bold mouse-face highlight)))
974 (goto-char file-area)
975 (add-text-properties (point) (point-max)
976 `(keymap ,hg-commit-mode-file-map))
977 (goto-char (point-min))
978 (insert hg-commit-message-start)
979 (add-text-properties (point-min) (point) '(face bold-italic))
983 (goto-char (point-max))
984 (search-backward hg-commit-message-end)
985 (add-text-properties (match-beginning 0) (point-max)
987 (goto-char (point-min))
988 (search-forward hg-commit-message-start)
989 (add-text-properties (match-beginning 0) (match-end 0)
994(defun hg-diff (path &optional rev1 rev2)
995 "Show the differences between REV1 and REV2 of PATH.
996When called interactively, the default behaviour is to treat REV1 as
997the \"parent\" revision, REV2 as the current edited version of the file, and
998PATH as the file edited in the current buffer.
999With a prefix argument, prompt for all of these."
1000 (interactive (list (hg-read-file-name " to diff")
1001 (let ((rev1 (hg-read-rev " to start with" 'parent)))
1002 (and (not (eq rev1 'parent)) rev1))
1003 (let ((rev2 (hg-read-rev " to end with" 'working-dir)))
1004 (and (not (eq rev2 'working-dir)) rev2))))
1005 (hg-sync-buffers path)
1006 (let ((a-path (hg-abbrev-file-name path))
1007 ;; none revision is specified explicitly
1008 (none (and (not rev1) (not rev2)))
1009 ;; only one revision is specified explicitly
1010 (one (or (and (or (equal rev1 rev2) (not rev2)) rev1)
1011 (and (not rev1) rev2)))
1013 (hg-view-output ((cond
1015 (format "Mercurial: Diff against parent of %s" a-path))
1017 (format "Mercurial: Diff of rev %s of %s" one a-path))
1019 (format "Mercurial: Diff from rev %s to %s of %s"
1020 rev1 rev2 a-path))))
1023 (call-process (hg-binary) nil t nil "diff" path))
1025 (call-process (hg-binary) nil t nil "diff" "-r" one path))
1027 (call-process (hg-binary) nil t nil "diff" "-r" rev1 "-r" rev2 path)))
1029 (setq diff (not (= (point-min) (point-max))))
1030 (font-lock-fontify-buffer)
1031 (cd (hg-root path)))
1034(defun hg-diff-repo (path &optional rev1 rev2)
1035 "Show the differences between REV1 and REV2 of repository containing PATH.
1036When called interactively, the default behaviour is to treat REV1 as
1037the \"parent\" revision, REV2 as the current edited version of the file, and
1038PATH as the `hg-root' of the current buffer.
1039With a prefix argument, prompt for all of these."
1040 (interactive (list (hg-read-file-name " to diff")
1041 (let ((rev1 (hg-read-rev " to start with" 'parent)))
1042 (and (not (eq rev1 'parent)) rev1))
1043 (let ((rev2 (hg-read-rev " to end with" 'working-dir)))
1044 (and (not (eq rev2 'working-dir)) rev2))))
1045 (hg-diff (hg-root path) rev1 rev2))
1047(defun hg-forget (path)
1048 "Lose track of PATH, which has been added, but not yet committed.
1049This will prevent the file from being incorporated into the Mercurial
1050repository on the next commit.
1051With a prefix argument, prompt for the path to forget."
1052 (interactive (list (hg-read-file-name " to forget")))
1053 (let ((buf (current-buffer))
1054 (update (equal buffer-file-name path)))
1055 (hg-view-output (hg-output-buffer-name)
1056 (apply 'call-process (hg-binary) nil t nil (list "forget" path))
1057 ;; "hg forget" shows pathes relative NOT TO ROOT BUT TO REPOSITORY
1059 (goto-char (point-min))
1060 (cd (hg-root path)))
1062 (with-current-buffer buf
1063 (when (local-variable-p 'backup-inhibited)
1064 (kill-local-variable 'backup-inhibited))
1067(defun hg-incoming (&optional repo)
1068 "Display changesets present in REPO that are not present locally."
1069 (interactive (list (hg-read-repo-name " where changes would come from")))
1070 (hg-view-output ((format "Mercurial: Incoming from %s to %s"
1071 (hg-abbrev-file-name (hg-root))
1072 (hg-abbrev-file-name
1073 (or repo hg-incoming-repository))))
1074 (call-process (hg-binary) nil t nil "incoming"
1075 (or repo hg-incoming-repository))
1081 (error "not implemented"))
1083(defun hg-log-mode ()
1084 "Mode for viewing a Mercurial change log."
1085 (goto-char (point-min))
1086 (when (looking-at "^searching for changes.*$")
1087 (delete-region (match-beginning 0) (match-end 0)))
1088 (run-hooks 'hg-log-mode-hook))
1090(defun hg-log (path &optional rev1 rev2 log-limit)
1091 "Display the revision history of PATH.
1092History is displayed between REV1 and REV2.
1093Number of displayed changesets is limited to LOG-LIMIT.
1094REV1 defaults to the tip, while REV2 defaults to 0.
1095LOG-LIMIT defaults to `hg-log-limit'.
1096With a prefix argument, prompt for each parameter."
1097 (interactive (list (hg-read-file-name " to log")
1098 (hg-read-rev " to start with"
1100 (hg-read-rev " to end with"
1102 (hg-read-number "Output limited to: "
1104 (let ((a-path (hg-abbrev-file-name path))
1105 (r1 (or rev1 "tip"))
1107 (limit (format "%d" (or log-limit hg-log-limit))))
1108 (hg-view-output ((if (equal r1 r2)
1109 (format "Mercurial: Log of rev %s of %s" rev1 a-path)
1111 "Mercurial: at most %s log(s) from rev %s to %s of %s"
1112 limit r1 r2 a-path)))
1113 (eval (list* 'call-process (hg-binary) nil t nil
1115 "-r" (format "%s:%s" r1 r2)
1117 (if (> (length path) (length (hg-root path)))
1121 (cd (hg-root path)))))
1123(defun hg-log-repo (path &optional rev1 rev2 log-limit)
1124 "Display the revision history of the repository containing PATH.
1125History is displayed between REV1 and REV2.
1126Number of displayed changesets is limited to LOG-LIMIT,
1127REV1 defaults to the tip, while REV2 defaults to 0.
1128LOG-LIMIT defaults to `hg-log-limit'.
1129With a prefix argument, prompt for each parameter."
1130 (interactive (list (hg-read-file-name " to log")
1131 (hg-read-rev " to start with"
1133 (hg-read-rev " to end with"
1135 (hg-read-number "Output limited to: "
1137 (hg-log (hg-root path) rev1 rev2 log-limit))
1139(defun hg-outgoing (&optional repo)
1140 "Display changesets present locally that are not present in REPO."
1141 (interactive (list (hg-read-repo-name " where changes would go to" nil
1142 hg-outgoing-repository)))
1143 (hg-view-output ((format "Mercurial: Outgoing from %s to %s"
1144 (hg-abbrev-file-name (hg-root))
1145 (hg-abbrev-file-name
1146 (or repo hg-outgoing-repository))))
1147 (call-process (hg-binary) nil t nil "outgoing"
1148 (or repo hg-outgoing-repository))
1152(defun hg-pull (&optional repo)
1153 "Pull changes from repository REPO.
1154This does not update the working directory."
1155 (interactive (list (hg-read-repo-name " to pull from")))
1156 (hg-view-output ((format "Mercurial: Pull to %s from %s"
1157 (hg-abbrev-file-name (hg-root))
1158 (hg-abbrev-file-name
1159 (or repo hg-incoming-repository))))
1160 (call-process (hg-binary) nil t nil "pull"
1161 (or repo hg-incoming-repository))
1164(defun hg-push (&optional repo)
1165 "Push changes to repository REPO."
1166 (interactive (list (hg-read-repo-name " to push to")))
1167 (hg-view-output ((format "Mercurial: Push from %s to %s"
1168 (hg-abbrev-file-name (hg-root))
1169 (hg-abbrev-file-name
1170 (or repo hg-outgoing-repository))))
1171 (call-process (hg-binary) nil t nil "push"
1172 (or repo hg-outgoing-repository))
1175(defun hg-revert-buffer-internal ()
1176 (let ((ctx (hg-buffer-context)))
1177 (message "Reverting %s..." buffer-file-name)
1178 (hg-run0 "revert" buffer-file-name)
1179 (revert-buffer t t t)
1180 (hg-restore-context ctx)
1182 (message "Reverting %s...done" buffer-file-name)))
1184(defun hg-revert-buffer ()
1185 "Revert current buffer's file back to the latest committed version.
1186If the file has not changed, nothing happens. Otherwise, this
1187displays a diff and asks for confirmation before reverting."
1189 (let ((vc-suppress-confirm nil)
1190 (obuf (current-buffer))
1194 (setq diff (hg-diff buffer-file-name))
1196 (unless (yes-or-no-p "Discard changes? ")
1197 (error "Revert cancelled")))
1199 (let ((buf (current-buffer)))
1200 (delete-window (selected-window))
1201 (kill-buffer buf))))
1204 (hg-revert-buffer-internal))))
1206(defun hg-root (&optional path)
1207 "Return the root of the repository that contains the given path.
1208If the path is outside a repository, return nil.
1209When called interactively, the root is printed. A prefix argument
1210prompts for a path to check."
1211 (interactive (list (hg-read-file-name)))
1212 (if (or path (not hg-root))
1213 (let ((root (do ((prev nil dir)
1214 (dir (file-name-directory
1218 (expand-file-name default-directory)))
1219 (file-name-directory (directory-file-name dir))))
1221 (when (file-directory-p (concat dir ".hg"))
1223 (when (interactive-p)
1225 (message "The root of this repository is `%s'." root)
1226 (message "The path `%s' is not in a Mercurial repository."
1227 (hg-abbrev-file-name path))))
1231(defun hg-cwd (&optional path)
1232 "Return the current directory of PATH within the repository."
1233 (do ((stack nil (cons (file-name-nondirectory
1234 (directory-file-name dir))
1237 (dir (file-name-directory (or path buffer-file-name
1238 (expand-file-name default-directory)))
1239 (file-name-directory (directory-file-name dir))))
1241 (when (file-directory-p (concat dir ".hg"))
1242 (let ((cwd (mapconcat 'identity stack "/")))
1243 (unless (equal cwd "")
1244 (return (file-name-as-directory cwd)))))))
1246(defun hg-status (path)
1247 "Print revision control status of a file or directory.
1248With prefix argument, prompt for the path to give status for.
1249Names are displayed relative to the repository root."
1250 (interactive (list (hg-read-file-name " for status" (hg-root))))
1251 (let ((root (hg-root)))
1252 (hg-view-output ((format "Mercurial: Status of %s in %s"
1253 (let ((name (substring (expand-file-name path)
1255 (if (> (length name) 0)
1258 (hg-abbrev-file-name root)))
1259 (apply 'call-process (hg-binary) nil t nil
1260 (list "--cwd" root "status" path))
1261 (cd (hg-root path)))))
1265 (error "not implemented"))
1269 (error "not implemented"))
1271(defun hg-version-other-window (rev)
1272 "Visit version REV of the current file in another window.
1273If the current file is named `F', the version is named `F.~REV~'.
1274If `F.~REV~' already exists, use it instead of checking it out again."
1275 (interactive "sVersion to visit (default is workfile version): ")
1276 (let* ((file buffer-file-name)
1277 (version (if (string-equal rev "")
1280 (automatic-backup (vc-version-backup-file-name file version))
1281 (manual-backup (vc-version-backup-file-name file version 'manual)))
1282 (unless (file-exists-p manual-backup)
1283 (if (file-exists-p automatic-backup)
1284 (rename-file automatic-backup manual-backup nil)
1285 (hg-run0 "-q" "cat" "-r" version "-o" manual-backup file)))
1286 (find-file-other-window manual-backup)))
1293;;; prompt-to-byte-compile: nil