]> code.delx.au - gnu-emacs/blob - lisp/vc-hg.el
*** empty log message ***
[gnu-emacs] / lisp / vc-hg.el
1 ;;; vc-hg.el --- VC backend for the mercurial version control system
2
3 ;; Copyright (C) 2006, 2007, 2008 Free Software Foundation, Inc.
4
5 ;; Author: Ivan Kanis
6 ;; Keywords: tools
7
8 ;; This file is part of GNU Emacs.
9
10 ;; GNU Emacs is free software: you can redistribute it and/or modify
11 ;; it under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation, either version 3 of the License, or
13 ;; (at your option) any later version.
14
15 ;; GNU Emacs is distributed in the hope that it will be useful,
16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 ;; GNU General Public License for more details.
19
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
22
23 ;;; Commentary:
24
25 ;; This is a mercurial version control backend
26
27 ;;; Thanks:
28
29 ;;; Bugs:
30
31 ;;; Installation:
32
33 ;;; Todo:
34
35 ;; Implement the rest of the vc interface. See the comment at the
36 ;; beginning of vc.el. The current status is:
37
38 ;; FUNCTION NAME STATUS
39 ;; BACKEND PROPERTIES
40 ;; * revision-granularity OK
41 ;; STATE-QUERYING FUNCTIONS
42 ;; * registered (file) OK
43 ;; * state (file) OK
44 ;; - state-heuristic (file) ?? PROBABLY NOT NEEDED
45 ;; * working-revision (file) OK
46 ;; - latest-on-branch-p (file) ??
47 ;; * checkout-model (files) OK
48 ;; - workfile-unchanged-p (file) OK
49 ;; - mode-line-string (file) NOT NEEDED
50 ;; - prettify-state-info (file) OK
51 ;; STATE-CHANGING FUNCTIONS
52 ;; * register (files &optional rev comment) OK
53 ;; * create-repo () OK
54 ;; - init-revision () NOT NEEDED
55 ;; - responsible-p (file) OK
56 ;; - could-register (file) OK
57 ;; - receive-file (file rev) ?? PROBABLY NOT NEEDED
58 ;; - unregister (file) COMMENTED OUT, MAY BE INCORRECT
59 ;; * checkin (files rev comment) OK
60 ;; * find-revision (file rev buffer) OK
61 ;; * checkout (file &optional editable rev) OK
62 ;; * revert (file &optional contents-done) OK
63 ;; - rollback (files) ?? PROBABLY NOT NEEDED
64 ;; - merge (file rev1 rev2) NEEDED
65 ;; - merge-news (file) NEEDED
66 ;; - steal-lock (file &optional revision) NOT NEEDED
67 ;; HISTORY FUNCTIONS
68 ;; * print-log (files &optional buffer) OK
69 ;; - log-view-mode () OK
70 ;; - show-log-entry (revision) NOT NEEDED, DEFAULT IS GOOD
71 ;; - wash-log (file) ??
72 ;; - comment-history (file) NOT NEEDED
73 ;; - update-changelog (files) NOT NEEDED
74 ;; * diff (files &optional rev1 rev2 buffer) OK
75 ;; - revision-completion-table (files) OK?
76 ;; - annotate-command (file buf &optional rev) OK
77 ;; - annotate-time () OK
78 ;; - annotate-current-time () ?? NOT NEEDED
79 ;; - annotate-extract-revision-at-line () OK
80 ;; SNAPSHOT SYSTEM
81 ;; - create-snapshot (dir name branchp) NEEDED (probably branch?)
82 ;; - assign-name (file name) NOT NEEDED
83 ;; - retrieve-snapshot (dir name update) ?? NEEDED??
84 ;; MISCELLANEOUS
85 ;; - make-version-backups-p (file) ??
86 ;; - repository-hostname (dirname) ??
87 ;; - previous-revision (file rev) OK
88 ;; - next-revision (file rev) OK
89 ;; - check-headers () ??
90 ;; - clear-headers () ??
91 ;; - delete-file (file) TEST IT
92 ;; - rename-file (old new) OK
93 ;; - find-file-hook () PROBABLY NOT NEEDED
94 ;; - find-file-not-found-hook () PROBABLY NOT NEEDED
95
96 ;; Implement Stefan Monnier's advice:
97 ;; vc-hg-registered and vc-hg-state
98 ;; Both of those functions should be super extra careful to fail gracefully in
99 ;; unexpected circumstances. The reason this is important is that any error
100 ;; there will prevent the user from even looking at the file :-(
101 ;; Ideally, just like in vc-arch and vc-cvs, checking that the file is under
102 ;; mercurial's control and extracting the current revision should be done
103 ;; without even using `hg' (this way even if you don't have `hg' installed,
104 ;; Emacs is able to tell you this file is under mercurial's control).
105
106 ;;; History:
107 ;;
108
109 ;;; Code:
110
111 (eval-when-compile
112 (require 'cl)
113 (require 'vc))
114
115 ;;; Customization options
116
117 (defcustom vc-hg-global-switches nil
118 "*Global switches to pass to any Hg command."
119 :type '(choice (const :tag "None" nil)
120 (string :tag "Argument String")
121 (repeat :tag "Argument List"
122 :value ("")
123 string))
124 :version "22.2"
125 :group 'vc)
126
127 \f
128 ;;; Properties of the backend
129
130 (defun vc-hg-revision-granularity () 'repository)
131 (defun vc-hg-checkout-model (files) 'implicit)
132
133 ;;; State querying functions
134
135 ;;;###autoload (defun vc-hg-registered (file)
136 ;;;###autoload "Return non-nil if FILE is registered with hg."
137 ;;;###autoload (if (vc-find-root file ".hg") ; short cut
138 ;;;###autoload (progn
139 ;;;###autoload (load "vc-hg")
140 ;;;###autoload (vc-hg-registered file))))
141
142 ;; Modelled after the similar function in vc-bzr.el
143 (defun vc-hg-registered (file)
144 "Return non-nil if FILE is registered with hg."
145 (when (vc-hg-root file) ; short cut
146 (let ((state (vc-hg-state file))) ; expensive
147 (vc-file-setprop file 'vc-state state)
148 (and state (not (memq state '(ignored unregistered)))))))
149
150 (defun vc-hg-state (file)
151 "Hg-specific version of `vc-state'."
152 (let*
153 ((status nil)
154 (out
155 (with-output-to-string
156 (with-current-buffer
157 standard-output
158 (setq status
159 (condition-case nil
160 ;; Ignore all errors.
161 (call-process
162 "hg" nil t nil "--cwd" (file-name-directory file)
163 "status" "-A" (file-name-nondirectory file))
164 ;; Some problem happened. E.g. We can't find an `hg'
165 ;; executable.
166 (error nil)))))))
167 (when (eq 0 status)
168 (when (null (string-match ".*: No such file or directory$" out))
169 (let ((state (aref out 0)))
170 (cond
171 ((eq state ?=) 'up-to-date)
172 ((eq state ?A) 'added)
173 ((eq state ?M) 'edited)
174 ((eq state ?I) 'ignored)
175 ((eq state ?R) 'removed)
176 ((eq state ?!) 'missing)
177 ((eq state ??) 'unregistered)
178 ((eq state ?C) 'up-to-date) ;; Older mercurials use this
179 (t 'up-to-date)))))))
180
181 (defun vc-hg-working-revision (file)
182 "Hg-specific version of `vc-working-revision'."
183 (let*
184 ((status nil)
185 (out
186 (with-output-to-string
187 (with-current-buffer
188 standard-output
189 (setq status
190 (condition-case nil
191 ;; Ignore all errors.
192 (call-process
193 "hg" nil t nil "--cwd" (file-name-directory file)
194 "log" "-l1" (file-name-nondirectory file))
195 ;; Some problem happened. E.g. We can't find an `hg'
196 ;; executable.
197 (error nil)))))))
198 (when (eq 0 status)
199 (if (string-match "changeset: *\\([0-9]*\\)" out)
200 (match-string 1 out)
201 "0"))))
202
203 ;;; History functions
204
205 (defun vc-hg-print-log (files &optional buffer)
206 "Get change log associated with FILES."
207 ;; `log-view-mode' needs to have the file names in order to function
208 ;; correctly. "hg log" does not print it, so we insert it here by
209 ;; hand.
210
211 ;; `vc-do-command' creates the buffer, but we need it before running
212 ;; the command.
213 (vc-setup-buffer buffer)
214 ;; If the buffer exists from a previous invocation it might be
215 ;; read-only.
216 (let ((inhibit-read-only t))
217 ;; We need to loop and call "hg log" on each file separately.
218 ;; "hg log" with multiple file arguments mashes all the logs
219 ;; together. Ironically enough, this puts us back near CVS
220 ;; which can't generate proper fileset logs either.
221 (dolist (file files)
222 (with-current-buffer
223 buffer
224 (insert "Working file: " file "\n")) ;; Like RCS/CVS.
225 (vc-hg-command buffer 0 file "log"))))
226
227 (defvar log-view-message-re)
228 (defvar log-view-file-re)
229 (defvar log-view-font-lock-keywords)
230
231 (define-derived-mode vc-hg-log-view-mode log-view-mode "Hg-Log-View"
232 (require 'add-log) ;; we need the add-log faces
233 (set (make-local-variable 'log-view-file-re) "^Working file:[ \t]+\\(.+\\)")
234 (set (make-local-variable 'log-view-message-re)
235 "^changeset:[ \t]*\\([0-9]+\\):\\(.+\\)")
236 (set (make-local-variable 'log-view-font-lock-keywords)
237 (append
238 log-view-font-lock-keywords
239 '(
240 ;; Handle the case:
241 ;; user: FirstName LastName <foo@bar>
242 ("^user:[ \t]+\\([^<(]+?\\)[ \t]*[(<]\\([A-Za-z0-9_.+-]+@[A-Za-z0-9_.-]+\\)[>)]"
243 (1 'change-log-name)
244 (2 'change-log-email))
245 ;; Handle the cases:
246 ;; user: foo@bar
247 ;; and
248 ;; user: foo
249 ("^user:[ \t]+\\([A-Za-z0-9_.+-]+\\(?:@[A-Za-z0-9_.-]+\\)?\\)"
250 (1 'change-log-email))
251 ("^date: \\(.+\\)" (1 'change-log-date))
252 ("^summary:[ \t]+\\(.+\\)" (1 'log-view-message))))))
253
254 (defun vc-hg-diff (files &optional oldvers newvers buffer)
255 "Get a difference report using hg between two revisions of FILES."
256 (let ((working (vc-working-revision (car files))))
257 (if (and (equal oldvers working) (not newvers))
258 (setq oldvers nil))
259 (if (and (not oldvers) newvers)
260 (setq oldvers working))
261 (apply #'vc-hg-command (or buffer "*vc-diff*") nil
262 (mapcar (lambda (file) (file-name-nondirectory file)) files)
263 "--cwd" (file-name-directory (car files))
264 "diff"
265 (append
266 (if oldvers
267 (if newvers
268 (list "-r" oldvers "-r" newvers)
269 (list "-r" oldvers)))))))
270
271 (defun vc-hg-revision-table (files)
272 (let ((default-directory (file-name-directory (car files))))
273 (with-temp-buffer
274 (vc-hg-command t nil files "log" "--template" "{rev} ")
275 (split-string
276 (buffer-substring-no-properties (point-min) (point-max))))))
277
278 ;; Modelled after the similar function in vc-cvs.el
279 (defun vc-hg-revision-completion-table (files)
280 (lexical-let ((files files)
281 table)
282 (setq table (lazy-completion-table
283 table (lambda () (vc-hg-revision-table files))))
284 table))
285
286 (defun vc-hg-annotate-command (file buffer &optional revision)
287 "Execute \"hg annotate\" on FILE, inserting the contents in BUFFER.
288 Optional arg REVISION is a revision to annotate from."
289 (vc-hg-command buffer 0 file "annotate" "-d" "-n" (if revision (concat "-r" revision)))
290 (with-current-buffer buffer
291 (goto-char (point-min))
292 (re-search-forward "^[0-9]")
293 (delete-region (point-min) (1- (point)))))
294
295
296 ;; The format for one line output by "hg annotate -d -n" looks like this:
297 ;;215 Wed Jun 20 21:22:58 2007 -0700: CONTENTS
298 ;; i.e: VERSION_NUMBER DATE: CONTENTS
299 (defconst vc-hg-annotate-re "^[ \t]*\\([0-9]+\\) \\(.\\{30\\}\\): ")
300
301 (defun vc-hg-annotate-time ()
302 (when (looking-at vc-hg-annotate-re)
303 (goto-char (match-end 0))
304 (vc-annotate-convert-time
305 (date-to-time (match-string-no-properties 2)))))
306
307 (defun vc-hg-annotate-extract-revision-at-line ()
308 (save-excursion
309 (beginning-of-line)
310 (if (looking-at vc-hg-annotate-re) (match-string-no-properties 1))))
311
312 (defun vc-hg-previous-revision (file rev)
313 (let ((newrev (1- (string-to-number rev))))
314 (when (>= newrev 0)
315 (number-to-string newrev))))
316
317 (defun vc-hg-next-revision (file rev)
318 (let ((newrev (1+ (string-to-number rev)))
319 (tip-revision
320 (with-temp-buffer
321 (vc-hg-command t 0 nil "tip")
322 (goto-char (point-min))
323 (re-search-forward "^changeset:[ \t]*\\([0-9]+\\):")
324 (string-to-number (match-string-no-properties 1)))))
325 ;; We don't want to exceed the maximum possible revision number, ie
326 ;; the tip revision.
327 (when (<= newrev tip-revision)
328 (number-to-string newrev))))
329
330 ;; Modelled after the similar function in vc-bzr.el
331 (defun vc-hg-delete-file (file)
332 "Delete FILE and delete it in the hg repository."
333 (condition-case ()
334 (delete-file file)
335 (file-error nil))
336 (vc-hg-command nil 0 file "remove" "--after" "--force"))
337
338 ;; Modelled after the similar function in vc-bzr.el
339 (defun vc-hg-rename-file (old new)
340 "Rename file from OLD to NEW using `hg mv'."
341 (vc-hg-command nil 0 new "mv" old))
342
343 (defun vc-hg-register (files &optional rev comment)
344 "Register FILES under hg.
345 REV is ignored.
346 COMMENT is ignored."
347 (vc-hg-command nil 0 files "add"))
348
349 (defun vc-hg-create-repo ()
350 "Create a new Mercurial repository."
351 (vc-hg-command nil 0 nil "init"))
352
353 (defalias 'vc-hg-responsible-p 'vc-hg-root)
354
355 ;; Modelled after the similar function in vc-bzr.el
356 (defun vc-hg-could-register (file)
357 "Return non-nil if FILE could be registered under hg."
358 (and (vc-hg-responsible-p file) ; shortcut
359 (condition-case ()
360 (with-temp-buffer
361 (vc-hg-command t nil file "add" "--dry-run"))
362 ;; The command succeeds with no output if file is
363 ;; registered.
364 (error))))
365
366 ;; XXX This would remove the file. Is that correct?
367 ;; (defun vc-hg-unregister (file)
368 ;; "Unregister FILE from hg."
369 ;; (vc-hg-command nil nil file "remove"))
370
371 (defun vc-hg-checkin (files rev comment)
372 "Hg-specific version of `vc-backend-checkin'.
373 REV is ignored."
374 (vc-hg-command nil 0 files "commit" "-m" comment))
375
376 (defun vc-hg-find-revision (file rev buffer)
377 (let ((coding-system-for-read 'binary)
378 (coding-system-for-write 'binary))
379 (if rev
380 (vc-hg-command buffer 0 file "cat" "-r" rev)
381 (vc-hg-command buffer 0 file "cat"))))
382
383 ;; Modelled after the similar function in vc-bzr.el
384 (defun vc-hg-checkout (file &optional editable rev)
385 "Retrieve a revision of FILE.
386 EDITABLE is ignored.
387 REV is the revision to check out into WORKFILE."
388 (let ((coding-system-for-read 'binary)
389 (coding-system-for-write 'binary))
390 (with-current-buffer (or (get-file-buffer file) (current-buffer))
391 (if rev
392 (vc-hg-command t 0 file "cat" "-r" rev)
393 (vc-hg-command t 0 file "cat")))))
394
395 ;; Modelled after the similar function in vc-bzr.el
396 (defun vc-hg-workfile-unchanged-p (file)
397 (eq 'up-to-date (vc-hg-state file)))
398
399 ;; Modelled after the similar function in vc-bzr.el
400 (defun vc-hg-revert (file &optional contents-done)
401 (unless contents-done
402 (with-temp-buffer (vc-hg-command t 0 file "revert"))))
403
404 ;;; Hg specific functionality.
405
406 (defvar vc-hg-extra-menu-map
407 (let ((map (make-sparse-keymap)))
408 (define-key map [incoming] '(menu-item "Show incoming" vc-hg-incoming))
409 (define-key map [outgoing] '(menu-item "Show outgoing" vc-hg-outgoing))
410 map))
411
412 (defun vc-hg-extra-menu () vc-hg-extra-menu-map)
413
414 (defun vc-hg-extra-status-menu () vc-hg-extra-menu-map)
415
416 (define-derived-mode vc-hg-outgoing-mode vc-hg-log-view-mode "Hg-Outgoing")
417
418 (define-derived-mode vc-hg-incoming-mode vc-hg-log-view-mode "Hg-Incoming")
419
420 (defstruct (vc-hg-extra-fileinfo
421 (:copier nil)
422 (:constructor vc-hg-create-extra-fileinfo (rename-state extra-name))
423 (:conc-name vc-hg-extra-fileinfo->))
424 rename-state ;; rename or copy state
425 extra-name) ;; original name for copies and rename targets, new name for
426
427 (defun vc-hg-status-printer (info)
428 "Pretty-printer for the vc-dir-fileinfo structure."
429 (let ((extra (vc-dir-fileinfo->extra info)))
430 (vc-default-status-printer 'Hg info)
431 (when extra
432 (insert (propertize
433 (format " (%s %s)"
434 (case (vc-hg-extra-fileinfo->rename-state extra)
435 ('copied "copied from")
436 ('renamed-from "renamed from")
437 ('renamed-to "renamed to"))
438 (vc-hg-extra-fileinfo->extra-name extra))
439 'face 'font-lock-comment-face)))))
440
441 (defun vc-hg-after-dir-status (update-function)
442 (let ((status-char nil)
443 (file nil)
444 (translation '((?= . up-to-date)
445 (?C . up-to-date)
446 (?A . added)
447 (?R . removed)
448 (?M . edited)
449 (?I . ignored)
450 (?! . missing)
451 (? . copy-rename-line)
452 (?? . unregistered)))
453 (translated nil)
454 (result nil)
455 (last-added nil)
456 (last-line-copy nil))
457 (goto-char (point-min))
458 (while (not (eobp))
459 (setq translated (cdr (assoc (char-after) translation)))
460 (setq file
461 (buffer-substring-no-properties (+ (point) 2)
462 (line-end-position)))
463 (cond ((not translated)
464 (setq last-line-copy nil))
465 ((eq translated 'up-to-date)
466 (setq last-line-copy nil))
467 ((eq translated 'copy-rename-line)
468 ;; For copied files the output looks like this:
469 ;; A COPIED_FILE_NAME
470 ;; ORIGINAL_FILE_NAME
471 (setf (nth 2 last-added)
472 (vc-hg-create-extra-fileinfo 'copied file))
473 (setq last-line-copy t))
474 ((and last-line-copy (eq translated 'removed))
475 ;; For renamed files the output looks like this:
476 ;; A NEW_FILE_NAME
477 ;; ORIGINAL_FILE_NAME
478 ;; R ORIGINAL_FILE_NAME
479 ;; We need to adjust the previous entry to not think it is a copy.
480 (setf (vc-hg-extra-fileinfo->rename-state (nth 2 last-added))
481 'renamed-from)
482 (push (list file translated
483 (vc-hg-create-extra-fileinfo
484 'renamed-to (nth 0 last-added))) result)
485 (setq last-line-copy nil))
486 (t
487 (setq last-added (list file translated nil))
488 (push last-added result)
489 (setq last-line-copy nil)))
490 (forward-line))
491 (funcall update-function result)))
492
493 (defun vc-hg-dir-status (dir update-function)
494 (vc-hg-command (current-buffer) 'async dir "status" "-C")
495 (vc-exec-after
496 `(vc-hg-after-dir-status (quote ,update-function))))
497
498 ;; XXX this adds another top level menu, instead figure out how to
499 ;; replace the Log-View menu.
500 (easy-menu-define log-view-mode-menu vc-hg-outgoing-mode-map
501 "Hg-outgoing Display Menu"
502 `("Hg-outgoing"
503 ["Push selected" vc-hg-push]))
504
505 (easy-menu-define log-view-mode-menu vc-hg-incoming-mode-map
506 "Hg-incoming Display Menu"
507 `("Hg-incoming"
508 ["Pull selected" vc-hg-pull]))
509
510 (defun vc-hg-outgoing ()
511 (interactive)
512 (let ((bname "*Hg outgoing*"))
513 (vc-hg-command bname 0 nil "outgoing" "-n")
514 (pop-to-buffer bname)
515 (vc-hg-outgoing-mode)))
516
517 (defun vc-hg-incoming ()
518 (interactive)
519 (let ((bname "*Hg incoming*"))
520 (vc-hg-command bname 0 nil "incoming" "-n")
521 (pop-to-buffer bname)
522 (vc-hg-incoming-mode)))
523
524 (declare-function log-view-get-marked "log-view" ())
525
526 ;; XXX maybe also add key bindings for these functions.
527 (defun vc-hg-push ()
528 (interactive)
529 (let ((marked-list (log-view-get-marked)))
530 (if marked-list
531 (vc-hg-command
532 nil 0 nil
533 (cons "push"
534 (apply 'nconc
535 (mapcar (lambda (arg) (list "-r" arg)) marked-list))))
536 (error "No log entries selected for push"))))
537
538 (defun vc-hg-pull ()
539 (interactive)
540 (let ((marked-list (log-view-get-marked)))
541 (if marked-list
542 (vc-hg-command
543 nil 0 nil
544 (cons "pull"
545 (apply 'nconc
546 (mapcar (lambda (arg) (list "-r" arg)) marked-list))))
547 (error "No log entries selected for pull"))))
548
549 ;;; Internal functions
550
551 (defun vc-hg-command (buffer okstatus file-or-list &rest flags)
552 "A wrapper around `vc-do-command' for use in vc-hg.el.
553 The difference to vc-do-command is that this function always invokes `hg',
554 and that it passes `vc-hg-global-switches' to it before FLAGS."
555 (apply 'vc-do-command buffer okstatus "hg" file-or-list
556 (if (stringp vc-hg-global-switches)
557 (cons vc-hg-global-switches flags)
558 (append vc-hg-global-switches
559 flags))))
560
561 (defun vc-hg-root (file)
562 (vc-find-root file ".hg"))
563
564 (provide 'vc-hg)
565
566 ;; arch-tag: bd094dc5-715a-434f-a331-37b9fb7cd954
567 ;;; vc-hg.el ends here