Don't cut off last character of commit descriptions.
[git/git.git] / contrib / emacs / git-blame.el
CommitLineData
b52ba1a5
JN
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
28389d45 7;; Message-ID: <87iren2vqx.fsf@morpheus.local>
b52ba1a5
JN
8;; License: GPL
9;; Keywords: git, version control, release management
10;;
3cc5ca39
XM
11;; Compatibility: Emacs21, Emacs22 and EmacsCVS
12;; Git 1.5 and up
b52ba1a5
JN
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;;
f6f125fb
JN
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.
b52ba1a5
JN
61
62;;; Compatibility:
63;;
3cc5ca39
XM
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:
b52ba1a5
JN
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
3cc5ca39
XM
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))
28389d45 107
24a2293a
JU
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
28389d45 115(defvar git-blame-dark-colors
3cc5ca39
XM
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).")
28389d45
DK
120
121(defvar git-blame-light-colors
3cc5ca39
XM
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).")
28389d45 126
3cc5ca39
XM
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.")
28389d45 133
d2589855
DK
134(defvar git-blame-autoupdate t
135 "*Automatically update the blame display while editing")
136
f0f39bb4
DK
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)
28389d45 148
d2589855
DK
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
3cc5ca39
XM
157;; FIXME: docstrings
158(defvar git-blame-file nil)
159(defvar git-blame-current nil)
160
28389d45
DK
161(defvar git-blame-mode nil)
162(make-variable-buffer-local 'git-blame-mode)
02f0559e
XM
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)))
28389d45 170
f6f125fb 171;;;###autoload
28389d45 172(defun git-blame-mode (&optional arg)
02f0559e
XM
173 "Toggle minor mode for displaying Git blame
174
175With prefix ARG, turn the mode on if ARG is positive."
28389d45 176 (interactive "P")
02f0559e
XM
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'."
28389d45 187 (make-local-variable 'git-blame-colors)
d2589855
DK
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))
fa882116 191 (git-blame-cleanup)
02f0559e
XM
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))
28389d45 207
f0f39bb4
DK
208;;;###autoload
209(defun git-reblame ()
210 "Recalculate all blame information in the current buffer"
02f0559e 211 (interactive)
f0f39bb4 212 (unless git-blame-mode
3cc5ca39 213 (error "Git-blame is not active"))
02f0559e 214
f0f39bb4
DK
215 (git-blame-cleanup)
216 (git-blame-run))
217
d2589855 218(defun git-blame-run (&optional startline endline)
f0f39bb4
DK
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
d2589855
DK
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))))
f0f39bb4 231 (setq git-blame-proc
d2589855
DK
232 (apply 'start-process
233 "git-blame" blame-buf
234 "git" "blame"
235 args))
f0f39bb4
DK
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))))
28389d45 246
96df551c
DK
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
28389d45
DK
253(defun git-blame-cleanup ()
254 "Remove all blame properties"
255 (mapcar 'delete-overlay git-blame-overlays)
256 (setq git-blame-overlays nil)
96df551c 257 (remove-git-blame-text-properties (point-min) (point-max)))
28389d45 258
d2589855
DK
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
28389d45 275(defun git-blame-sentinel (proc status)
f0f39bb4
DK
276 (with-current-buffer (process-buffer proc)
277 (with-current-buffer git-blame-file
d2589855
DK
278 (setq git-blame-proc nil)
279 (if git-blame-update-queue
280 (git-blame-delayed-update))))
28389d45 281 ;;(kill-buffer (process-buffer proc))
96df551c
DK
282 ;;(message "git blame finished")
283 )
28389d45
DK
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
9f85fb32
DK
306 (if (string= hash "0000000000000000000000000000000000000000")
307 nil
308 (git-blame-new-commit
309 hash src-line res-line num-lines))))
28389d45
DK
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
28389d45
DK
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))
d2589855
DK
334 (inhibit-point-motion-hooks t)
335 (inhibit-modification-hooks t))
28389d45 336 (when (not info)
3cc5ca39
XM
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)))
28389d45 342 (setq info (list hash src-line res-line num-lines
9f85fb32 343 (git-describe-commit hash)
28389d45
DK
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
9f85fb32
DK
357 (cdr (assq 'color (nthcdr 5 info)))))
358 ;; the point-entered property doesn't seem to work in overlays
28389d45
DK
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
9f85fb32
DK
378(defun git-describe-commit (hash)
379 (with-temp-buffer
380 (call-process "git" nil t nil
24a2293a
JU
381 "log" "-1"
382 (concat "--pretty=" git-blame-log-oneline-format)
9f85fb32 383 hash)
c79cc2e5 384 (buffer-substring (point-min) (point-max))))
9f85fb32
DK
385
386(defvar git-blame-last-identification nil)
387(make-variable-buffer-local 'git-blame-last-identification)
28389d45
DK
388(defun git-blame-identify (&optional hash)
389 (interactive)
9f85fb32
DK
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))))
b52ba1a5 394
d2589855
DK
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
b52ba1a5
JN
432(provide 'git-blame)
433
434;;; git-blame.el ends here