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