;;; p4-browse.el --- minor mode for browsing revisions of a given file ;; Author: Paul Du Bois ;; Maintainer: dubois@infinite-machine.com ;; $Id: //depot/tools/lisp/p4-browse.el#2 $ ;;; Commentary: ;; This is a minor mode for viewing revisions of a file. It binds ;; keys that bring up diffs and changelist descriptions for the currently ;; viewed revision of the file. ;; diffs and changelist buffers automatically update themselves when ;; the user moves to a different revision. ;; The single user entry point is the command `p4-browse-file-revisions'. ;;; Code: (require 'p4) (defun p4-browse-file-revisions (&optional file) "Begin browsing revisions of FILE. Interactively, browses the current buffer's file." (interactive) (or file (setq file buffer-file-name)) (p4b-display-revision file "have")) ;;; ---------------------------------------------------------------------- ;;; minor mode ;;; ---------------------------------------------------------------------- (defvar p4-browse-minor-mode nil "Minor mode for browsing through revisions of a file.") (make-variable-buffer-local 'p4-browse-minor-mode) ;; just here for documentation string (defun p4-browse-minor-mode () "Minor mode for browsing through revisions of a file. \\[p4b-revision-previous] \\[p4b-revision-next] go back and forward one revision, respectively. \\[p4b-revision-goto] will jump to a given revision. \\[p4b-describe-changelist] displays the changelist that created the current revision. \\[p4b-diff-with-previous] displays the differences from that changelist. \\[p4b-quit] quits browse mode.") (defvar p4-browse-map nil) (if p4-browse-map nil (let ((map (make-keymap))) (suppress-keymap map 'nodigits) (define-key map "c" 'p4b-describe-changelist) (define-key map "d" 'p4b-diff-with-previous) (define-key map "g" 'p4b-revision-goto) (define-key map "q" 'p4b-quit) (define-key map "<" 'p4b-revision-previous) (define-key map ">" 'p4b-revision-next) (define-key map "?" 'p4b-help) (setq p4-browse-map map) (setq minor-mode-map-alist (cons (cons 'p4-browse-minor-mode p4-browse-map) (delq 'p4-browse-minor-mode minor-mode-map-alist))))) (or (assq 'p4-browse-minor-mode minor-mode-alist) (setq minor-mode-alist (cons '(p4-browse-minor-mode " P4-Browse") minor-mode-alist))) ;; Name of buffer to use for "p4 describe" output (defvar p4b-description-buffer "*P4 browse: describe*") (defvar p4b-diff-buffer "*P4 browse: diff2*") ;;; ---------------------------------------------------------------------- ;;; User-level commands ;;; ---------------------------------------------------------------------- (defun p4b-help () "Display simple help for p4-browse minor mode. \\{p4-browse-map}" (interactive) (message "< > g: Prev/next/goto revision c d ?: Show changelist/diff/help q: quit")) (defun p4b-describe-changelist () "View the changelist description associated with the current revision." (interactive) (let ((inhibit-read-only t) (change (nth 2 (p4b-parse-header)))) (p4-exec-p4-fast p4b-description-buffer "describe" "-s" change) (save-excursion (set-buffer p4b-description-buffer) (set-buffer-modified-p nil) (toggle-read-only t) (view-mode-enter nil 'p4b-kill-buffer)) ;; play nicely with auto-update (if (null (get-buffer-window p4b-description-buffer 'visible)) (p4-display-output p4b-description-buffer)))) (defun p4b-kill-buffer (buffer-name) (let ((buffer (get-buffer buffer-name))) (if (null buffer) nil (delete-windows-on buffer) (kill-buffer buffer)))) (defun p4b-diff-with-previous () "Show diff that lead to current revision." (interactive) (let* ((header (p4b-parse-header)) (file (nth 0 header)) (revision (string-to-number (nth 1 header))) (buf p4b-diff-buffer)) (p4-exec-p4-fast buf "diff2" (format "%s#%d" file (1- revision)) (format "%s#%d" file revision)) (save-excursion (set-buffer buf) (run-hooks 'p4-diff-hook) (goto-char (point-min))) ;; play nicely with auto-update (if (null (get-buffer-window buf 'visible)) (p4-display-output buf "Diff")))) (defun p4b-revision-goto (arg) "View the passed revision of the file. Prefix argument is the revision to view." (interactive (list (if current-prefix-arg (prefix-numeric-value current-prefix-arg) ;; read string instead of number so user can ask for head/have (read-string "Revision: ")))) (let ((file (nth 0 (p4b-parse-header)))) (p4b-display-revision file arg 'reuse-window))) (defun p4b-revision-previous (arg) "View the previous revision of the file. Prefix argument is the number of revisions to go back." (interactive "p") (p4b-revision-next (- arg))) (defun p4b-revision-next (arg) "View the next revision of the file Prefix argument is the number of revisions to go forward." (interactive "p") (let ((file (nth 0 (p4b-parse-header))) (revision (+ arg (string-to-number (nth 1 (p4b-parse-header)))))) (p4b-display-revision file revision 'reuse-window))) (defun p4b-quit () "Quit and clean up any spare buffers" (interactive) (mapcar 'p4b-kill-buffer (list (current-buffer) p4b-description-buffer p4b-diff-buffer))) ;;; ---------------------------------------------------------------------- ;;; Internals ;;; ---------------------------------------------------------------------- (defun p4b-parse-header () (save-excursion (goto-char (point-min)) (if (not (looking-at "\\(.*\\)#\\(\\sw+\\).*change \\(\\sw+\\)")) (error "Can't find header") (list (match-string 1) (match-string 2) (match-string 3))))) (defun p4b-display-revision (file revision &optional reuse-window) (catch 'abort (let* ((dir default-directory) ;; FILE might be in //depot syntax (file-path (concat dir "/" (file-name-nondirectory file))) (buf (get-buffer-create "*tmp revision*")) (buffer-name (concat (file-name-nondirectory file) "#" revision)) (context (p4-make-window-context (selected-window)))) (message "Fetching revision %s..." revision) (p4-exec-p4-fast buf "print" (concat file "#" revision)) (message "Fetching revision %s... done." revision) (save-excursion (set-buffer buf) (goto-char (point-min)) ;; If the header is not valid, it's probably an error from Perforce (condition-case nil (p4b-parse-header) (error (goto-char 1) (end-of-line) (message "%s" (buffer-substring 1 (point))) (throw 'abort nil))) ;; normal-mode relies on the file name to choose a mode (set-visited-file-name file-path t) (normal-mode) (set-visited-file-name nil) ;; set default-dir so p4 client can find P4CONFIG files (setq default-directory dir) (setq p4-browse-minor-mode t) (if (get-buffer buffer-name) (kill-buffer buffer-name)) (rename-buffer buffer-name) (set-buffer-modified-p nil) (toggle-read-only t)) (if reuse-window (progn (kill-buffer (current-buffer)) (switch-to-buffer buf) (p4-restore-window-context (selected-window) context) (if (get-buffer-window p4b-description-buffer 'visible) (p4b-describe-changelist)) (if (get-buffer-window p4b-diff-buffer 'visible) (p4b-diff-with-previous))) (p4-display-output buf) (p4b-help)))))