Add rudimentary support for git diff, blame, and log.
[slh] / sy-git.el
1 ;; sy-git.el --- A couple of nice git tools   -*- Emacs-Lisp -*-
2
3 ;; Copyright (C) 2015 - 2017 Steve Youngs
4
5 ;; Author:     Steve Youngs <steve@sxemacs.org>
6 ;; Maintainer: Steve Youngs <steve@sxemacs.org>
7 ;; Created:    <2015-07-05>
8 ;; Time-stamp: <Monday Oct 16, 2017 09:05:17 steve>
9 ;; Homepage:   http://git.sxemacs.org/slh
10 ;; Keywords:   git, tools, convenience
11
12 ;; This file is part of SLH (Steve's Lisp Hacks).
13
14 ;; Redistribution and use in source and binary forms, with or without
15 ;; modification, are permitted provided that the following conditions
16 ;; are met:
17 ;;
18 ;; 1. Redistributions of source code must retain the above copyright
19 ;;    notice, this list of conditions and the following disclaimer.
20 ;;
21 ;; 2. Redistributions in binary form must reproduce the above copyright
22 ;;    notice, this list of conditions and the following disclaimer in the
23 ;;    documentation and/or other materials provided with the distribution.
24 ;;
25 ;; 3. Neither the name of the author nor the names of any contributors
26 ;;    may be used to endorse or promote products derived from this
27 ;;    software without specific prior written permission.
28 ;;
29 ;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR
30 ;; IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
31 ;; WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
32 ;; DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
33 ;; FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
34 ;; CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
35 ;; SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
36 ;; BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
37 ;; WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
38 ;; OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
39 ;; IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
40
41 ;;; Commentary:
42 ;; 
43 ;;   This is the beginnings of some convenience tools to use with git
44 ;;   from within SXEmacs.
45 ;;
46 ;;   Presently, all that is here is a 'add-log' function that
47 ;;   lets you write commit logs in a similar format to that of
48 ;;   `add-change-log-entry'.  It is globally bound to `C-x G a'.
49 ;;   See: `sy-git-add-log-entry'.
50 ;;
51 ;;   [2017-10-16 08:59]: Added rudimentary support for git-diff,
52 ;;   git-blame, and git-log.  For examining changes you can use
53 ;;   either plain `diff-mode' with `sy-git-diff', or Ediff with
54 ;;   `sy-git-ediff'.
55
56 ;;; Todo:
57 ;;
58 ;;     o Implement a variation of `patch-to-change-log'.
59 ;;        [2017-10-15 15:09]: Turns out that this isn't really needed
60 ;;        as you can invoke `sy-git-add-log-entry' directly from a diff.
61
62 ;;; Code:
63 ;; Need VC, ediff, and diff-mode
64 (vc-load-vc-hooks)
65 (add-to-list 'vc-handled-backends 'GIT)
66 (require 'ediff)
67 (require 'diff-mode)
68
69 ;; diff
70 (defun sy-git-diff ()
71   "Show a diff of the current file against HEAD."
72   (interactive)
73   (vc-diff nil))
74
75 (defun sy-git-ediff ()
76   "Run ediff-buffers on the working file and the HEAD version."
77   (interactive)
78   (let* ((bufferA (file-basename (buffer-file-name)))
79          (bufferB (concat bufferA ".~HEAD~")))
80     (progn
81       (vc-version-other-window "HEAD")
82       (ediff-buffers bufferA bufferB))))
83
84 ;; blame
85 (defun sy-git-blame ()
86   "Display git blame for the current file.
87
88 If the region is active the output will be for just the lines of the
89 file within the region."
90   (interactive)
91   (let ((gitcmd "git blame ")
92         beg end)
93     (when (region-active-p)
94       (setq beg (line-number (region-beginning))
95             end (line-number (region-end))))
96     (and beg end
97          (setq gitcmd (concat gitcmd (format "-L%d,%d " beg end))))
98     (setq gitcmd (concat gitcmd (file-basename (buffer-file-name))))
99     (with-electric-help
100      #'(lambda ()
101          (setq truncate-lines t)
102          (insert
103           (with-temp-buffer
104             (insert (shell-command-to-string gitcmd))
105             (buffer-string (current-buffer)))))
106      "*GIT Blame*")))
107
108 ;; log
109 (defun sy-git-log (all)
110   "Display git log of current file.
111
112 With prefix arg, ALL, display the log for the entire repo."
113   (interactive "p")
114   (let ((gitcmd "git log --format=fuller"))
115     (unless current-prefix-arg
116       (setq gitcmd
117             (concat gitcmd
118                     (format " %s" (file-basename (buffer-file-name))))))
119     (with-electric-help
120      #'(lambda ()
121          (insert
122           (with-temp-buffer
123             (insert (shell-command-to-string gitcmd))
124             (buffer-string (current-buffer)))))
125      "*GIT Log*")))
126
127 (defun sy-git-check-hook (hook)
128   "Return non-nil when HOOK script exists and is usable.
129
130 By \"usable\" we mean for `sy-git-add-log-entry'."
131   (let ((hookname (file-basename hook)))
132     (when (file-exists-p hook)
133       (with-temp-buffer
134         (insert-file-contents-literally hook)
135         (goto-char (point-min))
136         (cond
137          ((equal hookname "commit-msg")
138           (re-search-forward (regexp-quote "sed -i '/^#/d'") nil t))
139          ((equal hookname "post-commit")
140           (re-search-forward (regexp-quote "rm -f ${LOG}") nil t))
141          (t nil))))))
142
143 (defun sy-git-add-log-entry (&optional newlog)
144   "*A wrapper for `add-change-log-entry'.
145
146 Optional prefix argument, NEWLOG, forces a new log file to be
147 created. Use this if you need to start over.
148
149 To commit your changes with the log that this function creates use:
150
151   git commit -F ++log
152
153 This function allows you to create git commit logs in a similar format
154 to that used by `add-change-log-entry'.  Some commented instructions
155 are added to the top of the log which you should either delete yourself
156 prior to committing, or have a hook do it automatically \(preferred\).
157
158 Hooks: 
159 2 hooks will make using this function a lot simpler and automatic.
160 A 'commit-msg' hook, and a 'post-commit' hook.  They reside in
161 '$repo/.git/hooks/'.
162
163 Example commit-msg:
164
165   #!/bin/sh
166   # Delete lines beginning with '#'.
167   sed -i '/^#/d' \"$1\" || {
168       echo >&2 Commit aborted by commit-msg hook
169       exit 1
170   }
171   # End commit-msg
172
173 Example post-commit:
174
175   #!/bin/sh
176   # Delete log file after successful commit.
177   LOG=$(git rev-parse --show-toplevel)/++log
178   [ -f ${LOG} ] && rm -f ${LOG}
179   # End post-commit
180
181 "
182   (interactive "p")
183   (let* ((topd (substring (shell-command-to-string
184                            "git rev-parse --show-toplevel") 0 -1))
185          (logfile (expand-file-name "++log" topd))
186          (hookd (paths-construct-path `(,topd ".git" "hooks")))
187          (msg-hook (expand-file-name "commit-msg" hookd))
188          (commit-hook (expand-file-name "post-commit" hookd))
189          (add-log-full-name (substring (shell-command-to-string
190                                         "git config user.name") 0 -1))
191          (add-log-mailing-address (substring (shell-command-to-string
192                                               "git config user.email") 0 -1))
193          (add-log-keep-changes-together t)
194          (header (concat
195                   (format-time-string "%Y-%m-%d")
196                   "  "
197                   add-log-full-name "  <"
198                   add-log-mailing-address ">\n"))
199          (newhead
200           (concat
201            "# Copyright -- to fool `add-change-log-entry'
202 ### Instructions:
203 #
204 # Put your short one-line summary on the first blank line after these.
205 # Make sure that there is a blank line between your summary and the rest
206 # of your changes log.
207 #
208 ###"
209            (if (sy-git-check-hook msg-hook)
210                "\n# Lines beginning with '#' will be automatically deleted."
211              "\n# You MUST delete these lines before committing.")
212            (unless (sy-git-check-hook commit-hook)
213              "\n# No post-commit hook. Manually delete this log after you commit.")
214            "\n###"))
215          )
216     (and current-prefix-arg             ; User wants to start over
217          (file-exists-p logfile)
218          (ignore-errors
219            (delete-file logfile)))
220     ;; It is possible that the logfile is gone but the buffer is still
221     ;; active
222     (and (not (file-exists-p logfile))
223          (buffer-live-p (find-buffer-visiting logfile))
224          (kill-buffer (find-buffer-visiting logfile)))
225     (with-current-buffer (find-file-noselect logfile)
226       (save-excursion
227         (goto-char (point-min))
228         (when (re-search-forward "^# Copyright" (point-at-eol) t)
229           (replace-match "Copyright"))))
230     (add-change-log-entry nil logfile t nil)
231     (save-excursion
232       (goto-char (point-min))
233       (delete-matching-lines (regexp-quote header))
234       (when (re-search-forward "^Copyright" (point-at-eol) t)
235         (replace-match "# Copyright"))
236       (goto-char (point-min))
237       (unless (search-forward newhead nil t)
238         (insert newhead "\n\n\n")))))
239
240
241 (global-set-key [(control x) G a] #'sy-git-add-log-entry) 
242
243 (provide 'sy-git)
244 ;;; sy-git.el ends here