dotfiles/elpa/mo-git-blame-0.1.0/mo-git-blame.el
2013-03-15 10:35:58 -07:00

763 lines
34 KiB
EmacsLisp
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

;;; mo-git-blame.el --- An interactive, iterative 'git blame' mode for Emacs
;; Copyright (C) 2009, 2010 Moritz Bunkus <moritz@bunkus.org>
;; Copyright (C) 2010 `tpán Nmec <stepnem@gmail.com>
;; Author: Moritz Bunkus <moritz@bunkus.org>
;; Maintainer: Moritz Bunkus <moritz@bunkus.org>
;; Version: 0.1.0
;; Keywords: tools
;; URL: https://github.com/mbunkus/mo-git-blame
;; mo-git-blame is free software; you can redistribute it and/or
;; modify it under the terms of the GNU General Public License as
;; published by the Free Software Foundation; either version 3, or (at
;; your option) any later version.
;;
;; mo-git-blame is distributed in the hope that it will be useful, but
;; WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
;; General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs; see the file COPYING. If not, write to the
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
;; Boston, MA 02111-1307, USA.
;;; Installation:
;;;
;;; Put this file somewhere in your load-path or add the directory it
;;; is in to it, e.g.:
;;;
;;; (add-to-list 'load-path "~/.emacs.d/mo-git-blame")
;;;
;;; Then add two autoload definitions:
;;;
;;; (autoload 'mo-git-blame-file "mo-git-blame" nil t)
;;; (autoload 'mo-git-blame-current "mo-git-blame" nil t)
(require 'cl)
(require 'easymenu)
(defvar mo-git-blame-vars nil
"Buffer-local plist that stores various variables needed for
interactive use, e.g. the file name, current revision etc.")
(defvar mo-git-blame--wincfg nil)
(defvar mo-git-blame-mode-map
(let ((map (make-keymap)))
(suppress-keymap map t)
(define-key map (kbd "a") 'mo-git-blame-reblame-for-ancestor-of-revision-at)
(define-key map (kbd "A") 'mo-git-blame-reblame-for-ancestor-of-current-revision)
(define-key map (kbd "b") 'mo-git-blame-reblame-for-revision-at)
(define-key map (kbd "B") 'mo-git-blame-reblame-for-specific-revision)
(define-key map (kbd "c") 'mo-git-blame-content-for-revision-at)
(define-key map (kbd "i") 'mo-git-blame-display-info)
(define-key map (kbd "l") 'mo-git-blame-log-for-revision-at)
(define-key map (kbd "L") 'mo-git-blame-log-for-current-revision)
(define-key map (kbd "o") 'mo-git-blame-overwrite-file-with-revision-at)
(define-key map (kbd "O") 'mo-git-blame-overwrite-file-with-current-revision)
(define-key map (kbd "p") 'mo-git-blame-reblame-for-prior-revision)
(define-key map (kbd "q") 'mo-git-blame-quit)
(define-key map (kbd "s") 'mo-git-blame-show-revision-at)
(define-key map (kbd "S") 'mo-git-blame-show-current-revision)
(define-key map (kbd "RET") 'mo-git-blame-show-revision-at)
(define-key map (kbd "TAB") 'mo-git-blame-display-content-buffer)
(define-key map [?\C-x ?k] 'mo-git-blame-quit)
(define-key map [?\C-x ?\C-l] 'mo-git-blame-goto-line)
map)
"The mode map for the blame output window of mo-git-blame-mode.")
(defvar mo-git-blame-content-mode-map
(let ((map (make-keymap)))
(suppress-keymap map t)
(define-key map (kbd "A") 'mo-git-blame-reblame-for-ancestor-of-current-revision)
(define-key map (kbd "B") 'mo-git-blame-reblame-for-specific-revision)
(define-key map (kbd "i") 'mo-git-blame-display-info)
(define-key map (kbd "L") 'mo-git-blame-log-for-current-revision)
(define-key map (kbd "O") 'mo-git-blame-overwrite-file-with-current-revision)
(define-key map (kbd "q") 'mo-git-blame-quit)
(define-key map (kbd "S") 'mo-git-blame-show-current-revision)
(define-key map [?\C-x ?k] 'mo-git-blame-quit)
(define-key map [?\C-x ?\C-l] 'mo-git-blame-goto-line)
map)
"The mode map for the content window of mo-git-blame-mode.")
(easy-menu-define mo-git-blame-mode-menu mo-git-blame-mode-map
"MoGitBlame menu"
'("MoGitBlame"
["Re-blame for revision at point" mo-git-blame-reblame-for-revision-at t]
["Re-blame for ancestor of revision at point" mo-git-blame-reblame-for-ancestor-of-revision-at-point t]
["Raw content for revision at point" mo-git-blame-content-for-revision-at t]
["Log for revision at point" mo-git-blame-log-for-revision-at t]
["Overwrite file with revision at point" mo-git-blame-overwrite-file-with-revision-at t]
["'git show' for revision at point" mo-git-blame-show-revision-at t]
"---"
["Re-blame for ancestor of current revision" mo-git-blame-reblame-for-ancestor-of-current-revision t]
["Log for current revision" mo-git-blame-log-for-current-revision t]
["Overwrite file with current revision" mo-git-blame-overwrite-file-with-current-revision t]
["'git show' for current revision" mo-git-blame-show-current-revision t]
"---"
["Re-blame for prior revision" mo-git-blame-reblame-for-prior-revision t]
["Re-blame for a specific revision" mo-git-blame-reblame-for-specific-revision t]
"---"
["Display status information" mo-git-blame-display-info t]
["Display content buffer" mo-git-blame-display-content-buffer t]
"---"
["Exit MoGitBlame" mo-git-blame-quit t]))
(defgroup mo-git-blame nil
"Interactively use Git's 'blame' from Emacs."
:prefix "mo-git-blame-"
:group 'tools)
(defcustom mo-git-blame-git-executable "git"
"The name of the Git executable."
:group 'mo-git-blame
:type 'string)
(defcustom mo-git-blame-incremental t
"Runs `git blame' in the background with the --incremental
option if this variable is non-nil."
:group 'mo-git-blame
:type '(choice (const :tag "Use --incremental" t)
(const :tag "Don't use --incremental" nil)))
(defcustom mo-git-blame-blame-window-width 45
"The width of the 'blame' window leaving the rest for the
'content' window."
:group 'mo-git-blame
:type 'integer)
(defcustom mo-git-blame-use-ido 'if-available
"Controls whether or not ido will be used. Possible choices:
`never' -- do not use ido even if it is loaded
`if-available' -- use ido if it has been loaded before
`always' -- automatically load ido and use it"
:group 'mo-git-blame
:type '(choice (const :tag "Always" always)
(const :tag "If available" if-available)
(const :tag "Never" never)))
;; This function was taken from magit (called 'magit-trim-line' there).
(defun mo-git-blame-trim-line (str)
(cond ((string= str "")
nil)
((equal (elt str (- (length str) 1)) ?\n)
(substring str 0 (- (length str) 1)))
(t str)))
;; This function was taken from magit (called 'magit-git-output' there).
(defun mo-git-blame-git-output (args)
(with-output-to-string
(with-current-buffer standard-output
(apply #'process-file
mo-git-blame-git-executable
nil (list t nil) nil
args))))
;; This function was taken from magit (called 'magit-git-string' there).
(defun mo-git-blame-git-string (&rest args)
(mo-git-blame-trim-line (mo-git-blame-git-output args)))
(defun mo-git-blame-get-top-dir (cwd)
(let ((cwd (expand-file-name cwd))
git-dir)
(setq git-dir
(or (getenv "GIT_WORK_TREE")
(if (file-directory-p cwd)
(let* ((default-directory cwd)
(dir (mo-git-blame-git-string "rev-parse" "--git-dir"))
(dir (if dir (file-name-directory (expand-file-name dir)) "")))
(if (and dir (file-directory-p dir))
(file-name-as-directory dir))))))
(or git-dir
(error "No Git repository found"))))
(defun mo-git-blame-run (&rest args)
(message "Running 'git %s'..." (car args))
(apply 'call-process mo-git-blame-git-executable nil (current-buffer) nil args)
(message "Running 'git %s'... done" (car args)))
(defvar mo-git-blame-process nil)
(defvar mo-git-blame-client-buffer nil)
(defun mo-git-blame-assert-not-running ()
"Exits with an error if `mo-git-blame-incremental' is true and
git is already/still running."
(if (and mo-git-blame-incremental
mo-git-blame-process
(get-buffer "*mo-git-blame-process*"))
(error "Git is already running")))
(defun mo-git-blame-process-sentinel (process event)
(let ((msg (format "Git %s." (substring event 0 -1)))
(successp (string-match "^finished" event)))
(with-current-buffer (process-buffer process)
(let ((inhibit-read-only t))
(goto-char (point-max))
(insert msg "\n")
(message msg)))
(setq mo-git-blame-process nil)
(message "Running 'git blame'... done")))
(defun mo-git-blame-commit-info-to-time (entry)
(let* ((tz (plist-get entry :author-tz))
(mult (if (string= "+" (substring tz 0 1)) 1 -1))
(hours (string-to-number (substring tz 1 3)))
(minutes (string-to-number (substring tz 3 5))))
(seconds-to-time (+ (string-to-number (plist-get entry :author-time))
(* mult
(+ (* minutes 60)
(* hours 3600)))))))
(defun mo-git-blame-process-filter-process-entry (entry)
(with-current-buffer (plist-get mo-git-blame-vars :blame-buffer)
(save-excursion
(let ((inhibit-read-only t)
(info (format "%s (%s %s %s) %s"
(substring (symbol-name (plist-get entry :hash)) 0 8)
(plist-get entry :author)
(format-time-string "%Y-%m-%d %T" (mo-git-blame-commit-info-to-time entry) t)
(plist-get entry :author-tz)
(plist-get entry :filename)))
i)
(mo-git-blame-goto-line-markless (plist-get entry :result-line))
(dotimes (i (plist-get entry :num-lines))
(insert info)
(goto-char (line-beginning-position 2)))))))
(defun mo-git-blame-set-entry (key value)
(let ((plist (or (plist-get mo-git-blame-data mo-git-blame-curr-entry)
(list :hash mo-git-blame-curr-entry))))
(setq mo-git-blame-data
(plist-put mo-git-blame-data
mo-git-blame-curr-entry
(plist-put plist key value)))))
(defun mo-git-blame-process-filter (process string)
(with-current-buffer (process-buffer process)
(let ((inhibit-read-only t)
done matched)
(save-excursion
(goto-char (process-mark process))
(insert string)
(set-marker (process-mark process) (point)))
(while (not done)
(goto-char (line-end-position))
(setq done (= (point) (point-max)))
(goto-char (line-beginning-position))
(unless done
(setq matched t)
(cond ((and (not mo-git-blame-curr-entry)
(looking-at "^\\([a-fA-F0-9]\\{40\\}\\) +\\([0-9]+\\) +\\([0-9]+\\) +\\([0-9]+\\)$"))
;; SHA line, beginning of entry
(setq mo-git-blame-curr-entry (intern (buffer-substring-no-properties (match-beginning 1) (match-end 1))))
(mo-git-blame-set-entry :source-line (string-to-number (buffer-substring-no-properties (match-beginning 2) (match-end 2))))
(mo-git-blame-set-entry :result-line (string-to-number (buffer-substring-no-properties (match-beginning 3) (match-end 3))))
(mo-git-blame-set-entry :num-lines (string-to-number (buffer-substring-no-properties (match-beginning 4) (match-end 4))))
)
((and mo-git-blame-curr-entry
(looking-at "^filename +\\(.+\\)$"))
;; filename line, end of entry
(mo-git-blame-set-entry :filename (buffer-substring-no-properties (match-beginning 1) (match-end 1)))
(mo-git-blame-process-filter-process-entry (plist-get mo-git-blame-data mo-git-blame-curr-entry))
(setq mo-git-blame-curr-entry nil)
)
((and mo-git-blame-curr-entry
(looking-at "^\\([a-zA-Z0-9-]+\\) +\\(.+\\)$"))
;; property line
(mo-git-blame-set-entry (intern (concat ":" (buffer-substring-no-properties (match-beginning 1) (match-end 1))))
(buffer-substring-no-properties (match-beginning 2) (match-end 2)))
)
(t (setq matched nil)))
(forward-line 1))))))
(defun mo-git-blame-run* (&rest args)
(message "Running 'git blame'...")
(let ((buf (get-buffer-create "*mo-git-blame-process*"))
(cmd (car args))
(dir default-directory)
(vars mo-git-blame-vars))
(save-excursion
(set-buffer buf)
(setq buffer-read-only t)
(let ((inhibit-read-only t))
(set (make-local-variable 'mo-git-blame-data) nil)
(set (make-local-variable 'mo-git-blame-curr-entry) nil)
(set (make-local-variable 'mo-git-blame-vars) vars)
(setq default-directory dir
mo-git-blame-process (apply 'start-file-process cmd buf mo-git-blame-git-executable args))
(set-process-sentinel mo-git-blame-process 'mo-git-blame-process-sentinel)
(set-process-filter mo-git-blame-process 'mo-git-blame-process-filter)))))
(defun mo-git-blame-get-output-buffer ()
(let* ((name "*mo-git-blame-output*")
(buffer (get-buffer name)))
(if (null buffer)
(progn
(setq buffer (get-buffer-create name))
(with-current-buffer buffer
(use-local-map mo-git-blame-mode-map))))
buffer))
(defun mo-git-blame-parse-rev (revision)
(let ((result (mo-git-blame-git-string "rev-parse" "--short" revision)))
(unless result
(error "Unparseable revision %s" revision))
result))
(defun mo-git-blame-parse-blame-line ()
(save-excursion
(save-match-data
(beginning-of-line)
(cond ((looking-at "^\\([a-f0-9]+\\) +\\(([^)]+)\\) *$")
(list :hash (buffer-substring (match-beginning 1) (match-end 1))
:file-name (plist-get mo-git-blame-vars :file-name)
:timestamp (buffer-substring (match-beginning 2) (match-end 2))))
((looking-at "^\\([a-f0-9]+\\) +\\(([^)]+)\\) +\\(.+\\)")
(list :hash (buffer-substring (match-beginning 1) (match-end 1))
:file-name (buffer-substring (match-beginning 3) (match-end 3))
:timestamp (buffer-substring (match-beginning 2) (match-end 2))))
(t (error "Not a 'git blame' line"))))))
(defun mo-git-blame-revision-at-point ()
(plist-get (mo-git-blame-parse-blame-line) :hash))
(defun mo-git-blame-log-for-revision (revision)
(let ((file-name (plist-get mo-git-blame-vars :file-name))
(buffer (mo-git-blame-get-output-buffer)))
(with-current-buffer buffer
(erase-buffer)
(mo-git-blame-run "log" revision "--" file-name)
(goto-char (point-min)))
(display-buffer buffer)))
(defun mo-git-blame-log-for-revision-at ()
"Calls 'git log' for revision in the current line."
(interactive)
(mo-git-blame-log-for-revision (mo-git-blame-revision-at-point)))
(defun mo-git-blame-log-for-current-revision ()
"Calls 'git log' for the buffer's current revision and file."
(interactive)
(mo-git-blame-log-for-revision (plist-get mo-git-blame-vars :current-revision)))
(defun mo-git-blame-show-revision (revision)
(let ((buffer (mo-git-blame-get-output-buffer)))
(with-current-buffer buffer
(erase-buffer)
(mo-git-blame-run "show" revision)
(goto-char (point-min))
(diff-mode))
(display-buffer buffer)))
(defun mo-git-blame-show-revision-at ()
"Calls 'git show' for the revision in the current line."
(interactive)
(mo-git-blame-show-revision (mo-git-blame-revision-at-point)))
(defun mo-git-blame-show-current-revision ()
"Calls 'git show' for the current revision."
(interactive)
(mo-git-blame-show-revision (plist-get mo-git-blame-vars :current-revision)))
(defun mo-git-blame-content-for-revision-at ()
"Calls 'git cat-file' for the revision in the current line."
(interactive)
(let ((info (mo-git-blame-parse-blame-line))
(buffer (mo-git-blame-get-output-buffer)))
(with-current-buffer buffer
(erase-buffer)
(mo-git-blame-run "cat-file" "blob" (concat (plist-get info :hash) ":" (plist-get info :file-name)))
(goto-char (point-min)))
(display-buffer buffer)))
(defun mo-git-blame-overwrite-file-with-revision (revision)
(let ((file-name (plist-get mo-git-blame-vars :original-file-name)))
(if (yes-or-no-p (format "Do you really want to overwrite %s with revision %s " file-name revision))
(progn
(find-file (concat (plist-get mo-git-blame-vars :top-dir) file-name))
(erase-buffer)
(mo-git-blame-run "cat-file" "blob" (concat revision ":" file-name))
(goto-char (point-min))))))
(defun mo-git-blame-overwrite-file-with-revision-at ()
"Calls 'git cat-file' for the revision in the current line and overwrites
the original file's content. The file is not saved but left modified in an
open buffer."
(interactive)
(mo-git-blame-overwrite-file-with-revision (mo-git-blame-revision-at-point)))
(defun mo-git-blame-overwrite-file-with-current-revision ()
"Calls 'git cat-file' for the current revision and overwrites
the original file's content. The file is not saved but left modified in an
open buffer."
(interactive)
(mo-git-blame-overwrite-file-with-revision (plist-get mo-git-blame-vars :current-revision)))
(defun mo-git-blame-reblame-for-ancestor-of-revision-at (&optional arg)
"Calls 'git blame' for the ancestor of the revision in the current line.
With a numeric prefix argument ARG only the ARG lines before and
after point are blamed by using git blame's `-L'
option. Otherwise the whole file is blamed."
(interactive "P")
(mo-git-blame-reblame-for-specific-revision (mo-git-blame-parse-rev (concat (plist-get (mo-git-blame-parse-blame-line) :hash) "~")) arg))
(defun mo-git-blame-reblame-for-ancestor-of-current-revision (&optional arg)
"Calls 'git blame' for the ancestor of the current revision.
With a numeric prefix argument ARG only the ARG lines before and
after point are blamed by using git blame's `-L'
option. Otherwise the whole file is blamed."
(interactive "P")
(mo-git-blame-reblame-for-specific-revision (mo-git-blame-parse-rev (concat (plist-get mo-git-blame-vars :current-revision) "~")) arg))
(defun mo-git-blame-reblame-for-revision-at (&optional arg)
"Calls 'git blame' for the revision in the current line.
With a numeric prefix argument ARG only the ARG lines before and
after point are blamed by using git blame's `-L'
option. Otherwise the whole file is blamed."
(interactive "P")
(let* ((info (mo-git-blame-parse-blame-line))
(revision (plist-get info :hash)))
(if (string= revision (plist-get mo-git-blame-vars :current-revision))
(error "Already showing this revision"))
(mo-git-blame-file (concat (plist-get mo-git-blame-vars :top-dir) (plist-get info :file-name)) revision (plist-get mo-git-blame-vars :original-file-name) arg)))
(defun mo-git-blame-reblame-for-specific-revision (&optional revision arg)
"Calls 'git blame' for a specific REVISION.
With a numeric prefix argument ARG only the ARG lines before and
after point are blamed by using git blame's `-L'
option. Otherwise the whole file is blamed."
(interactive "sRevision: \nP")
(setq revision (mo-git-blame-parse-rev revision))
(if (string= revision (plist-get mo-git-blame-vars :current-revision))
(error "Already showing this revision"))
(mo-git-blame-file (concat (plist-get mo-git-blame-vars :top-dir) (plist-get mo-git-blame-vars :file-name)) revision (plist-get mo-git-blame-vars :original-file-name) arg))
(defun mo-git-blame-reblame-for-prior-revision (&optional arg)
"Calls 'git blame' for the revision shown before the current
one (see `prior revisions' in the info output of
`mo-git-blame-display-info').
With a numeric prefix argument ARG only the ARG lines before and
after point are blamed by using git blame's `-L'
option. Otherwise the whole file is blamed."
(interactive "P")
(let ((rev-list (plist-get mo-git-blame-vars :prior-revisions))
revision-plist)
(unless rev-list
(error "No revision shown prior to the current one"))
(setq revision-plist (car rev-list))
(mo-git-blame-file (plist-get revision-plist :full-file-name)
(plist-get revision-plist :revision)
(plist-get mo-git-blame-vars :original-file-name)
arg)))
(defun mo-git-blame-display-info ()
"Displays short information about the current revision."
(interactive)
(let* ((buffer (mo-git-blame-get-output-buffer))
(vars mo-git-blame-vars)
(prior-revs (plist-get vars :prior-revisions))
(prior-revs-str (if prior-revs
(reduce (lambda (joined element) (concat (or joined "") (if joined " " "") element))
(mapcar (lambda (element) (plist-get element :revision))
prior-revs))
"none")))
(with-current-buffer buffer
(erase-buffer)
(insert (format "Current revision: %s\n" (plist-get vars :current-revision))
(format "Prior revisions: %s\n" prior-revs-str)
(format "Git repository: %s\n" (plist-get vars :top-dir))
(format "Original file name: %s\n" (file-relative-name (plist-get vars :original-file-name)
(plist-get vars :top-dir)))
(format "Current file name: %s\n" (plist-get vars :file-name)))
(goto-char (point-min)))
(display-buffer buffer)))
(defun mo-git-blame-number-of-content-lines ()
(with-current-buffer (plist-get mo-git-blame-vars :content-buffer)
(save-excursion
(goto-char (point-max))
(line-number-at-pos))))
(defun mo-git-blame-mode ()
"Show the output of 'git blame' and the content of the file in
two frames side-by-side. Allows iterative re-blaming for specific
revisions. Can show the output of 'git log' and 'git show'. Can
overwrite the file with the content of specific revisions by
calling 'git cat-file blob ...'.
Use 'mo-git-blame-current' interactively or 'mo-git-blame-file'
from elisp.
\\{mo-git-blame-mode-map}"
(setq major-mode 'mo-git-blame-mode
mode-name "MoGitBlame"
mode-line-process ""
truncate-lines t)
(use-local-map mo-git-blame-mode-map))
(defun mo-git-blame-run-blame-normally (start-line lines-to-blame)
(let* ((num-content-lines (mo-git-blame-number-of-content-lines))
(num-lines-to-append (if (and start-line
(< (+ start-line lines-to-blame)
num-content-lines))
(- num-content-lines start-line lines-to-blame)))
args i)
(if (and start-line (> start-line 1))
(dotimes (i (1- start-line))
(insert "\n")))
(setq args (list (plist-get mo-git-blame-vars :current-revision) "--" (plist-get mo-git-blame-vars :file-name)))
(if start-line
(setq args (append (list "-L" (format "%d,+%d" start-line lines-to-blame))
args)))
(apply 'mo-git-blame-run "blame" args)
(if num-lines-to-append
(dotimes (i num-lines-to-append)
(insert "\n")))))
(defun mo-git-blame-run-blame-incrementally (start-line lines-to-blame)
(let* ((num-content-lines (mo-git-blame-number-of-content-lines))
i)
(dotimes (i (1- num-content-lines))
(insert "\n"))
(setq args (list "--incremental" (plist-get mo-git-blame-vars :current-revision) "--" (plist-get mo-git-blame-vars :file-name)))
(if start-line
(setq args (append (list "-L" (format "%d,+%d" start-line lines-to-blame))
args)))
(mo-git-blame-assert-not-running)
(apply 'mo-git-blame-run* "blame" args)))
(defun mo-git-blame-init-blame-buffer (start-line lines-to-blame)
(if mo-git-blame-incremental
(mo-git-blame-run-blame-incrementally start-line lines-to-blame)
(mo-git-blame-run-blame-normally start-line lines-to-blame))
(goto-char (point-min))
(save-match-data
(while (re-search-forward "^\\([a-f0-9]+\\) +\\(([^)]+)\\) \\(.*\\)" nil t)
(replace-match "\\1 \\2" nil nil))
(goto-char (point-min))
(while (re-search-forward "^\\([a-f0-9]+\\) +\\([^ ]+\\) +\\(([^)]+)\\) \\(.*\\)" nil t)
(replace-match "\\1 \\3 \\2" nil nil))
(goto-char (point-min))
(while (re-search-forward " +[0-9]+)" nil t)
(replace-match ")" nil nil)))
(toggle-read-only t)
(goto-char (point-min))
(set (make-local-variable 'line-move-visual) nil))
(defun mo-git-blame-init-content-buffer ()
(let ((vars mo-git-blame-vars))
(rename-buffer (concat "*mo-git-blame:" (file-name-nondirectory (plist-get vars :full-file-name)) ":" (plist-get vars :current-revision) "*"))
(setq buffer-file-name (file-name-nondirectory (plist-get vars :full-file-name))
default-directory (plist-get vars :top-dir))
(mo-git-blame-run "cat-file" "blob" (concat (plist-get vars :current-revision) ":" (plist-get vars :file-name)))
(normal-mode)
(use-local-map mo-git-blame-content-mode-map)
(font-lock-fontify-buffer)
(toggle-read-only t)
(set-buffer-modified-p nil)
(setq truncate-lines t)
(set (make-local-variable 'mo-git-blame-vars) vars)
(set (make-local-variable 'line-move-visual) nil)))
(defun mo-git-blame-read-file-name ()
"Calls `read-file-name' or `ido-read-file-name' depending on
the value of `mo-git-blame-use-ido'."
(let ((the-func (cond ((eq mo-git-blame-use-ido 'always)
(require 'ido)
'ido-read-file-name)
((and (eq mo-git-blame-use-ido 'if-available)
(functionp 'ido-read-file-name))
'ido-read-file-name)
(t 'read-file-name))))
(funcall the-func "File for 'git blame': " nil nil t)))
;;;###autoload
(defun mo-git-blame-file (&optional file-name revision original-file-name num-lines-to-blame)
"Calls `git blame' for REVISION of FILE-NAME or `HEAD' if
REVISION is not given. Initializes the two windows that will show
the output of 'git blame' and the content.
If FILE-NAME is missing it will be read with `find-file' in
interactive mode.
ORIGINAL-FILE-NAME defaults to FILE-NAME if not given. This is
used for tracking renaming and moving of files during iterative
re-blaming.
With a numeric prefix argument or with NUM-LINES-TO-BLAME only
the NUM-LINES-TO-BLAME lines before and after point are blamed by
using git blame's `-L' option. Otherwise the whole file is
blamed."
(interactive)
(mo-git-blame-assert-not-running)
(unless mo-git-blame--wincfg
(setq mo-git-blame--wincfg (current-window-configuration)))
(let* ((file-name (or file-name (mo-git-blame-read-file-name)))
(has-blame-vars (local-variable-p 'mo-git-blame-vars))
(the-raw-revision (or revision "HEAD"))
(the-revision (if (string= the-raw-revision "HEAD")
(mo-git-blame-parse-rev "HEAD")
the-raw-revision))
(base-name (concat (file-name-nondirectory file-name) "@" the-revision))
(blame-buffer (get-buffer-create "*mo-git-blame*"))
(content-buffer-name (concat "*mo-git-blame:" (file-name-nondirectory file-name) ":" the-revision "*"))
(content-buffer (if has-blame-vars
(plist-get mo-git-blame-vars :content-buffer)
(get-buffer-create content-buffer-name)))
(top-dir (mo-git-blame-get-top-dir (file-name-directory file-name)))
(relative-file-name (file-relative-name file-name top-dir))
(blame-window (selected-window))
(prior-vars (if has-blame-vars mo-git-blame-vars))
(line-to-go-to (line-number-at-pos))
(lines-to-blame (or num-lines-to-blame
(if (and current-prefix-arg (> (prefix-numeric-value current-prefix-arg) 0))
(prefix-numeric-value current-prefix-arg))))
content-window the-buffer prior-revisions start-line)
(switch-to-buffer blame-buffer)
(setq prior-revisions (if prior-vars (plist-get prior-vars :prior-revisions)))
(setq prior-revisions
(if (and prior-revisions (string= the-revision (plist-get (car prior-revisions) :revision)))
(cdr prior-revisions)
(if prior-vars
(cons (list :full-file-name (plist-get prior-vars :full-file-name)
:revision (plist-get prior-vars :current-revision))
prior-revisions))))
(if (window-full-width-p)
(split-window-horizontally mo-git-blame-blame-window-width))
(select-window (setq content-window (next-window)))
(switch-to-buffer content-buffer)
(select-window blame-window)
(dolist (the-buffer (list blame-buffer content-buffer))
(with-current-buffer the-buffer
(toggle-read-only 0)
(kill-all-local-variables)
(buffer-disable-undo)
(erase-buffer)
(setq default-directory top-dir)
(set (make-local-variable 'mo-git-blame-vars)
(list :top-dir top-dir
:file-name relative-file-name
:full-file-name file-name
:original-file-name (or original-file-name file-name)
:current-revision the-revision
:prior-revisions prior-revisions
:blame-buffer blame-buffer
:blame-window blame-window
:content-buffer content-buffer
:content-window content-window))))
(with-current-buffer content-buffer
(mo-git-blame-init-content-buffer))
(when lines-to-blame
(setq start-line (max 1 (- line-to-go-to lines-to-blame))
lines-to-blame (1+ (- (+ line-to-go-to lines-to-blame)
start-line))))
(with-current-buffer blame-buffer
(mo-git-blame-mode)
(mo-git-blame-init-blame-buffer start-line lines-to-blame))
(mo-git-blame-goto-line line-to-go-to)
(add-to-list 'window-scroll-functions 'mo-git-blame-window-scrolled)))
(defvar mo-git-blame-scroll-info
nil
"Information which window to scroll and where to scroll to.")
(defun mo-git-blame-window-scrolled (window new-start-pos)
(if (and window
(eq window (selected-window))
(local-variable-p 'mo-git-blame-vars))
(let* ((vars (with-current-buffer (window-buffer window) mo-git-blame-vars))
(start-line (line-number-at-pos new-start-pos))
(point-line (line-number-at-pos (window-point window)))
(window-to-scroll (if (eq window (plist-get vars :blame-window))
(plist-get vars :content-window)
(plist-get vars :blame-window))))
(setq mo-git-blame-scroll-info (list :window-to-scroll window-to-scroll
:start-line start-line
:point-line point-line))
(run-at-time "0 sec" nil 'mo-git-blame-update-other-window-after-scrolling))))
(defun mo-git-blame-update-other-window-after-scrolling ()
(if mo-git-blame-scroll-info
(let ((window (plist-get mo-git-blame-scroll-info :window-to-scroll))
new-start-pos)
(with-selected-window window
(with-current-buffer (window-buffer window)
(goto-char (point-min))
(setq new-start-pos (line-beginning-position (plist-get mo-git-blame-scroll-info :start-line)))
(goto-char (point-min))
(goto-char (line-beginning-position (plist-get mo-git-blame-scroll-info :point-line)))
(set-window-start window new-start-pos)))
(setq mo-git-blame-scroll-info nil))))
(defun mo-git-blame-quit ()
"Kill the mo-git-blame buffers."
(interactive)
(setq window-scroll-functions (remq 'mo-git-blame-window-scrolled window-scroll-functions))
(let ((buffer))
(dolist (buffer (buffer-list))
(if (string-match-p "^\\*mo-git-blame" (buffer-name buffer))
(kill-buffer buffer))))
(set-window-configuration mo-git-blame--wincfg)
(setq mo-git-blame--wincfg nil))
(defun mo-git-blame-display-content-buffer ()
"Show the content buffer in the content window."
(interactive)
; Declare buffer here because mo-git-blame-vars might not be available in the other buffer.
(let ((buffer (plist-get mo-git-blame-vars :content-buffer))
(line-num (line-number-at-pos)))
(mo-git-blame-goto-line-markless line-num)
(recenter)
(with-selected-window (plist-get mo-git-blame-vars :content-window)
(switch-to-buffer buffer)
(mo-git-blame-goto-line-markless line-num)
(recenter))))
(defun mo-git-blame-other-buffer ()
(plist-get mo-git-blame-vars
(if (eq (current-buffer) (plist-get mo-git-blame-vars :blame-buffer))
:content-buffer
:blame-buffer)))
(defun mo-git-blame-goto-line-markless (line)
(goto-char (point-min))
(goto-char (line-beginning-position line)))
(defun mo-git-blame-goto-line (line)
"Goto a line in both the blame and the content buffer."
(interactive "nGoto line: ")
(with-selected-window (plist-get mo-git-blame-vars :blame-window)
(mo-git-blame-goto-line-markless line))
(with-selected-window (plist-get mo-git-blame-vars :content-window)
(mo-git-blame-goto-line-markless line)))
;;;###autoload
(defun mo-git-blame-current ()
"Calls `mo-git-blame-file' for HEAD for the current buffer."
(interactive)
(if (null (buffer-file-name))
(error "The current buffer is not associated with a file."))
(mo-git-blame-file (buffer-file-name)))
(provide 'mo-git-blame)
;; Leave this in for debugging purposes:
;; (global-set-key [?\C-c ?i ?b] (lambda () (interactive) (let ((mo-git-blame-incremental t)) (mo-git-blame-current))))
;; (global-set-key [?\C-c ?i ?B] (lambda () (interactive) (let ((mo-git-blame-incremental nil)) (mo-git-blame-current))))
;;; mo-git-blame.el ends here