add mo-git-blame
authorJoerg Jaspert <joerg@debian.org>
Sun, 3 Mar 2013 13:35:10 +0000 (14:35 +0100)
committerJoerg Jaspert <joerg@debian.org>
Sun, 3 Mar 2013 13:35:10 +0000 (14:35 +0100)
elisp/mo-git-blame/ChangeLog [new file with mode: 0644]
elisp/mo-git-blame/README [new file with mode: 0644]
elisp/mo-git-blame/mo-git-blame.el [new file with mode: 0644]

diff --git a/elisp/mo-git-blame/ChangeLog b/elisp/mo-git-blame/ChangeLog
new file mode 100644 (file)
index 0000000..1373f86
--- /dev/null
@@ -0,0 +1,14 @@
+2011-05-04  Moritz Bunkus  <moritz@bunkus.org>
+
+       * mo-git-blame.el (mo-git-blame-process-filter): Avoid hanging
+       Emacs if `truncate-partial-width-windows' is set to nil. Fixes
+       issue #1.
+
+2010-12-28  Moritz Bunkus  <moritz@bunkus.org>
+
+       * mo-git-blame.el: Scroll both mo-git-blame buffers automatically
+       after one of them changes its position. Uses
+       `window-scroll-functions' instead of `scroll-all-mode' which
+       scrolls too many buffers and doesn't work with e.g. searching.
+
+
diff --git a/elisp/mo-git-blame/README b/elisp/mo-git-blame/README
new file mode 100644 (file)
index 0000000..b44eb63
--- /dev/null
@@ -0,0 +1,151 @@
+mo-git-blame -- An interactive, interative 'git blame' mode for Emacs
+
+Copyright
+---------
+
+Copyright (C) 2009  Moritz Bunkus <moritz@bunkus.org>
+
+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)
+
+Optionally bind keys to these functions, e.g.
+
+(global-set-key [?\C-c ?g ?c] 'mo-git-blame-current)
+(global-set-key [?\C-c ?g ?f] 'mo-git-blame-file)
+
+Usage
+-----
+
+There are two ways to invoke it: `mo-git-blame-file' which lets you
+select a file for which 'git blame' should be called and
+`mo-git-blame-current' which does the same for the current buffer's
+associated file.
+
+Once the mode starts it splits the window vertically. The left one
+is filled with the blame output while the right one is filled with the
+content at the current revision. Syntax highlighting is enabled for
+the content window. The content buffer's name will contain the file
+name and the current revision.
+
+mo-git-blame works with three different buffers:
+
+1. the `blame' buffer which contains the output of `git blame' and
+which is the main buffer from which actions can be invoked
+
+2. the `content' buffer which shows the file content at the current
+revision. This buffer has syntax highlighting turned on. Only a few
+actions can be invoked from this buffer (e.g. `q' for exiting
+mo-git-blame).
+
+3. the `output' buffer which contains the output of several git
+commands, e.g. 'git show' or 'git cat-file'.
+
+Normally the `blame' and the `content' buffer are shown
+side-by-side. If the user requests additional information then the
+`output' buffer is shown instead of the `content' buffer. The user can
+return to the `content' buffer with <TAB>.
+
+You can invoke several actions from the blame buffer with single key
+strokes. These include but are not limited to:
+
+a   -- Call 'git blame' for the file for the first ancestor of the
+       revision listed in the current line. Sets the content buffer to
+       contain the file content at the new revision.
+A   -- Call 'git blame' for the file for the first ancestor of the
+       current revision. Sets the content buffer to contain the file
+       content at the new revision.
+b   -- Call 'git blame' for the file for the revision listed in the
+       current line. Sets the content buffer to contain the file
+       content at the new revision.
+B   -- Call 'git blame' for the file for a specific revision read from
+       the minibuffer.
+c   -- Call 'git cat-file blob ...' for the revision listed in the
+       current line and show the output in the `output' buffer. The
+       output will not have syntax highlighting.
+i   -- Display the current state information (current revision, git
+       repository path etc) in the right window.
+l   -- Call 'git log' for the revision listed in the current line and
+       show the output in the `output' buffer.
+L   -- Call 'git log' for the current revision and show the output in
+       the `output' buffer.
+p   -- Call 'git blame' for the file for the revision that was shown
+       prior to the current one. Works only if you've used `b'
+       before.
+o   -- Overwrite the file with the content of the revision listed in
+       the current line. Asks for confirmation before actually
+       overwriting the file.
+O   -- Overwrite the file with the content of the current
+       revision. Asks for confirmation before actually overwriting the
+       file.
+q   -- Exit mo-git-blame and kill all its buffers.
+s   -- Call 'git show' for the revision listed in the current line and
+       show the output in the `output' buffer.
+S   -- Call 'git show' for the current revision and show the output in
+       the `output' buffer.
+TAB -- Re-display the `content' buffer in the right window if it has
+       replaced with the `output' buffer.
+RET -- Same as `s'.
+
+Customizing
+-----------
+
+There are several variables that can be customized. They belong to the
+group `mo-git-blame' and can be customized with
+
+(customize-group 'mo-git-blame)
+
+These variables include:
+
+* `mo-git-blame-git-executable' defaults to `git' and should point to
+  your `git' executable if it is not in your path.
+* `mo-git-blame-blame-window-width' defaults to 45 and determines the
+  initial width of the `blame' window.
+* `mo-git-blame-use-ido' determines whether or not the `ido' package
+  is used for various interactive lookups.
+
+Bugs, feature requests, contact
+-------------------------------
+
+You can reach me via email at Moritz Bunkus <moritz@bunkus.org>. I'm
+always grateful for bug reports and usually open to feature
+suggestions as far as my time permits.
+
+You can also file an issue on github's issue tracker:
+
+https://github.com/mbunkus/mo-git-blame/issues
+
+Patches, source code
+--------------------
+
+The latest version can always be retrieved from my Git repository:
+
+git clone git://github.com/mbunkus/mo-git-blame.git
+
+If you want to send patches: Yes! By all means! Please do so! Please
+note that I'm using wide windows and do not like forced wrapping
+around 80 columns. I also don't like tabs. Thanks.
diff --git a/elisp/mo-git-blame/mo-git-blame.el b/elisp/mo-git-blame/mo-git-blame.el
new file mode 100644 (file)
index 0000000..750eed8
--- /dev/null
@@ -0,0 +1,768 @@
+;;; mo-git-blame.el --- An interactive, iterative 'git blame' mode for Emacs
+
+;; Copyright (C) 2009, 2010  Moritz Bunkus <moritz@bunkus.org>
+;; Copyright (C) 2010  Štěpán Němec <stepnem@gmail.com>
+
+;; Author: Moritz Bunkus <moritz@bunkus.org>
+;; Maintainer: Moritz Bunkus <moritz@bunkus.org>
+;; Version: 0.1.0
+;; Keywords: tools
+
+;; 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 (concat (or (file-remote-p cwd) "") 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 'shell-command
+         (apply 'concat mo-git-blame-git-executable
+                (mapcar (lambda (arg)
+                          (concat " " (shell-quote-argument arg)))
+                        args))
+         (current-buffer) nil)
+  (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