chiark / gitweb /
stgit.el: Automatically update git-status buffer when necessary
[stgit] / contrib / stgit.el
CommitLineData
3a59f3db
KH
1;; stgit.el: An emacs mode for StGit
2;;
3;; Copyright (C) 2007 David Kågedal <davidk@lysator.liu.se>
4;;
5;; To install: put this file on the load-path and place the following
6;; in your .emacs file:
7;;
8;; (require 'stgit)
9;;
10;; To start: `M-x stgit'
11
0f076fe6
GH
12(require 'git nil t)
13
56d81fe5 14(defun stgit (dir)
a53347d9 15 "Manage StGit patches for the tree in DIR."
56d81fe5 16 (interactive "DDirectory: \n")
52144ce5 17 (switch-to-stgit-buffer (git-get-top-dir dir))
1f0bf00f 18 (stgit-reload))
56d81fe5 19
074a4fb0
GH
20(unless (fboundp 'git-get-top-dir)
21 (defun git-get-top-dir (dir)
22 "Retrieve the top-level directory of a git tree."
23 (let ((cdup (with-output-to-string
24 (with-current-buffer standard-output
25 (cd dir)
26 (unless (eq 0 (call-process "git" nil t nil
27 "rev-parse" "--show-cdup"))
28 (error "cannot find top-level git tree for %s." dir))))))
29 (expand-file-name (concat (file-name-as-directory dir)
30 (car (split-string cdup "\n")))))))
31
32(defun stgit-refresh-git-status (&optional dir)
33 "If it exists, refresh the `git-status' buffer belonging to
34directory DIR or `default-directory'"
35 (when (and (fboundp 'git-find-status-buffer)
36 (fboundp 'git-refresh-status))
37 (let* ((top-dir (git-get-top-dir (or dir default-directory)))
38 (git-status-buffer (and top-dir (git-find-status-buffer top-dir))))
39 (when git-status-buffer
40 (with-current-buffer git-status-buffer
41 (git-refresh-status))))))
52144ce5 42
56d81fe5 43(defun switch-to-stgit-buffer (dir)
a53347d9 44 "Switch to a (possibly new) buffer displaying StGit patches for DIR."
56d81fe5
DK
45 (setq dir (file-name-as-directory dir))
46 (let ((buffers (buffer-list)))
47 (while (and buffers
48 (not (with-current-buffer (car buffers)
49 (and (eq major-mode 'stgit-mode)
50 (string= default-directory dir)))))
51 (setq buffers (cdr buffers)))
52 (switch-to-buffer (if buffers
53 (car buffers)
54 (create-stgit-buffer dir)))))
55
56(defun create-stgit-buffer (dir)
57 "Create a buffer for showing StGit patches.
58Argument DIR is the repository path."
59 (let ((buf (create-file-buffer (concat dir "*stgit*")))
60 (inhibit-read-only t))
61 (with-current-buffer buf
62 (setq default-directory dir)
63 (stgit-mode)
64 (setq buffer-read-only t))
65 buf))
66
67(defmacro stgit-capture-output (name &rest body)
a53347d9 68 "Capture StGit output and show it in a window at the end."
34afb86c
DK
69 `(let ((output-buf (get-buffer-create ,(or name "*StGit output*")))
70 (stgit-dir default-directory)
71 (inhibit-read-only t))
56d81fe5 72 (with-current-buffer output-buf
34afb86c
DK
73 (erase-buffer)
74 (setq default-directory stgit-dir)
75 (setq buffer-read-only t))
56d81fe5
DK
76 (let ((standard-output output-buf))
77 ,@body)
34afb86c
DK
78 (with-current-buffer output-buf
79 (set-buffer-modified-p nil)
80 (setq buffer-read-only t)
81 (if (< (point-min) (point-max))
82 (display-buffer output-buf t)))))
56d81fe5
DK
83(put 'stgit-capture-output 'lisp-indent-function 1)
84
9aecd505 85(defun stgit-run-silent (&rest args)
56d81fe5
DK
86 (apply 'call-process "stg" nil standard-output nil args))
87
9aecd505
DK
88(defun stgit-run (&rest args)
89 (let ((msgcmd (mapconcat #'identity args " ")))
90 (message "Running stg %s..." msgcmd)
91 (apply 'call-process "stg" nil standard-output nil args)
92 (message "Running stg %s...done" msgcmd)))
93
1f0bf00f 94(defun stgit-reload ()
a53347d9 95 "Update the contents of the StGit buffer."
56d81fe5
DK
96 (interactive)
97 (let ((inhibit-read-only t)
98 (curline (line-number-at-pos))
99 (curpatch (stgit-patch-at-point)))
100 (erase-buffer)
101 (insert "Branch: ")
9aecd505
DK
102 (stgit-run-silent "branch")
103 (stgit-run-silent "series" "--description")
6df83d42 104 (stgit-rescan)
56d81fe5
DK
105 (if curpatch
106 (stgit-goto-patch curpatch)
074a4fb0
GH
107 (goto-line curline)))
108 (stgit-refresh-git-status))
56d81fe5 109
07f464e0
DK
110(defface stgit-description-face
111 '((((background dark)) (:foreground "tan"))
112 (((background light)) (:foreground "dark red")))
113 "The face used for StGit desriptions")
114
115(defface stgit-top-patch-face
116 '((((background dark)) (:weight bold :foreground "yellow"))
117 (((background light)) (:weight bold :foreground "purple"))
118 (t (:weight bold)))
119 "The face used for the top patch names")
120
121(defface stgit-applied-patch-face
122 '((((background dark)) (:foreground "light yellow"))
123 (((background light)) (:foreground "purple"))
124 (t ()))
125 "The face used for applied patch names")
126
127(defface stgit-unapplied-patch-face
128 '((((background dark)) (:foreground "gray80"))
129 (((background light)) (:foreground "orchid"))
130 (t ()))
131 "The face used for unapplied patch names")
132
6df83d42
DK
133(defun stgit-rescan ()
134 "Rescan the status buffer."
07f464e0 135 (save-excursion
6df83d42
DK
136 (let ((marked ()))
137 (goto-char (point-min))
138 (while (not (eobp))
139 (cond ((looking-at "Branch: \\(.*\\)")
140 (put-text-property (match-beginning 1) (match-end 1)
141 'face 'bold))
8ee1e4b4 142 ((looking-at "\\([>+-]\\)\\( \\)\\([^ ]+\\) *[|#] \\(.*\\)")
6df83d42
DK
143 (let ((state (match-string 1))
144 (patchsym (intern (match-string 3))))
145 (put-text-property
146 (match-beginning 3) (match-end 3) 'face
147 (cond ((string= state ">") 'stgit-top-patch-face)
148 ((string= state "+") 'stgit-applied-patch-face)
149 ((string= state "-") 'stgit-unapplied-patch-face)))
150 (put-text-property (match-beginning 4) (match-end 4)
151 'face 'stgit-description-face)
152 (when (memq patchsym stgit-marked-patches)
153 (replace-match "*" nil nil nil 2)
1c2426dc 154 (setq marked (cons patchsym marked)))))
ad80ce22
DK
155 ((or (looking-at "stg series: Branch \".*\" not initialised")
156 (looking-at "stg series: .*: branch not initialized"))
1c2426dc
DK
157 (forward-line 1)
158 (insert "Run M-x stgit-init to initialise")))
6df83d42
DK
159 (forward-line 1))
160 (setq stgit-marked-patches (nreverse marked)))))
07f464e0 161
83327d53 162(defun stgit-quit ()
a53347d9 163 "Hide the stgit buffer."
83327d53
GH
164 (interactive)
165 (bury-buffer))
166
0f076fe6 167(defun stgit-git-status ()
a53347d9 168 "Show status using `git-status'."
0f076fe6
GH
169 (interactive)
170 (unless (fboundp 'git-status)
171 (error "stgit-git-status requires git-status"))
172 (let ((dir default-directory))
173 (save-selected-window
174 (pop-to-buffer nil)
175 (git-status dir))))
176
56d81fe5
DK
177(defvar stgit-mode-hook nil
178 "Run after `stgit-mode' is setup.")
179
180(defvar stgit-mode-map nil
181 "Keymap for StGit major mode.")
182
183(unless stgit-mode-map
184 (setq stgit-mode-map (make-keymap))
185 (suppress-keymap stgit-mode-map)
022a3664
GH
186 (mapc (lambda (arg) (define-key stgit-mode-map (car arg) (cdr arg)))
187 '((" " . stgit-mark)
3dccdc9b 188 ("m" . stgit-mark)
9b151b27
GH
189 ("\d" . stgit-unmark-up)
190 ("u" . stgit-unmark-down)
022a3664
GH
191 ("?" . stgit-help)
192 ("h" . stgit-help)
193 ("p" . previous-line)
194 ("n" . next-line)
0f076fe6 195 ("s" . stgit-git-status)
022a3664
GH
196 ("g" . stgit-reload)
197 ("r" . stgit-refresh)
198 ("\C-c\C-r" . stgit-rename)
199 ("e" . stgit-edit)
200 ("c" . stgit-coalesce)
201 ("N" . stgit-new)
202 ("R" . stgit-repair)
203 ("C" . stgit-commit)
204 ("U" . stgit-uncommit)
205 (">" . stgit-push-next)
206 ("<" . stgit-pop-next)
207 ("P" . stgit-push-or-pop)
208 ("G" . stgit-goto)
209 ("=" . stgit-show)
210 ("D" . stgit-delete)
211 ([(control ?/)] . stgit-undo)
83327d53
GH
212 ("\C-_" . stgit-undo)
213 ("q" . stgit-quit))))
56d81fe5
DK
214
215(defun stgit-mode ()
216 "Major mode for interacting with StGit.
217Commands:
218\\{stgit-mode-map}"
219 (kill-all-local-variables)
220 (buffer-disable-undo)
221 (setq mode-name "StGit"
222 major-mode 'stgit-mode
223 goal-column 2)
224 (use-local-map stgit-mode-map)
225 (set (make-local-variable 'list-buffers-directory) default-directory)
6df83d42 226 (set (make-local-variable 'stgit-marked-patches) nil)
2870f8b8 227 (set-variable 'truncate-lines 't)
56d81fe5
DK
228 (run-hooks 'stgit-mode-hook))
229
6df83d42
DK
230(defun stgit-add-mark (patch)
231 (let ((patchsym (intern patch)))
232 (setq stgit-marked-patches (cons patchsym stgit-marked-patches))))
233
234(defun stgit-remove-mark (patch)
235 (let ((patchsym (intern patch)))
236 (setq stgit-marked-patches (delq patchsym stgit-marked-patches))))
237
e6b1fdae
DK
238(defun stgit-clear-marks ()
239 (setq stgit-marked-patches '()))
240
6df83d42
DK
241(defun stgit-marked-patches ()
242 "Return the names of the marked patches."
243 (mapcar 'symbol-name stgit-marked-patches))
244
018fa1ac
GH
245(defun stgit-patch-at-point (&optional cause-error)
246 "Return the patch name on the current line. If CAUSE-ERROR is
247not nil, signal an error if none found."
56d81fe5
DK
248 (save-excursion
249 (beginning-of-line)
018fa1ac
GH
250 (cond ((looking-at "[>+-][ *]\\([^ ]*\\)")
251 (match-string-no-properties 1))
252 (cause-error
253 (error "No patch on this line")))))
56d81fe5 254
7755d7f1
KH
255(defun stgit-patches-marked-or-at-point ()
256 "Return the names of the marked patches, or the patch on the current line."
257 (if stgit-marked-patches
258 (stgit-marked-patches)
259 (let ((patch (stgit-patch-at-point)))
260 (if patch
261 (list patch)
262 '()))))
263
56d81fe5 264(defun stgit-goto-patch (patch)
a53347d9 265 "Move point to the line containing PATCH."
56d81fe5
DK
266 (let ((p (point)))
267 (goto-char (point-min))
6df83d42 268 (if (re-search-forward (concat "^[>+-][ *]" (regexp-quote patch) " ") nil t)
56d81fe5
DK
269 (progn (move-to-column goal-column)
270 t)
271 (goto-char p)
272 nil)))
273
1c2426dc 274(defun stgit-init ()
a53347d9 275 "Run stg init."
1c2426dc
DK
276 (interactive)
277 (stgit-capture-output nil
b0424080 278 (stgit-run "init"))
1f0bf00f 279 (stgit-reload))
1c2426dc 280
6df83d42 281(defun stgit-mark ()
a53347d9 282 "Mark the patch under point."
6df83d42 283 (interactive)
018fa1ac 284 (let ((patch (stgit-patch-at-point t)))
6df83d42 285 (stgit-add-mark patch)
1f0bf00f 286 (stgit-reload))
6df83d42
DK
287 (next-line))
288
9b151b27 289(defun stgit-unmark-up ()
a53347d9 290 "Remove mark from the patch on the previous line."
6df83d42
DK
291 (interactive)
292 (forward-line -1)
018fa1ac 293 (stgit-remove-mark (stgit-patch-at-point t))
9b151b27
GH
294 (stgit-reload))
295
296(defun stgit-unmark-down ()
a53347d9 297 "Remove mark from the patch on the current line."
9b151b27 298 (interactive)
018fa1ac 299 (stgit-remove-mark (stgit-patch-at-point t))
9b151b27
GH
300 (forward-line)
301 (stgit-reload))
6df83d42 302
56d81fe5 303(defun stgit-rename (name)
018fa1ac
GH
304 "Rename the patch under point to NAME."
305 (interactive (list (read-string "Patch name: " (stgit-patch-at-point t))))
306 (let ((old-name (stgit-patch-at-point t)))
56d81fe5
DK
307 (stgit-capture-output nil
308 (stgit-run "rename" old-name name))
1f0bf00f 309 (stgit-reload)
56d81fe5
DK
310 (stgit-goto-patch name)))
311
26201d96 312(defun stgit-repair ()
a53347d9 313 "Run stg repair."
26201d96
DK
314 (interactive)
315 (stgit-capture-output nil
b0424080 316 (stgit-run "repair"))
1f0bf00f 317 (stgit-reload))
26201d96 318
c4aad9a7
DK
319(defun stgit-commit ()
320 "Run stg commit."
321 (interactive)
322 (stgit-capture-output nil (stgit-run "commit"))
1f0bf00f 323 (stgit-reload))
c4aad9a7
DK
324
325(defun stgit-uncommit (arg)
326 "Run stg uncommit. Numeric arg determines number of patches to uncommit."
327 (interactive "p")
328 (stgit-capture-output nil (stgit-run "uncommit" "-n" (number-to-string arg)))
1f0bf00f 329 (stgit-reload))
c4aad9a7 330
0b661144
DK
331(defun stgit-push-next (npatches)
332 "Push the first unapplied patch.
333With numeric prefix argument, push that many patches."
334 (interactive "p")
335 (stgit-capture-output nil (stgit-run "push" "-n"
336 (number-to-string npatches)))
074a4fb0
GH
337 (stgit-reload)
338 (stgit-refresh-git-status))
56d81fe5 339
0b661144
DK
340(defun stgit-pop-next (npatches)
341 "Pop the topmost applied patch.
342With numeric prefix argument, pop that many patches."
343 (interactive "p")
344 (stgit-capture-output nil (stgit-run "pop" "-n" (number-to-string npatches)))
074a4fb0
GH
345 (stgit-reload)
346 (stgit-refresh-git-status))
56d81fe5 347
f9182fca
KH
348(defun stgit-applied-at-point ()
349 "Is the patch on the current line applied?"
350 (save-excursion
351 (beginning-of-line)
352 (looking-at "[>+]")))
353
354(defun stgit-push-or-pop ()
a53347d9 355 "Push or pop the patch on the current line."
f9182fca 356 (interactive)
018fa1ac 357 (let ((patch (stgit-patch-at-point t))
f9182fca
KH
358 (applied (stgit-applied-at-point)))
359 (stgit-capture-output nil
b0424080 360 (stgit-run (if applied "pop" "push") patch))
1f0bf00f 361 (stgit-reload)))
f9182fca 362
c7adf5ef 363(defun stgit-goto ()
a53347d9 364 "Go to the patch on the current line."
c7adf5ef 365 (interactive)
018fa1ac 366 (let ((patch (stgit-patch-at-point t)))
c7adf5ef 367 (stgit-capture-output nil
b0424080 368 (stgit-run "goto" patch))
1f0bf00f 369 (stgit-reload)))
c7adf5ef 370
56d81fe5 371(defun stgit-show ()
a53347d9 372 "Show the patch on the current line."
56d81fe5
DK
373 (interactive)
374 (stgit-capture-output "*StGit patch*"
018fa1ac 375 (stgit-run "show" (stgit-patch-at-point t))
56d81fe5
DK
376 (with-current-buffer standard-output
377 (goto-char (point-min))
378 (diff-mode))))
0663524d 379
0bca35c8 380(defun stgit-edit ()
a53347d9 381 "Edit the patch on the current line."
0bca35c8 382 (interactive)
018fa1ac 383 (let ((patch (stgit-patch-at-point t))
0780be79 384 (edit-buf (get-buffer-create "*StGit edit*"))
0bca35c8
DK
385 (dir default-directory))
386 (log-edit 'stgit-confirm-edit t nil edit-buf)
387 (set (make-local-variable 'stgit-edit-patch) patch)
388 (setq default-directory dir)
389 (let ((standard-output edit-buf))
9aecd505 390 (stgit-run-silent "edit" "--save-template=-" patch))))
0bca35c8
DK
391
392(defun stgit-confirm-edit ()
393 (interactive)
394 (let ((file (make-temp-file "stgit-edit-")))
395 (write-region (point-min) (point-max) file)
396 (stgit-capture-output nil
397 (stgit-run "edit" "-f" file stgit-edit-patch))
398 (with-current-buffer log-edit-parent-buffer
1f0bf00f 399 (stgit-reload))))
0bca35c8 400
64c097a0 401(defun stgit-new ()
a53347d9 402 "Create a new patch."
64c097a0 403 (interactive)
c5d45b92
GH
404 (let ((edit-buf (get-buffer-create "*StGit edit*"))
405 (dir default-directory))
406 (log-edit 'stgit-confirm-new t nil edit-buf)
407 (setq default-directory dir)))
64c097a0
DK
408
409(defun stgit-confirm-new ()
410 (interactive)
27b0f9e4 411 (let ((file (make-temp-file "stgit-edit-")))
64c097a0
DK
412 (write-region (point-min) (point-max) file)
413 (stgit-capture-output nil
27b0f9e4 414 (stgit-run "new" "-f" file))
64c097a0 415 (with-current-buffer log-edit-parent-buffer
1f0bf00f 416 (stgit-reload))))
64c097a0
DK
417
418(defun stgit-create-patch-name (description)
419 "Create a patch name from a long description"
420 (let ((patch ""))
421 (while (> (length description) 0)
422 (cond ((string-match "\\`[a-zA-Z_-]+" description)
423 (setq patch (downcase (concat patch (match-string 0 description))))
424 (setq description (substring description (match-end 0))))
425 ((string-match "\\` +" description)
426 (setq patch (concat patch "-"))
427 (setq description (substring description (match-end 0))))
428 ((string-match "\\`[^a-zA-Z_-]+" description)
429 (setq description (substring description (match-end 0))))))
430 (cond ((= (length patch) 0)
431 "patch")
432 ((> (length patch) 20)
433 (substring patch 0 20))
434 (t patch))))
0bca35c8 435
7755d7f1 436(defun stgit-delete (patch-names)
a53347d9 437 "Delete the named patches."
7755d7f1
KH
438 (interactive (list (stgit-patches-marked-or-at-point)))
439 (if (zerop (length patch-names))
440 (error "No patches to delete")
441 (when (yes-or-no-p (format "Really delete %d patches? "
442 (length patch-names)))
443 (stgit-capture-output nil
444 (apply 'stgit-run "delete" patch-names))
1f0bf00f 445 (stgit-reload))))
7755d7f1 446
ea0def18 447(defun stgit-coalesce (patch-names)
a53347d9 448 "Run stg coalesce on the named patches."
ea0def18 449 (interactive (list (stgit-marked-patches)))
0780be79 450 (let ((edit-buf (get-buffer-create "*StGit edit*"))
ea0def18
DK
451 (dir default-directory))
452 (log-edit 'stgit-confirm-coalesce t nil edit-buf)
453 (set (make-local-variable 'stgit-patches) patch-names)
454 (setq default-directory dir)
455 (let ((standard-output edit-buf))
9aecd505 456 (apply 'stgit-run-silent "coalesce" "--save-template=-" patch-names))))
ea0def18
DK
457
458(defun stgit-confirm-coalesce ()
459 (interactive)
460 (let ((file (make-temp-file "stgit-edit-")))
461 (write-region (point-min) (point-max) file)
462 (stgit-capture-output nil
463 (apply 'stgit-run "coalesce" "-f" file stgit-patches))
464 (with-current-buffer log-edit-parent-buffer
e6b1fdae
DK
465 (stgit-clear-marks)
466 ;; Go to first marked patch and stay there
467 (goto-char (point-min))
468 (re-search-forward (concat "^[>+-]\\*") nil t)
469 (move-to-column goal-column)
470 (let ((pos (point)))
1f0bf00f 471 (stgit-reload)
e6b1fdae 472 (goto-char pos)))))
ea0def18 473
0663524d
KH
474(defun stgit-help ()
475 "Display help for the StGit mode."
476 (interactive)
477 (describe-function 'stgit-mode))
3a59f3db 478
83e51dbf
DK
479(defun stgit-undo (&optional arg)
480 "Run stg undo.
481With prefix argument, run it with the --hard flag."
482 (interactive "P")
483 (stgit-capture-output nil
484 (if arg
485 (stgit-run "undo" "--hard")
486 (stgit-run "undo")))
1f0bf00f 487 (stgit-reload))
83e51dbf 488
4d73c4d8
DK
489(defun stgit-refresh (&optional arg)
490 "Run stg refresh.
a53347d9 491With prefix argument, refresh the marked patch or the patch under point."
4d73c4d8
DK
492 (interactive "P")
493 (let ((patchargs (if arg
b0424080
GH
494 (let ((patches (stgit-patches-marked-or-at-point)))
495 (cond ((null patches)
496 (error "no patch to update"))
497 ((> (length patches) 1)
498 (error "too many patches selected"))
499 (t
500 (cons "-p" patches))))
501 nil)))
4d73c4d8 502 (stgit-capture-output nil
074a4fb0
GH
503 (apply 'stgit-run "refresh" patchargs))
504 (stgit-refresh-git-status))
4d73c4d8
DK
505 (stgit-reload))
506
3a59f3db 507(provide 'stgit)