contrib / emacs / git-blame.elon commit Merge branch 'maint-1.6.4' into maint (050dfc4)
   1;;; git-blame.el --- Minor mode for incremental blame for Git  -*- coding: utf-8 -*-
   2;;
   3;; Copyright (C) 2007  David Kågedal
   4;;
   5;; Authors:    David Kågedal <davidk@lysator.liu.se>
   6;; Created:    31 Jan 2007
   7;; Message-ID: <87iren2vqx.fsf@morpheus.local>
   8;; License:    GPL
   9;; Keywords:   git, version control, release management
  10;;
  11;; Compatibility: Emacs21, Emacs22 and EmacsCVS
  12;;                Git 1.5 and up
  13
  14;; This file is *NOT* part of GNU Emacs.
  15;; This file is distributed under the same terms as GNU Emacs.
  16
  17;; This program is free software; you can redistribute it and/or
  18;; modify it under the terms of the GNU General Public License as
  19;; published by the Free Software Foundation; either version 2 of
  20;; the License, or (at your option) any later version.
  21
  22;; This program is distributed in the hope that it will be
  23;; useful, but WITHOUT ANY WARRANTY; without even the implied
  24;; warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
  25;; PURPOSE.  See the GNU General Public License for more details.
  26
  27;; You should have received a copy of the GNU General Public
  28;; License along with this program; if not, write to the Free
  29;; Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
  30;; MA 02111-1307 USA
  31
  32;; http://www.fsf.org/copyleft/gpl.html
  33
  34
  35;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  36;;
  37;;; Commentary:
  38;;
  39;; Here is an Emacs implementation of incremental git-blame.  When you
  40;; turn it on while viewing a file, the editor buffer will be updated by
  41;; setting the background of individual lines to a color that reflects
  42;; which commit it comes from.  And when you move around the buffer, a
  43;; one-line summary will be shown in the echo area.
  44
  45;;; Installation:
  46;;
  47;; To use this package, put it somewhere in `load-path' (or add
  48;; directory with git-blame.el to `load-path'), and add the following
  49;; line to your .emacs:
  50;;
  51;;    (require 'git-blame)
  52;;
  53;; If you do not want to load this package before it is necessary, you
  54;; can make use of the `autoload' feature, e.g. by adding to your .emacs
  55;; the following lines
  56;;
  57;;    (autoload 'git-blame-mode "git-blame"
  58;;              "Minor mode for incremental blame for Git." t)
  59;;
  60;; Then first use of `M-x git-blame-mode' would load the package.
  61
  62;;; Compatibility:
  63;;
  64;; It requires GNU Emacs 21 or later and Git 1.5.0 and up
  65;;
  66;; If you'are using Emacs 20, try changing this:
  67;;
  68;;            (overlay-put ovl 'face (list :background
  69;;                                         (cdr (assq 'color (cddddr info)))))
  70;;
  71;; to
  72;;
  73;;            (overlay-put ovl 'face (cons 'background-color
  74;;                                         (cdr (assq 'color (cddddr info)))))
  75
  76
  77;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  78;;
  79;;; Code:
  80
  81(eval-when-compile (require 'cl))                             ; to use `push', `pop'
  82
  83
  84(defun git-blame-color-scale (&rest elements)
  85  "Given a list, returns a list of triples formed with each
  86elements of the list.
  87
  88a b => bbb bba bab baa abb aba aaa aab"
  89  (let (result)
  90    (dolist (a elements)
  91      (dolist (b elements)
  92        (dolist (c elements)
  93          (setq result (cons (format "#%s%s%s" a b c) result)))))
  94    result))
  95
  96;; (git-blame-color-scale "0c" "04" "24" "1c" "2c" "34" "14" "3c") =>
  97;; ("#3c3c3c" "#3c3c14" "#3c3c34" "#3c3c2c" "#3c3c1c" "#3c3c24"
  98;; "#3c3c04" "#3c3c0c" "#3c143c" "#3c1414" "#3c1434" "#3c142c" ...)
  99
 100(defmacro git-blame-random-pop (l)
 101  "Select a random element from L and returns it. Also remove
 102selected element from l."
 103  ;; only works on lists with unique elements
 104  `(let ((e (elt ,l (random (length ,l)))))
 105     (setq ,l (remove e ,l))
 106     e))
 107
 108(defvar git-blame-log-oneline-format
 109  "format:[%cr] %cn: %s"
 110  "*Formatting option used for describing current line in the minibuffer.
 111
 112This option is used to pass to git log --pretty= command-line option,
 113and describe which commit the current line was made.")
 114
 115(defvar git-blame-dark-colors
 116  (git-blame-color-scale "0c" "04" "24" "1c" "2c" "34" "14" "3c")
 117  "*List of colors (format #RGB) to use in a dark environment.
 118
 119To check out the list, evaluate (list-colors-display git-blame-dark-colors).")
 120
 121(defvar git-blame-light-colors
 122  (git-blame-color-scale "c4" "d4" "cc" "dc" "f4" "e4" "fc" "ec")
 123  "*List of colors (format #RGB) to use in a light environment.
 124
 125To check out the list, evaluate (list-colors-display git-blame-light-colors).")
 126
 127(defvar git-blame-colors '()
 128  "Colors used by git-blame. The list is built once when activating git-blame
 129minor mode.")
 130
 131(defvar git-blame-ancient-color "dark green"
 132  "*Color to be used for ancient commit.")
 133
 134(defvar git-blame-autoupdate t
 135  "*Automatically update the blame display while editing")
 136
 137(defvar git-blame-proc nil
 138  "The running git-blame process")
 139(make-variable-buffer-local 'git-blame-proc)
 140
 141(defvar git-blame-overlays nil
 142  "The git-blame overlays used in the current buffer.")
 143(make-variable-buffer-local 'git-blame-overlays)
 144
 145(defvar git-blame-cache nil
 146  "A cache of git-blame information for the current buffer")
 147(make-variable-buffer-local 'git-blame-cache)
 148
 149(defvar git-blame-idle-timer nil
 150  "An idle timer that updates the blame")
 151(make-variable-buffer-local 'git-blame-cache)
 152
 153(defvar git-blame-update-queue nil
 154  "A queue of update requests")
 155(make-variable-buffer-local 'git-blame-update-queue)
 156
 157;; FIXME: docstrings
 158(defvar git-blame-file nil)
 159(defvar git-blame-current nil)
 160
 161(defvar git-blame-mode nil)
 162(make-variable-buffer-local 'git-blame-mode)
 163
 164(defvar git-blame-mode-line-string " blame"
 165  "String to display on the mode line when git-blame is active.")
 166
 167(or (assq 'git-blame-mode minor-mode-alist)
 168    (setq minor-mode-alist
 169          (cons '(git-blame-mode git-blame-mode-line-string) minor-mode-alist)))
 170
 171;;;###autoload
 172(defun git-blame-mode (&optional arg)
 173  "Toggle minor mode for displaying Git blame
 174
 175With prefix ARG, turn the mode on if ARG is positive."
 176  (interactive "P")
 177  (cond
 178   ((null arg)
 179    (if git-blame-mode (git-blame-mode-off) (git-blame-mode-on)))
 180   ((> (prefix-numeric-value arg) 0) (git-blame-mode-on))
 181   (t (git-blame-mode-off))))
 182
 183(defun git-blame-mode-on ()
 184  "Turn on git-blame mode.
 185
 186See also function `git-blame-mode'."
 187  (make-local-variable 'git-blame-colors)
 188  (if git-blame-autoupdate
 189      (add-hook 'after-change-functions 'git-blame-after-change nil t)
 190    (remove-hook 'after-change-functions 'git-blame-after-change t))
 191  (git-blame-cleanup)
 192  (let ((bgmode (cdr (assoc 'background-mode (frame-parameters)))))
 193    (if (eq bgmode 'dark)
 194        (setq git-blame-colors git-blame-dark-colors)
 195      (setq git-blame-colors git-blame-light-colors)))
 196  (setq git-blame-cache (make-hash-table :test 'equal))
 197  (setq git-blame-mode t)
 198  (git-blame-run))
 199
 200(defun git-blame-mode-off ()
 201  "Turn off git-blame mode.
 202
 203See also function `git-blame-mode'."
 204  (git-blame-cleanup)
 205  (if git-blame-idle-timer (cancel-timer git-blame-idle-timer))
 206  (setq git-blame-mode nil))
 207
 208;;;###autoload
 209(defun git-reblame ()
 210  "Recalculate all blame information in the current buffer"
 211  (interactive)
 212  (unless git-blame-mode
 213    (error "Git-blame is not active"))
 214
 215  (git-blame-cleanup)
 216  (git-blame-run))
 217
 218(defun git-blame-run (&optional startline endline)
 219  (if git-blame-proc
 220      ;; Should maybe queue up a new run here
 221      (message "Already running git blame")
 222    (let ((display-buf (current-buffer))
 223          (blame-buf (get-buffer-create
 224                      (concat " git blame for " (buffer-name))))
 225          (args '("--incremental" "--contents" "-")))
 226      (if startline
 227          (setq args (append args
 228                             (list "-L" (format "%d,%d" startline endline)))))
 229      (setq args (append args
 230                         (list (file-name-nondirectory buffer-file-name))))
 231      (setq git-blame-proc
 232            (apply 'start-process
 233                   "git-blame" blame-buf
 234                   "git" "blame"
 235                   args))
 236      (with-current-buffer blame-buf
 237        (erase-buffer)
 238        (make-local-variable 'git-blame-file)
 239        (make-local-variable 'git-blame-current)
 240        (setq git-blame-file display-buf)
 241        (setq git-blame-current nil))
 242      (set-process-filter git-blame-proc 'git-blame-filter)
 243      (set-process-sentinel git-blame-proc 'git-blame-sentinel)
 244      (process-send-region git-blame-proc (point-min) (point-max))
 245      (process-send-eof git-blame-proc))))
 246
 247(defun remove-git-blame-text-properties (start end)
 248  (let ((modified (buffer-modified-p))
 249        (inhibit-read-only t))
 250    (remove-text-properties start end '(point-entered nil))
 251    (set-buffer-modified-p modified)))
 252
 253(defun git-blame-cleanup ()
 254  "Remove all blame properties"
 255    (mapcar 'delete-overlay git-blame-overlays)
 256    (setq git-blame-overlays nil)
 257    (remove-git-blame-text-properties (point-min) (point-max)))
 258
 259(defun git-blame-update-region (start end)
 260  "Rerun blame to get updates between START and END"
 261  (let ((overlays (overlays-in start end)))
 262    (while overlays
 263      (let ((overlay (pop overlays)))
 264        (if (< (overlay-start overlay) start)
 265            (setq start (overlay-start overlay)))
 266        (if (> (overlay-end overlay) end)
 267            (setq end (overlay-end overlay)))
 268        (setq git-blame-overlays (delete overlay git-blame-overlays))
 269        (delete-overlay overlay))))
 270  (remove-git-blame-text-properties start end)
 271  ;; We can be sure that start and end are at line breaks
 272  (git-blame-run (1+ (count-lines (point-min) start))
 273                 (count-lines (point-min) end)))
 274
 275(defun git-blame-sentinel (proc status)
 276  (with-current-buffer (process-buffer proc)
 277    (with-current-buffer git-blame-file
 278      (setq git-blame-proc nil)
 279      (if git-blame-update-queue
 280          (git-blame-delayed-update))))
 281  ;;(kill-buffer (process-buffer proc))
 282  ;;(message "git blame finished")
 283  )
 284
 285(defvar in-blame-filter nil)
 286
 287(defun git-blame-filter (proc str)
 288  (save-excursion
 289    (set-buffer (process-buffer proc))
 290    (goto-char (process-mark proc))
 291    (insert-before-markers str)
 292    (goto-char 0)
 293    (unless in-blame-filter
 294      (let ((more t)
 295            (in-blame-filter t))
 296        (while more
 297          (setq more (git-blame-parse)))))))
 298
 299(defun git-blame-parse ()
 300  (cond ((looking-at "\\([0-9a-f]\\{40\\}\\) \\([0-9]+\\) \\([0-9]+\\) \\([0-9]+\\)\n")
 301         (let ((hash (match-string 1))
 302               (src-line (string-to-number (match-string 2)))
 303               (res-line (string-to-number (match-string 3)))
 304               (num-lines (string-to-number (match-string 4))))
 305           (setq git-blame-current
 306                 (if (string= hash "0000000000000000000000000000000000000000")
 307                     nil
 308                   (git-blame-new-commit
 309                    hash src-line res-line num-lines))))
 310         (delete-region (point) (match-end 0))
 311         t)
 312        ((looking-at "filename \\(.+\\)\n")
 313         (let ((filename (match-string 1)))
 314           (git-blame-add-info "filename" filename))
 315         (delete-region (point) (match-end 0))
 316         t)
 317        ((looking-at "\\([a-z-]+\\) \\(.+\\)\n")
 318         (let ((key (match-string 1))
 319               (value (match-string 2)))
 320           (git-blame-add-info key value))
 321         (delete-region (point) (match-end 0))
 322         t)
 323        ((looking-at "boundary\n")
 324         (setq git-blame-current nil)
 325         (delete-region (point) (match-end 0))
 326         t)
 327        (t
 328         nil)))
 329
 330(defun git-blame-new-commit (hash src-line res-line num-lines)
 331  (save-excursion
 332    (set-buffer git-blame-file)
 333    (let ((info (gethash hash git-blame-cache))
 334          (inhibit-point-motion-hooks t)
 335          (inhibit-modification-hooks t))
 336      (when (not info)
 337        ;; Assign a random color to each new commit info
 338        ;; Take care not to select the same color multiple times
 339        (let ((color (if git-blame-colors
 340                         (git-blame-random-pop git-blame-colors)
 341                       git-blame-ancient-color)))
 342          (setq info (list hash src-line res-line num-lines
 343                           (git-describe-commit hash)
 344                           (cons 'color color))))
 345        (puthash hash info git-blame-cache))
 346      (goto-line res-line)
 347      (while (> num-lines 0)
 348        (if (get-text-property (point) 'git-blame)
 349            (forward-line)
 350          (let* ((start (point))
 351                 (end (progn (forward-line 1) (point)))
 352                 (ovl (make-overlay start end)))
 353            (push ovl git-blame-overlays)
 354            (overlay-put ovl 'git-blame info)
 355            (overlay-put ovl 'help-echo hash)
 356            (overlay-put ovl 'face (list :background
 357                                         (cdr (assq 'color (nthcdr 5 info)))))
 358            ;; the point-entered property doesn't seem to work in overlays
 359            ;;(overlay-put ovl 'point-entered
 360            ;;             `(lambda (x y) (git-blame-identify ,hash)))
 361            (let ((modified (buffer-modified-p)))
 362              (put-text-property (if (= start 1) start (1- start)) (1- end)
 363                                 'point-entered
 364                                 `(lambda (x y) (git-blame-identify ,hash)))
 365              (set-buffer-modified-p modified))))
 366        (setq num-lines (1- num-lines))))))
 367
 368(defun git-blame-add-info (key value)
 369  (if git-blame-current
 370      (nconc git-blame-current (list (cons (intern key) value)))))
 371
 372(defun git-blame-current-commit ()
 373  (let ((info (get-char-property (point) 'git-blame)))
 374    (if info
 375        (car info)
 376      (error "No commit info"))))
 377
 378(defun git-describe-commit (hash)
 379  (with-temp-buffer
 380    (call-process "git" nil t nil
 381                  "log" "-1"
 382                  (concat "--pretty=" git-blame-log-oneline-format)
 383                  hash)
 384    (buffer-substring (point-min) (point-max))))
 385
 386(defvar git-blame-last-identification nil)
 387(make-variable-buffer-local 'git-blame-last-identification)
 388(defun git-blame-identify (&optional hash)
 389  (interactive)
 390  (let ((info (gethash (or hash (git-blame-current-commit)) git-blame-cache)))
 391    (when (and info (not (eq info git-blame-last-identification)))
 392      (message "%s" (nth 4 info))
 393      (setq git-blame-last-identification info))))
 394
 395;; (defun git-blame-after-save ()
 396;;   (when git-blame-mode
 397;;     (git-blame-cleanup)
 398;;     (git-blame-run)))
 399;; (add-hook 'after-save-hook 'git-blame-after-save)
 400
 401(defun git-blame-after-change (start end length)
 402  (when git-blame-mode
 403    (git-blame-enq-update start end)))
 404
 405(defvar git-blame-last-update nil)
 406(make-variable-buffer-local 'git-blame-last-update)
 407(defun git-blame-enq-update (start end)
 408  "Mark the region between START and END as needing blame update"
 409  ;; Try to be smart and avoid multiple callouts for sequential
 410  ;; editing
 411  (cond ((and git-blame-last-update
 412              (= start (cdr git-blame-last-update)))
 413         (setcdr git-blame-last-update end))
 414        ((and git-blame-last-update
 415              (= end (car git-blame-last-update)))
 416         (setcar git-blame-last-update start))
 417        (t
 418         (setq git-blame-last-update (cons start end))
 419         (setq git-blame-update-queue (nconc git-blame-update-queue
 420                                             (list git-blame-last-update)))))
 421  (unless (or git-blame-proc git-blame-idle-timer)
 422    (setq git-blame-idle-timer
 423          (run-with-idle-timer 0.5 nil 'git-blame-delayed-update))))
 424
 425(defun git-blame-delayed-update ()
 426  (setq git-blame-idle-timer nil)
 427  (if git-blame-update-queue
 428      (let ((first (pop git-blame-update-queue))
 429            (inhibit-point-motion-hooks t))
 430        (git-blame-update-region (car first) (cdr first)))))
 431
 432(provide 'git-blame)
 433
 434;;; git-blame.el ends here