aboutsummaryrefslogtreecommitdiffstats
path: root/local-lib/git-undo.el
diff options
context:
space:
mode:
Diffstat (limited to 'local-lib/git-undo.el')
-rw-r--r--local-lib/git-undo.el152
1 files changed, 152 insertions, 0 deletions
diff --git a/local-lib/git-undo.el b/local-lib/git-undo.el
new file mode 100644
index 0000000..e6eeb34
--- /dev/null
+++ b/local-lib/git-undo.el
@@ -0,0 +1,152 @@
1;;; git-undo.el Foundation, Inc.
2
3;; Author: John Wiegley <johnw@newartisans.com>
4;; Created: 20 Nov 2017
5;; Version: 0.1
6
7;; Keywords: git diff history log undo
8;; X-URL: https://github.com/jwiegley/git-undo
9
10;; This program is free software; you can redistribute it and/or
11;; modify it under the terms of the GNU General Public License as
12;; published by the Free Software Foundation; either version 2, or (at
13;; your option) any later version.
14
15;; This program is distributed in the hope that it will be useful, but
16;; WITHOUT ANY WARRANTY; without even the implied warranty of
17;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18;; General Public License for more details.
19
20;; You should have received a copy of the GNU General Public License
21;; along with GNU Emacs; see the file COPYING. If not, write to the
22;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
23;; Boston, MA 02111-1307, USA.
24
25;;; Commentary:
26
27;; Select a region and then use M-x git-undo to revert changes in that region
28;; to the most recent Git historical version. Use C-x z to repeatdly walk back
29;; through the history. M-x git-undo-browse will let you see the history of
30;; changes in a separate buffer.
31
32;;; Code:
33
34(require 'cl)
35
36(defgroup git-undo nil
37 "Successively undo a buffer region using Git history"
38 :group 'emacs)
39
40(defvar git-undo--region-start)
41(defvar git-undo--region-end)
42(defvar git-undo--history)
43
44(defun git-undo--apply-diff (hunk)
45 (with-temp-buffer
46 (insert hunk)
47 (goto-char (point-min))
48 (while (not (eobp))
49 (pcase (char-after)
50 (?\ (delete-char 1) (forward-line))
51 (?\+ (delete-char 1) (forward-line))
52 (?\- (delete-region (point) (and (forward-line) (point))))
53 (t (delete-region (point) (point-max)))))
54 (buffer-string)))
55
56(defun git-undo--replace-region ()
57 (goto-char git-undo--region-start)
58 (delete-region git-undo--region-start git-undo--region-end)
59 (if (null git-undo--history)
60 (error "There is no more Git history to undo")
61 (insert (git-undo--apply-diff (car git-undo--history)))
62 (setq git-undo--history (cdr git-undo--history)))
63 (goto-char git-undo--region-end))
64
65(defun git-undo--compute-offsets (start end)
66 "Taking uncommitted changes into account, find the location in
67Git history for a given line."
68 (let ((file-name (buffer-file-name))
69 (buffer-lines (line-number-at-pos (point-max))))
70 (with-temp-buffer
71 (shell-command
72 (format "git --no-pager diff -U%d HEAD -- %s"
73 buffer-lines (file-name-nondirectory file-name))
74 (current-buffer))
75 (goto-char (point-min))
76 (re-search-forward "^@@")
77 (forward-line)
78 (let ((adjustment 0)
79 (line 1)
80 adjusted-start
81 adjusted-end)
82 (while (not (eobp))
83 (pcase (char-after)
84 (?\+ (setq adjustment (1- adjustment)))
85 (?\- (setq adjustment (1+ adjustment)))
86 (t (setq line (1+ line))))
87 (when (= (- start adjustment) line)
88 (setq adjusted-start (+ start adjustment)))
89 (when (= (- end adjustment) line)
90 (setq adjusted-end (+ end adjustment))
91 (goto-char (point-max)))
92 (forward-line))
93 (cons (1+ adjusted-start) adjusted-end)))))
94
95(defun git-undo--build-history (start end)
96 (let ((file-name (buffer-file-name)))
97 (destructuring-bind (start-line . end-line)
98 (git-undo--compute-offsets (line-number-at-pos start)
99 (1- (line-number-at-pos end)))
100 (with-temp-buffer
101 (message "Retrieving Git history for lines %d to %d..."
102 start-line end-line)
103 (shell-command
104 (format "git --no-pager log --no-expand-tabs -p -L%d,%d:%s"
105 start-line end-line
106 (file-name-nondirectory file-name))
107 (current-buffer))
108 (message "")
109 (goto-char (point-min))
110 (let ((commit t) history)
111 (while (and commit
112 (re-search-forward "^@@" nil t)
113 (forward-line))
114 (delete-region (point-min) (point))
115 (setq commit (and (re-search-forward "^commit " nil t)
116 (match-beginning 0)))
117 (setq history (cons (buffer-substring-no-properties
118 (point-min) (or commit (point-max)))
119 history)))
120 (nreverse history))))))
121
122;;;###autoload
123(defun git-undo (&optional start end)
124 "Undo Git-historical changes in the region from START to END."
125 (interactive "r")
126 (if (eq last-command 'git-undo)
127 (git-undo--replace-region)
128 (set (make-local-variable 'git-undo--region-start)
129 (copy-marker start nil))
130 (set (make-local-variable 'git-undo--region-end)
131 (copy-marker end t))
132 (set (make-local-variable 'git-undo--history)
133 (git-undo--build-history start end))
134 (git-undo--replace-region)))
135
136;;;###autoload
137(defun git-undo-browse (&optional start end)
138 "Undo Git-historical changes in the region from START to END."
139 (interactive "r")
140 (let ((history (git-undo--build-history start end)))
141 (display-buffer
142 (with-current-buffer
143 (get-buffer-create "*Git Region History*")
144 (delete-region (point-min) (point-max))
145 (dolist (entry history)
146 (insert (git-undo--apply-diff entry)
147 #("-----\n" 0 5 (face bold))))
148 (delete-region (- (point) 6) (point))
149 (goto-char (point-min))
150 (current-buffer)))))
151
152;;; git-undo.el ends here