~ubuntu-branches/ubuntu/edgy/git-core/edgy-backports

« back to all changes in this revision

Viewing changes to contrib/emacs/git-blame.el

  • Committer: Package Import Robot
  • Author(s): LaMont Jones
  • Date: 2007-11-29 07:28:44 UTC
  • mfrom: (8.1.2 dapper-backports)
  • Revision ID: package-import@ubuntu.com-20071129072844-umsb7y3140yhxkth
Tags: 1:1.5.3.6-1.1~dapper1
* backport to dapper et al.
  - debian/rules changes to support source:Upstream-Version for old dpkg.
  - allow asciidoc (>7.0.2-3)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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
 
86
elements of the list.
 
87
 
 
88
a 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
 
102
selected 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-dark-colors
 
109
  (git-blame-color-scale "0c" "04" "24" "1c" "2c" "34" "14" "3c")
 
110
  "*List of colors (format #RGB) to use in a dark environment.
 
111
 
 
112
To check out the list, evaluate (list-colors-display git-blame-dark-colors).")
 
113
 
 
114
(defvar git-blame-light-colors
 
115
  (git-blame-color-scale "c4" "d4" "cc" "dc" "f4" "e4" "fc" "ec")
 
116
  "*List of colors (format #RGB) to use in a light environment.
 
117
 
 
118
To check out the list, evaluate (list-colors-display git-blame-light-colors).")
 
119
 
 
120
(defvar git-blame-colors '()
 
121
  "Colors used by git-blame. The list is built once when activating git-blame
 
122
minor mode.")
 
123
 
 
124
(defvar git-blame-ancient-color "dark green"
 
125
  "*Color to be used for ancient commit.")
 
126
 
 
127
(defvar git-blame-autoupdate t
 
128
  "*Automatically update the blame display while editing")
 
129
 
 
130
(defvar git-blame-proc nil
 
131
  "The running git-blame process")
 
132
(make-variable-buffer-local 'git-blame-proc)
 
133
 
 
134
(defvar git-blame-overlays nil
 
135
  "The git-blame overlays used in the current buffer.")
 
136
(make-variable-buffer-local 'git-blame-overlays)
 
137
 
 
138
(defvar git-blame-cache nil
 
139
  "A cache of git-blame information for the current buffer")
 
140
(make-variable-buffer-local 'git-blame-cache)
 
141
 
 
142
(defvar git-blame-idle-timer nil
 
143
  "An idle timer that updates the blame")
 
144
(make-variable-buffer-local 'git-blame-cache)
 
145
 
 
146
(defvar git-blame-update-queue nil
 
147
  "A queue of update requests")
 
148
(make-variable-buffer-local 'git-blame-update-queue)
 
149
 
 
150
;; FIXME: docstrings
 
151
(defvar git-blame-file nil)
 
152
(defvar git-blame-current nil)
 
153
 
 
154
(defvar git-blame-mode nil)
 
155
(make-variable-buffer-local 'git-blame-mode)
 
156
 
 
157
(defvar git-blame-mode-line-string " blame"
 
158
  "String to display on the mode line when git-blame is active.")
 
159
 
 
160
(or (assq 'git-blame-mode minor-mode-alist)
 
161
    (setq minor-mode-alist
 
162
          (cons '(git-blame-mode git-blame-mode-line-string) minor-mode-alist)))
 
163
 
 
164
;;;###autoload
 
165
(defun git-blame-mode (&optional arg)
 
166
  "Toggle minor mode for displaying Git blame
 
167
 
 
168
With prefix ARG, turn the mode on if ARG is positive."
 
169
  (interactive "P")
 
170
  (cond
 
171
   ((null arg)
 
172
    (if git-blame-mode (git-blame-mode-off) (git-blame-mode-on)))
 
173
   ((> (prefix-numeric-value arg) 0) (git-blame-mode-on))
 
174
   (t (git-blame-mode-off))))
 
175
 
 
176
(defun git-blame-mode-on ()
 
177
  "Turn on git-blame mode.
 
178
 
 
179
See also function `git-blame-mode'."
 
180
  (make-local-variable 'git-blame-colors)
 
181
  (if git-blame-autoupdate
 
182
      (add-hook 'after-change-functions 'git-blame-after-change nil t)
 
183
    (remove-hook 'after-change-functions 'git-blame-after-change t))
 
184
  (git-blame-cleanup)
 
185
  (let ((bgmode (cdr (assoc 'background-mode (frame-parameters)))))
 
186
    (if (eq bgmode 'dark)
 
187
        (setq git-blame-colors git-blame-dark-colors)
 
188
      (setq git-blame-colors git-blame-light-colors)))
 
189
  (setq git-blame-cache (make-hash-table :test 'equal))
 
190
  (setq git-blame-mode t)
 
191
  (git-blame-run))
 
192
 
 
193
(defun git-blame-mode-off ()
 
194
  "Turn off git-blame mode.
 
195
 
 
196
See also function `git-blame-mode'."
 
197
  (git-blame-cleanup)
 
198
  (if git-blame-idle-timer (cancel-timer git-blame-idle-timer))
 
199
  (setq git-blame-mode nil))
 
200
 
 
201
;;;###autoload
 
202
(defun git-reblame ()
 
203
  "Recalculate all blame information in the current buffer"
 
204
  (interactive)
 
205
  (unless git-blame-mode
 
206
    (error "Git-blame is not active"))
 
207
 
 
208
  (git-blame-cleanup)
 
209
  (git-blame-run))
 
210
 
 
211
(defun git-blame-run (&optional startline endline)
 
212
  (if git-blame-proc
 
213
      ;; Should maybe queue up a new run here
 
214
      (message "Already running git blame")
 
215
    (let ((display-buf (current-buffer))
 
216
          (blame-buf (get-buffer-create
 
217
                      (concat " git blame for " (buffer-name))))
 
218
          (args '("--incremental" "--contents" "-")))
 
219
      (if startline
 
220
          (setq args (append args
 
221
                             (list "-L" (format "%d,%d" startline endline)))))
 
222
      (setq args (append args
 
223
                         (list (file-name-nondirectory buffer-file-name))))
 
224
      (setq git-blame-proc
 
225
            (apply 'start-process
 
226
                   "git-blame" blame-buf
 
227
                   "git" "blame"
 
228
                   args))
 
229
      (with-current-buffer blame-buf
 
230
        (erase-buffer)
 
231
        (make-local-variable 'git-blame-file)
 
232
        (make-local-variable 'git-blame-current)
 
233
        (setq git-blame-file display-buf)
 
234
        (setq git-blame-current nil))
 
235
      (set-process-filter git-blame-proc 'git-blame-filter)
 
236
      (set-process-sentinel git-blame-proc 'git-blame-sentinel)
 
237
      (process-send-region git-blame-proc (point-min) (point-max))
 
238
      (process-send-eof git-blame-proc))))
 
239
 
 
240
(defun remove-git-blame-text-properties (start end)
 
241
  (let ((modified (buffer-modified-p))
 
242
        (inhibit-read-only t))
 
243
    (remove-text-properties start end '(point-entered nil))
 
244
    (set-buffer-modified-p modified)))
 
245
 
 
246
(defun git-blame-cleanup ()
 
247
  "Remove all blame properties"
 
248
    (mapcar 'delete-overlay git-blame-overlays)
 
249
    (setq git-blame-overlays nil)
 
250
    (remove-git-blame-text-properties (point-min) (point-max)))
 
251
 
 
252
(defun git-blame-update-region (start end)
 
253
  "Rerun blame to get updates between START and END"
 
254
  (let ((overlays (overlays-in start end)))
 
255
    (while overlays
 
256
      (let ((overlay (pop overlays)))
 
257
        (if (< (overlay-start overlay) start)
 
258
            (setq start (overlay-start overlay)))
 
259
        (if (> (overlay-end overlay) end)
 
260
            (setq end (overlay-end overlay)))
 
261
        (setq git-blame-overlays (delete overlay git-blame-overlays))
 
262
        (delete-overlay overlay))))
 
263
  (remove-git-blame-text-properties start end)
 
264
  ;; We can be sure that start and end are at line breaks
 
265
  (git-blame-run (1+ (count-lines (point-min) start))
 
266
                 (count-lines (point-min) end)))
 
267
 
 
268
(defun git-blame-sentinel (proc status)
 
269
  (with-current-buffer (process-buffer proc)
 
270
    (with-current-buffer git-blame-file
 
271
      (setq git-blame-proc nil)
 
272
      (if git-blame-update-queue
 
273
          (git-blame-delayed-update))))
 
274
  ;;(kill-buffer (process-buffer proc))
 
275
  ;;(message "git blame finished")
 
276
  )
 
277
 
 
278
(defvar in-blame-filter nil)
 
279
 
 
280
(defun git-blame-filter (proc str)
 
281
  (save-excursion
 
282
    (set-buffer (process-buffer proc))
 
283
    (goto-char (process-mark proc))
 
284
    (insert-before-markers str)
 
285
    (goto-char 0)
 
286
    (unless in-blame-filter
 
287
      (let ((more t)
 
288
            (in-blame-filter t))
 
289
        (while more
 
290
          (setq more (git-blame-parse)))))))
 
291
 
 
292
(defun git-blame-parse ()
 
293
  (cond ((looking-at "\\([0-9a-f]\\{40\\}\\) \\([0-9]+\\) \\([0-9]+\\) \\([0-9]+\\)\n")
 
294
         (let ((hash (match-string 1))
 
295
               (src-line (string-to-number (match-string 2)))
 
296
               (res-line (string-to-number (match-string 3)))
 
297
               (num-lines (string-to-number (match-string 4))))
 
298
           (setq git-blame-current
 
299
                 (if (string= hash "0000000000000000000000000000000000000000")
 
300
                     nil
 
301
                   (git-blame-new-commit
 
302
                    hash src-line res-line num-lines))))
 
303
         (delete-region (point) (match-end 0))
 
304
         t)
 
305
        ((looking-at "filename \\(.+\\)\n")
 
306
         (let ((filename (match-string 1)))
 
307
           (git-blame-add-info "filename" filename))
 
308
         (delete-region (point) (match-end 0))
 
309
         t)
 
310
        ((looking-at "\\([a-z-]+\\) \\(.+\\)\n")
 
311
         (let ((key (match-string 1))
 
312
               (value (match-string 2)))
 
313
           (git-blame-add-info key value))
 
314
         (delete-region (point) (match-end 0))
 
315
         t)
 
316
        ((looking-at "boundary\n")
 
317
         (setq git-blame-current nil)
 
318
         (delete-region (point) (match-end 0))
 
319
         t)
 
320
        (t
 
321
         nil)))
 
322
 
 
323
(defun git-blame-new-commit (hash src-line res-line num-lines)
 
324
  (save-excursion
 
325
    (set-buffer git-blame-file)
 
326
    (let ((info (gethash hash git-blame-cache))
 
327
          (inhibit-point-motion-hooks t)
 
328
          (inhibit-modification-hooks t))
 
329
      (when (not info)
 
330
        ;; Assign a random color to each new commit info
 
331
        ;; Take care not to select the same color multiple times
 
332
        (let ((color (if git-blame-colors
 
333
                         (git-blame-random-pop git-blame-colors)
 
334
                       git-blame-ancient-color)))
 
335
          (setq info (list hash src-line res-line num-lines
 
336
                           (git-describe-commit hash)
 
337
                           (cons 'color color))))
 
338
        (puthash hash info git-blame-cache))
 
339
      (goto-line res-line)
 
340
      (while (> num-lines 0)
 
341
        (if (get-text-property (point) 'git-blame)
 
342
            (forward-line)
 
343
          (let* ((start (point))
 
344
                 (end (progn (forward-line 1) (point)))
 
345
                 (ovl (make-overlay start end)))
 
346
            (push ovl git-blame-overlays)
 
347
            (overlay-put ovl 'git-blame info)
 
348
            (overlay-put ovl 'help-echo hash)
 
349
            (overlay-put ovl 'face (list :background
 
350
                                         (cdr (assq 'color (nthcdr 5 info)))))
 
351
            ;; the point-entered property doesn't seem to work in overlays
 
352
            ;;(overlay-put ovl 'point-entered
 
353
            ;;             `(lambda (x y) (git-blame-identify ,hash)))
 
354
            (let ((modified (buffer-modified-p)))
 
355
              (put-text-property (if (= start 1) start (1- start)) (1- end)
 
356
                                 'point-entered
 
357
                                 `(lambda (x y) (git-blame-identify ,hash)))
 
358
              (set-buffer-modified-p modified))))
 
359
        (setq num-lines (1- num-lines))))))
 
360
 
 
361
(defun git-blame-add-info (key value)
 
362
  (if git-blame-current
 
363
      (nconc git-blame-current (list (cons (intern key) value)))))
 
364
 
 
365
(defun git-blame-current-commit ()
 
366
  (let ((info (get-char-property (point) 'git-blame)))
 
367
    (if info
 
368
        (car info)
 
369
      (error "No commit info"))))
 
370
 
 
371
(defun git-describe-commit (hash)
 
372
  (with-temp-buffer
 
373
    (call-process "git" nil t nil
 
374
                  "log" "-1" "--pretty=oneline"
 
375
                  hash)
 
376
    (buffer-substring (point-min) (1- (point-max)))))
 
377
 
 
378
(defvar git-blame-last-identification nil)
 
379
(make-variable-buffer-local 'git-blame-last-identification)
 
380
(defun git-blame-identify (&optional hash)
 
381
  (interactive)
 
382
  (let ((info (gethash (or hash (git-blame-current-commit)) git-blame-cache)))
 
383
    (when (and info (not (eq info git-blame-last-identification)))
 
384
      (message "%s" (nth 4 info))
 
385
      (setq git-blame-last-identification info))))
 
386
 
 
387
;; (defun git-blame-after-save ()
 
388
;;   (when git-blame-mode
 
389
;;     (git-blame-cleanup)
 
390
;;     (git-blame-run)))
 
391
;; (add-hook 'after-save-hook 'git-blame-after-save)
 
392
 
 
393
(defun git-blame-after-change (start end length)
 
394
  (when git-blame-mode
 
395
    (git-blame-enq-update start end)))
 
396
 
 
397
(defvar git-blame-last-update nil)
 
398
(make-variable-buffer-local 'git-blame-last-update)
 
399
(defun git-blame-enq-update (start end)
 
400
  "Mark the region between START and END as needing blame update"
 
401
  ;; Try to be smart and avoid multiple callouts for sequential
 
402
  ;; editing
 
403
  (cond ((and git-blame-last-update
 
404
              (= start (cdr git-blame-last-update)))
 
405
         (setcdr git-blame-last-update end))
 
406
        ((and git-blame-last-update
 
407
              (= end (car git-blame-last-update)))
 
408
         (setcar git-blame-last-update start))
 
409
        (t
 
410
         (setq git-blame-last-update (cons start end))
 
411
         (setq git-blame-update-queue (nconc git-blame-update-queue
 
412
                                             (list git-blame-last-update)))))
 
413
  (unless (or git-blame-proc git-blame-idle-timer)
 
414
    (setq git-blame-idle-timer
 
415
          (run-with-idle-timer 0.5 nil 'git-blame-delayed-update))))
 
416
 
 
417
(defun git-blame-delayed-update ()
 
418
  (setq git-blame-idle-timer nil)
 
419
  (if git-blame-update-queue
 
420
      (let ((first (pop git-blame-update-queue))
 
421
            (inhibit-point-motion-hooks t))
 
422
        (git-blame-update-region (car first) (cdr first)))))
 
423
 
 
424
(provide 'git-blame)
 
425
 
 
426
;;; git-blame.el ends here