chiark / gitweb /
stgit.el: Add "B" for stgit-branch
[stgit] / contrib / stgit.el
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
12 (require 'git nil t)
13
14 (defun stgit (dir)
15   "Manage StGit patches for the tree in DIR."
16   (interactive "DDirectory: \n")
17   (switch-to-stgit-buffer (git-get-top-dir dir))
18   (stgit-reload))
19
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
34 directory 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))))))
42
43 (defun switch-to-stgit-buffer (dir)
44   "Switch to a (possibly new) buffer displaying StGit patches for DIR."
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.
58 Argument 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)
68   "Capture StGit output and show it in a window at the end."
69   `(let ((output-buf (get-buffer-create ,(or name "*StGit output*")))
70          (stgit-dir default-directory)
71          (inhibit-read-only t))
72      (with-current-buffer output-buf
73        (erase-buffer)
74        (setq default-directory stgit-dir)
75        (setq buffer-read-only t))
76      (let ((standard-output output-buf))
77        ,@body)
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)))))
83 (put 'stgit-capture-output 'lisp-indent-function 1)
84
85 (defun stgit-make-run-args (args)
86   "Return a copy of ARGS with its elements converted to strings."
87   (mapcar (lambda (x)
88             ;; don't use (format "%s" ...) to limit type errors
89             (cond ((stringp x) x)
90                   ((integerp x) (number-to-string x))
91                   ((symbolp x) (symbol-name x))
92                   (t
93                    (error "Bad element in stgit-make-run-args args: %S" x))))
94           args))
95
96 (defun stgit-run-silent (&rest args)
97   (setq args (stgit-make-run-args args))
98   (apply 'call-process "stg" nil standard-output nil args))
99
100 (defun stgit-run (&rest args)
101   (setq args (stgit-make-run-args args))
102   (let ((msgcmd (mapconcat #'identity args " ")))
103     (message "Running stg %s..." msgcmd)
104     (apply 'call-process "stg" nil standard-output nil args)
105     (message "Running stg %s...done" msgcmd)))
106
107 (defun stgit-run-git (&rest args)
108   (setq args (stgit-make-run-args args))
109   (let ((msgcmd (mapconcat #'identity args " ")))
110     (message "Running git %s..." msgcmd)
111     (apply 'call-process "git" nil standard-output nil args)
112     (message "Running git %s...done" msgcmd)))
113
114 (defun stgit-run-git-silent (&rest args)
115   (setq args (stgit-make-run-args args))
116   (apply 'call-process "git" nil standard-output nil args))
117
118 (defun stgit-reload ()
119   "Update the contents of the StGit buffer."
120   (interactive)
121   (let ((inhibit-read-only t)
122         (curline (line-number-at-pos))
123         (curpatch (stgit-patch-at-point)))
124     (erase-buffer)
125     (insert "Branch: ")
126     (stgit-run-silent "branch")
127     (stgit-run-silent "series" "--description" "--empty")
128     (stgit-rescan)
129     (if curpatch
130         (stgit-goto-patch curpatch)
131       (goto-line curline)))
132   (stgit-refresh-git-status))
133
134 (defgroup stgit nil
135   "A user interface for the StGit patch maintenance tool."
136   :group 'tools)
137
138 (defface stgit-description-face
139   '((((background dark)) (:foreground "tan"))
140     (((background light)) (:foreground "dark red")))
141   "The face used for StGit descriptions"
142   :group 'stgit)
143
144 (defface stgit-top-patch-face
145   '((((background dark)) (:weight bold :foreground "yellow"))
146     (((background light)) (:weight bold :foreground "purple"))
147     (t (:weight bold)))
148   "The face used for the top patch names"
149   :group 'stgit)
150
151 (defface stgit-applied-patch-face
152   '((((background dark)) (:foreground "light yellow"))
153     (((background light)) (:foreground "purple"))
154     (t ()))
155   "The face used for applied patch names"
156   :group 'stgit)
157
158 (defface stgit-unapplied-patch-face
159   '((((background dark)) (:foreground "gray80"))
160     (((background light)) (:foreground "orchid"))
161     (t ()))
162   "The face used for unapplied patch names"
163   :group 'stgit)
164
165 (defface stgit-modified-file-face
166   '((((class color) (background light)) (:foreground "purple"))
167     (((class color) (background dark)) (:foreground "salmon")))
168   "StGit mode face used for modified file status"
169   :group 'stgit)
170
171 (defface stgit-unmerged-file-face
172   '((((class color) (background light)) (:foreground "red" :bold t))
173     (((class color) (background dark)) (:foreground "red" :bold t)))
174   "StGit mode face used for unmerged file status"
175   :group 'stgit)
176
177 (defface stgit-unknown-file-face
178   '((((class color) (background light)) (:foreground "goldenrod" :bold t))
179     (((class color) (background dark)) (:foreground "goldenrod" :bold t)))
180   "StGit mode face used for unknown file status"
181   :group 'stgit)
182
183 (defface stgit-file-permission-face
184   '((((class color) (background light)) (:foreground "green" :bold t))
185     (((class color) (background dark)) (:foreground "green" :bold t)))
186   "StGit mode face used for permission changes."
187   :group 'stgit)
188
189 (defcustom stgit-expand-find-copies-harder
190   nil
191   "Try harder to find copied files when listing patches.
192
193 When not nil, runs git diff-tree with the --find-copies-harder
194 flag, which reduces performance."
195   :type 'boolean
196   :group 'stgit)
197
198 (defconst stgit-file-status-code-strings
199   (mapcar (lambda (arg)
200             (cons (car arg)
201                   (propertize (cadr arg) 'face (car (cddr arg)))))
202           '((add         "Added"       stgit-modified-file-face)
203             (copy        "Copied"      stgit-modified-file-face)
204             (delete      "Deleted"     stgit-modified-file-face)
205             (modify      "Modified"    stgit-modified-file-face)
206             (rename      "Renamed"     stgit-modified-file-face)
207             (mode-change "Mode change" stgit-modified-file-face)
208             (unmerged    "Unmerged"    stgit-unmerged-file-face)
209             (unknown     "Unknown"     stgit-unknown-file-face)))
210   "Alist of code symbols to description strings")
211
212 (defun stgit-file-status-code-as-string (code)
213   "Return stgit status code as string"
214   (let ((str (assq (if (consp code) (car code) code)
215                    stgit-file-status-code-strings)))
216     (when str
217       (format "%-11s  "
218               (if (and str (consp code) (/= (cdr code) 100))
219                   (format "%s %s" (cdr str)
220                           (propertize (format "%d%%" (cdr code))
221                                       'face 'stgit-description-face))
222                 (cdr str))))))
223
224 (defun stgit-file-status-code (str &optional score)
225   "Return stgit status code from git status string"
226   (let ((code (assoc str '(("A" . add)
227                            ("C" . copy)
228                            ("D" . delete)
229                            ("M" . modify)
230                            ("R" . rename)
231                            ("T" . mode-change)
232                            ("U" . unmerged)
233                            ("X" . unknown)))))
234     (setq code (if code (cdr code) 'unknown))
235     (when (stringp score)
236       (if (> (length score) 0)
237           (setq score (string-to-number score))
238         (setq score nil)))
239     (if score (cons code score) code)))
240
241 (defconst stgit-file-type-strings
242   '((#o100 . "file")
243     (#o120 . "symlink")
244     (#o160 . "subproject"))
245   "Alist of names of file types")
246
247 (defun stgit-file-type-string (type)
248   "Return string describing file type TYPE (the high bits of file permission).
249 Cf. `stgit-file-type-strings' and `stgit-file-type-change-string'."
250   (let ((type-str (assoc type stgit-file-type-strings)))
251     (or (and type-str (cdr type-str))
252         (format "unknown type %o" type))))
253
254 (defun stgit-file-type-change-string (old-perm new-perm)
255   "Return string describing file type change from OLD-PERM to NEW-PERM.
256 Cf. `stgit-file-type-string'."
257   (let ((old-type (lsh old-perm -9))
258         (new-type (lsh new-perm -9)))
259     (cond ((= old-type new-type) "")
260           ((zerop new-type) "")
261           ((zerop old-type)
262            (if (= new-type #o100)
263                ""
264              (format "   (%s)" (stgit-file-type-string new-type))))
265           (t (format "   (%s -> %s)"
266                      (stgit-file-type-string old-type)
267                      (stgit-file-type-string new-type))))))
268
269 (defun stgit-file-mode-change-string (old-perm new-perm)
270   "Return string describing file mode change from OLD-PERM to NEW-PERM.
271 Cf. `stgit-file-type-change-string'."
272   (setq old-perm (logand old-perm #o777)
273         new-perm (logand new-perm #o777))
274   (if (or (= old-perm new-perm)
275           (zerop old-perm)
276           (zerop new-perm))
277       ""
278     (let* ((modified       (logxor old-perm new-perm))
279            (not-x-modified (logand (logxor old-perm new-perm) #o666)))
280       (cond ((zerop modified) "")
281             ((and (zerop not-x-modified)
282                   (or (and (eq #o111 (logand old-perm #o111))
283                            (propertize "-x" 'face 'stgit-file-permission-face))
284                       (and (eq #o111 (logand new-perm #o111))
285                            (propertize "+x" 'face
286                                        'stgit-file-permission-face)))))
287             (t (concat (propertize (format "%o" old-perm)
288                                    'face 'stgit-file-permission-face)
289                        (propertize " -> "
290                                    'face 'stgit-description-face)
291                        (propertize (format "%o" new-perm)
292                                    'face 'stgit-file-permission-face)))))))
293
294 (defun stgit-expand-patch (patchsym)
295   "Expand (show modification of) the patch with name PATCHSYM (a
296 symbol) at point.
297 `stgit-expand-find-copies-harder' controls how hard to try to
298 find copied files."
299   (save-excursion
300     (forward-line)
301     (let* ((start (point))
302            (result (with-output-to-string
303                      (stgit-run-git "diff-tree" "-r" "-z"
304                                     (if stgit-expand-find-copies-harder
305                                         "--find-copies-harder"
306                                       "-C")
307                                     (stgit-id patchsym)))))
308       (let (mstart)
309         (while (string-match "\0:\\([0-7]+\\) \\([0-7]+\\) [0-9A-Fa-f]\\{40\\} [0-9A-Fa-f]\\{40\\} \\(\\([CR]\\)\\([0-9]*\\)\0\\([^\0]*\\)\0\\([^\0]*\\)\\|\\([ABD-QS-Z]\\)\0\\([^\0]*\\)\\)"
310                              result mstart)
311           (let ((copy-or-rename (match-string 4 result))
312                 (old-perm       (read (format "#o%s" (match-string 1 result))))
313                 (new-perm       (read (format "#o%s" (match-string 2 result))))
314                 (line-start (point))
315                 status
316                 change
317                 properties)
318             (insert "    ")
319             (if copy-or-rename
320                 (let ((cr-score       (match-string 5 result))
321                       (cr-from-file   (match-string 6 result))
322                       (cr-to-file     (match-string 7 result)))
323                   (setq status (stgit-file-status-code copy-or-rename
324                                                        cr-score)
325                         properties (list 'stgit-old-file cr-from-file
326                                          'stgit-new-file cr-to-file)
327                         change (concat
328                                 cr-from-file
329                                 (propertize " -> "
330                                             'face 'stgit-description-face)
331                                 cr-to-file)))
332               (setq status (stgit-file-status-code (match-string 8 result))
333                     properties (list 'stgit-file (match-string 9 result))
334                     change (match-string 9 result)))
335
336             (let ((mode-change (stgit-file-mode-change-string old-perm
337                                                               new-perm)))
338               (insert (format "%-12s" (stgit-file-status-code-as-string
339                                        status))
340                       mode-change
341                       (if (> (length mode-change) 0) " " "")
342                       change
343                       (propertize (stgit-file-type-change-string old-perm
344                                                                  new-perm)
345                                   'face 'stgit-description-face)
346                       ?\n))
347             (add-text-properties line-start (point) properties))
348           (setq mstart (match-end 0))))
349       (when (= start (point))
350         (insert "    <no files>\n"))
351       (put-text-property start (point) 'stgit-file-patchsym patchsym))))
352
353 (defun stgit-collapse-patch (patchsym)
354   "Collapse the patch with name PATCHSYM after the line at point."
355   (save-excursion
356     (forward-line)
357     (let ((start (point)))
358       (while (eq (get-text-property (point) 'stgit-file-patchsym) patchsym)
359         (forward-line))
360       (delete-region start (point)))))
361
362 (defun stgit-rescan ()
363   "Rescan the status buffer."
364   (save-excursion
365     (let ((marked ())
366           found-any)
367       (goto-char (point-min))
368       (while (not (eobp))
369         (cond ((looking-at "Branch: \\(.*\\)")
370                (put-text-property (match-beginning 1) (match-end 1)
371                                   'face 'bold))
372               ((looking-at "\\([0 ]\\)\\([>+-]\\)\\( \\)\\([^ ]+\\) *[|#] \\(.*\\)")
373                (setq found-any t)
374                (let ((empty (match-string 1))
375                      (state (match-string 2))
376                      (patchsym (intern (match-string 4))))
377                  (put-text-property
378                   (match-beginning 4) (match-end 4) 'face
379                   (cond ((string= state ">") 'stgit-top-patch-face)
380                         ((string= state "+") 'stgit-applied-patch-face)
381                         ((string= state "-") 'stgit-unapplied-patch-face)))
382                  (put-text-property (match-beginning 5) (match-end 5)
383                                     'face 'stgit-description-face)
384                  (when (memq patchsym stgit-marked-patches)
385                    (save-excursion
386                      (replace-match "*" nil nil nil 3))
387                    (setq marked (cons patchsym marked)))
388                  (put-text-property (match-beginning 0) (match-end 0)
389                                     'stgit-patchsym patchsym)
390                  (when (memq patchsym stgit-expanded-patches)
391                    (stgit-expand-patch patchsym))
392                  (when (equal "0" empty)
393                    (save-excursion
394                      (goto-char (match-beginning 5))
395                      (insert "(empty) ")))
396                  (delete-char 1)
397                  ))
398               ((or (looking-at "stg series: Branch \".*\" not initialised")
399                    (looking-at "stg series: .*: branch not initialized"))
400                (setq found-any t)
401                (forward-line 1)
402                (insert "Run M-x stgit-init to initialise")))
403         (forward-line 1))
404       (setq stgit-marked-patches (nreverse marked))
405       (unless found-any
406         (insert "\n  "
407                 (propertize "no patches in series"
408                             'face 'stgit-description-face))))))
409
410 (defun stgit-select-file ()
411   (let ((patched-file (stgit-patched-file-at-point)))
412     (unless patched-file
413       (error "No patch or file on the current line"))
414     (let ((filename (expand-file-name (cdr patched-file))))
415       (unless (file-exists-p filename)
416         (error "File does not exist"))
417       (find-file filename))))
418
419 (defun stgit-toggle-patch-file-list (curpath)
420   (let ((inhibit-read-only t))
421     (if (memq curpatch stgit-expanded-patches)
422         (save-excursion
423           (setq stgit-expanded-patches (delq curpatch stgit-expanded-patches))
424           (stgit-collapse-patch curpatch))
425       (progn
426         (setq stgit-expanded-patches (cons curpatch stgit-expanded-patches))
427         (stgit-expand-patch curpatch)))))
428
429 (defun stgit-select ()
430   "Expand or collapse the current entry"
431   (interactive)
432   (let ((curpatch (stgit-patch-at-point)))
433     (if curpatch
434         (stgit-toggle-patch-file-list curpatch)
435       (stgit-select-file))))
436
437
438 (defun stgit-find-file-other-window ()
439   "Open file at point in other window"
440   (interactive)
441   (let ((patched-file (stgit-patched-file-at-point)))
442     (unless patched-file
443       (error "No file on the current line"))
444     (let ((filename (expand-file-name (cdr patched-file))))
445       (unless (file-exists-p filename)
446         (error "File does not exist"))
447       (find-file-other-window filename))))
448
449 (defun stgit-quit ()
450   "Hide the stgit buffer."
451   (interactive)
452   (bury-buffer))
453
454 (defun stgit-git-status ()
455   "Show status using `git-status'."
456   (interactive)
457   (unless (fboundp 'git-status)
458     (error "The stgit-git-status command requires git-status"))
459   (let ((dir default-directory))
460     (save-selected-window
461       (pop-to-buffer nil)
462       (git-status dir))))
463
464 (defun stgit-next-line (&optional arg try-vscroll)
465   "Move cursor vertically down ARG lines"
466   (interactive "p\np")
467   (next-line arg try-vscroll)
468   (when (looking-at "  \\S-")
469     (forward-char 2)))
470
471 (defun stgit-previous-line (&optional arg try-vscroll)
472   "Move cursor vertically up ARG lines"
473   (interactive "p\np")
474   (previous-line arg try-vscroll)
475   (when (looking-at "  \\S-")
476     (forward-char 2)))
477
478 (defun stgit-next-patch (&optional arg)
479   "Move cursor down ARG patches"
480   (interactive "p")
481   (unless arg
482     (setq arg 1))
483   (if (< arg 0)
484       (stgit-previous-patch (- arg))
485     (while (not (zerop arg))
486       (setq arg (1- arg))
487       (while (progn (stgit-next-line)
488                     (not (stgit-patch-at-point)))))))
489
490 (defun stgit-previous-patch (&optional arg)
491   "Move cursor up ARG patches"
492   (interactive "p")
493   (unless arg
494     (setq arg 1))
495   (if (< arg 0)
496       (stgit-next-patch (- arg))
497     (while (not (zerop arg))
498       (setq arg (1- arg))
499       (while (progn (stgit-previous-line)
500                     (not (stgit-patch-at-point)))))))
501
502 (defvar stgit-mode-hook nil
503   "Run after `stgit-mode' is setup.")
504
505 (defvar stgit-mode-map nil
506   "Keymap for StGit major mode.")
507
508 (unless stgit-mode-map
509   (setq stgit-mode-map (make-keymap))
510   (suppress-keymap stgit-mode-map)
511   (mapc (lambda (arg) (define-key stgit-mode-map (car arg) (cdr arg)))
512         '((" " .        stgit-mark)
513           ("m" .        stgit-mark)
514           ("\d" .       stgit-unmark-up)
515           ("u" .        stgit-unmark-down)
516           ("?" .        stgit-help)
517           ("h" .        stgit-help)
518           ("p" .        stgit-previous-line)
519           ("n" .        stgit-next-line)
520           ("\C-p" .     stgit-previous-patch)
521           ("\C-n" .     stgit-next-patch)
522           ("\M-{" .     stgit-previous-patch)
523           ("\M-}" .     stgit-next-patch)
524           ("s" .        stgit-git-status)
525           ("g" .        stgit-reload)
526           ("r" .        stgit-refresh)
527           ("\C-c\C-r" . stgit-rename)
528           ("e" .        stgit-edit)
529           ("c" .        stgit-coalesce)
530           ("N" .        stgit-new)
531           ("R" .        stgit-repair)
532           ("C" .        stgit-commit)
533           ("U" .        stgit-uncommit)
534           ("\r" .       stgit-select)
535           ("o" .        stgit-find-file-other-window)
536           (">" .        stgit-push-next)
537           ("<" .        stgit-pop-next)
538           ("P" .        stgit-push-or-pop)
539           ("G" .        stgit-goto)
540           ("=" .        stgit-show)
541           ("D" .        stgit-delete)
542           ([(control ?/)] . stgit-undo)
543           ("\C-_" .     stgit-undo)
544           ("B" .        stgit-branch)
545           ("q" .        stgit-quit))))
546
547 (defun stgit-mode ()
548   "Major mode for interacting with StGit.
549 Commands:
550 \\{stgit-mode-map}"
551   (kill-all-local-variables)
552   (buffer-disable-undo)
553   (setq mode-name "StGit"
554         major-mode 'stgit-mode
555         goal-column 2)
556   (use-local-map stgit-mode-map)
557   (set (make-local-variable 'list-buffers-directory) default-directory)
558   (set (make-local-variable 'stgit-marked-patches) nil)
559   (set (make-local-variable 'stgit-expanded-patches) nil)
560   (set-variable 'truncate-lines 't)
561   (run-hooks 'stgit-mode-hook))
562
563 (defun stgit-add-mark (patchsym)
564   "Mark the patch PATCHSYM."
565   (setq stgit-marked-patches (cons patchsym stgit-marked-patches)))
566
567 (defun stgit-remove-mark (patchsym)
568   "Unmark the patch PATCHSYM."
569   (setq stgit-marked-patches (delq patchsym stgit-marked-patches)))
570
571 (defun stgit-clear-marks ()
572   "Unmark all patches."
573   (setq stgit-marked-patches '()))
574
575 (defun stgit-patch-at-point (&optional cause-error allow-file)
576   "Return the patch name on the current line as a symbol.
577 If CAUSE-ERROR is not nil, signal an error if none found.
578 If ALLOW-FILE is not nil, also handle when point is on a file of
579 a patch."
580   (or (get-text-property (point) 'stgit-patchsym)
581       (and allow-file
582            (get-text-property (point) 'stgit-file-patchsym))
583       (when cause-error
584         (error "No patch on this line"))))
585
586 (defun stgit-patched-file-at-point (&optional both-files)
587   "Returns a cons of the patchsym and file name at point. For
588 copies and renames, return the new file if the patch is either
589 applied. If BOTH-FILES is non-nil, return a cons of the old and
590 the new file names instead of just one name."
591   (let ((patchsym (get-text-property (point) 'stgit-file-patchsym))
592         (file     (get-text-property (point) 'stgit-file)))
593     (cond ((not patchsym) nil)
594           (file (cons patchsym file))
595           (both-files
596            (cons patchsym (cons (get-text-property (point) 'stgit-old-file)
597                                 (get-text-property (point) 'stgit-new-file))))
598           (t
599            (let ((file-sym (save-excursion
600                              (stgit-previous-patch)
601                              (unless (eq (stgit-patch-at-point)
602                                          patchsym)
603                                (error "Cannot find the %s patch" patchsym))
604                              (beginning-of-line)
605                              (if (= (char-after) ?-)
606                                  'stgit-old-file 
607                                'stgit-new-file))))
608              (cons patchsym (get-text-property (point) file-sym)))))))
609
610 (defun stgit-patches-marked-or-at-point ()
611   "Return the symbols of the marked patches, or the patch on the current line."
612   (if stgit-marked-patches
613       stgit-marked-patches
614     (let ((patch (stgit-patch-at-point)))
615       (if patch
616           (list patch)
617         '()))))
618
619 (defun stgit-goto-patch (patchsym)
620   "Move point to the line containing patch PATCHSYM.
621 If that patch cannot be found, return nil."
622   (let ((p (text-property-any (point-min) (point-max)
623                               'stgit-patchsym patchsym)))
624     (when p
625       (goto-char p)
626       (move-to-column goal-column))))
627
628 (defun stgit-init ()
629   "Run stg init."
630   (interactive)
631   (stgit-capture-output nil
632     (stgit-run "init"))
633   (stgit-reload))
634
635 (defun stgit-mark ()
636   "Mark the patch under point."
637   (interactive)
638   (let ((patch (stgit-patch-at-point t)))
639     (stgit-add-mark patch)
640     (stgit-reload))
641   (stgit-next-patch))
642
643 (defun stgit-unmark-up ()
644   "Remove mark from the patch on the previous line."
645   (interactive)
646   (stgit-previous-patch)
647   (stgit-remove-mark (stgit-patch-at-point t))
648   (stgit-reload))
649
650 (defun stgit-unmark-down ()
651   "Remove mark from the patch on the current line."
652   (interactive)
653   (stgit-remove-mark (stgit-patch-at-point t))
654   (stgit-reload)
655   (stgit-next-patch))
656
657 (defun stgit-rename (name)
658   "Rename the patch under point to NAME."
659   (interactive (list (read-string "Patch name: "
660                                   (symbol-name (stgit-patch-at-point t)))))
661   (let ((old-patchsym (stgit-patch-at-point t)))
662     (stgit-capture-output nil
663       (stgit-run "rename" old-patchsym name))
664     (let ((name-sym (intern name)))
665       (when (memq old-patchsym stgit-expanded-patches)
666         (setq stgit-expanded-patches
667             (cons name-sym (delq old-patchsym stgit-expanded-patches))))
668       (when (memq old-patchsym stgit-marked-patches)
669         (setq stgit-marked-patches
670             (cons name-sym (delq old-patchsym stgit-marked-patches))))
671       (stgit-reload)
672       (stgit-goto-patch name-sym))))
673
674 (defun stgit-repair ()
675   "Run stg repair."
676   (interactive)
677   (stgit-capture-output nil
678     (stgit-run "repair"))
679   (stgit-reload))
680
681 (defun stgit-available-branches ()
682   "Returns a list of the available stg branches"
683   (let ((output (with-output-to-string
684                   (stgit-run "branch" "--list")))
685         (start 0)
686         result)
687     (while (string-match "^>?\\s-+s\\s-+\\(\\S-+\\)" output start)
688       (setq result (cons (match-string 1 output) result))
689       (setq start (match-end 0)))
690     result))
691
692 (defun stgit-branch (branch)
693   "Switch to branch BRANCH."
694   (interactive (list (completing-read "Switch to branch: "
695                                       (stgit-available-branches))))
696   (stgit-capture-output nil (stgit-run "branch" "--" branch))
697   (stgit-reload))
698
699 (defun stgit-commit (count)
700   "Run stg commit on COUNT commits.
701 Interactively, the prefix argument is used as COUNT."
702   (interactive "p")
703   (stgit-capture-output nil (stgit-run "commit" "-n" count))
704   (stgit-reload))
705
706 (defun stgit-uncommit (count)
707   "Run stg uncommit on COUNT commits.
708 Interactively, the prefix argument is used as COUNT."
709   (interactive "p")
710   (stgit-capture-output nil (stgit-run "uncommit" "-n" count))
711   (stgit-reload))
712
713 (defun stgit-push-next (npatches)
714   "Push the first unapplied patch.
715 With numeric prefix argument, push that many patches."
716   (interactive "p")
717   (stgit-capture-output nil (stgit-run "push" "-n" npatches))
718   (stgit-reload)
719   (stgit-refresh-git-status))
720
721 (defun stgit-pop-next (npatches)
722   "Pop the topmost applied patch.
723 With numeric prefix argument, pop that many patches."
724   (interactive "p")
725   (stgit-capture-output nil (stgit-run "pop" "-n" npatches))
726   (stgit-reload)
727   (stgit-refresh-git-status))
728
729 (defun stgit-applied-at-point ()
730   "Is the patch on the current line applied?"
731   (save-excursion
732     (beginning-of-line)
733     (looking-at "[>+]")))
734
735 (defun stgit-push-or-pop ()
736   "Push or pop the patch on the current line."
737   (interactive)
738   (let ((patchsym (stgit-patch-at-point t))
739         (applied (stgit-applied-at-point)))
740     (stgit-capture-output nil
741       (stgit-run (if applied "pop" "push") patchsym))
742     (stgit-reload)))
743
744 (defun stgit-goto ()
745   "Go to the patch on the current line."
746   (interactive)
747   (let ((patchsym (stgit-patch-at-point t)))
748     (stgit-capture-output nil
749       (stgit-run "goto" patchsym))
750     (stgit-reload)))
751
752 (defun stgit-id (patchsym)
753   "Return the git commit id for PATCHSYM."
754   (let ((result (with-output-to-string
755                   (stgit-run-silent "id" patchsym))))
756     (unless (string-match "^\\([0-9A-Fa-f]\\{40\\}\\)$" result)
757       (error "Cannot find commit id for %s" patchsym))
758     (match-string 1 result)))
759
760 (defun stgit-show ()
761   "Show the patch on the current line."
762   (interactive)
763   (stgit-capture-output "*StGit patch*"
764     (let ((patchsym (stgit-patch-at-point)))
765       (if (not patchsym)
766           (let ((patched-file (stgit-patched-file-at-point t)))
767             (unless patched-file
768               (error "No patch or file at point"))
769             (let ((id (stgit-id (car patched-file))))
770               (if (consp (cdr patched-file))
771                   ;; two files (copy or rename)
772                   (stgit-run-git "diff" "-C" "-C" (concat id "^") id "--"
773                                  (cadr patched-file) (cddr patched-file))
774                 ;; just one file
775                 (stgit-run-git "diff" (concat id "^") id "--"
776                                (cdr patched-file)))))
777         (stgit-run "show" "-O" "--patch-with-stat" patchsym))
778       (with-current-buffer standard-output
779         (goto-char (point-min))
780         (diff-mode)))))
781
782 (defun stgit-edit ()
783   "Edit the patch on the current line."
784   (interactive)
785   (let ((patchsym (stgit-patch-at-point t))
786         (edit-buf (get-buffer-create "*StGit edit*"))
787         (dir default-directory))
788     (log-edit 'stgit-confirm-edit t nil edit-buf)
789     (set (make-local-variable 'stgit-edit-patchsym) patchsym)
790     (setq default-directory dir)
791     (let ((standard-output edit-buf))
792       (stgit-run-silent "edit" "--save-template=-" patchsym))))
793
794 (defun stgit-confirm-edit ()
795   (interactive)
796   (let ((file (make-temp-file "stgit-edit-")))
797     (write-region (point-min) (point-max) file)
798     (stgit-capture-output nil
799       (stgit-run "edit" "-f" file stgit-edit-patchsym))
800     (with-current-buffer log-edit-parent-buffer
801       (stgit-reload))))
802
803 (defun stgit-new (add-sign)
804   "Create a new patch.
805 With a prefix argument, include a \"Signed-off-by:\" line at the
806 end of the patch."
807   (interactive "P")
808   (let ((edit-buf (get-buffer-create "*StGit edit*"))
809         (dir default-directory))
810     (log-edit 'stgit-confirm-new t nil edit-buf)
811     (setq default-directory dir)
812     (when add-sign
813       (save-excursion
814         (let ((standard-output (current-buffer)))
815           (stgit-run-silent "new" "--sign" "--save-template=-"))))))
816
817 (defun stgit-confirm-new ()
818   (interactive)
819   (let ((file (make-temp-file "stgit-edit-")))
820     (write-region (point-min) (point-max) file)
821     (stgit-capture-output nil
822       (stgit-run "new" "-f" file))
823     (with-current-buffer log-edit-parent-buffer
824       (stgit-reload))))
825
826 (defun stgit-create-patch-name (description)
827   "Create a patch name from a long description"
828   (let ((patch ""))
829     (while (> (length description) 0)
830       (cond ((string-match "\\`[a-zA-Z_-]+" description)
831              (setq patch (downcase (concat patch
832                                            (match-string 0 description))))
833              (setq description (substring description (match-end 0))))
834             ((string-match "\\` +" description)
835              (setq patch (concat patch "-"))
836              (setq description (substring description (match-end 0))))
837             ((string-match "\\`[^a-zA-Z_-]+" description)
838              (setq description (substring description (match-end 0))))))
839     (cond ((= (length patch) 0)
840            "patch")
841           ((> (length patch) 20)
842            (substring patch 0 20))
843           (t patch))))
844
845 (defun stgit-delete (patchsyms &optional spill-p)
846   "Delete the patches in PATCHSYMS.
847 Interactively, delete the marked patches, or the patch at point.
848
849 With a prefix argument, or SPILL-P, spill the patch contents to
850 the work tree and index."
851   (interactive (list (stgit-patches-marked-or-at-point)
852                      current-prefix-arg))
853   (unless patchsyms
854     (error "No patches to delete"))
855   (let ((npatches (length patchsyms)))
856     (when (yes-or-no-p (format "Really delete %d patch%s%s? "
857                                npatches
858                                (if (= 1 npatches) "" "es")
859                                (if spill-p
860                                    " (spilling contents to index)"
861                                  "")))
862       (let ((args (if spill-p 
863                       (cons "--spill" patchsyms)
864                     patchsyms)))
865         (stgit-capture-output nil
866           (apply 'stgit-run "delete" args))
867         (stgit-reload)))))
868
869 (defun stgit-coalesce (patchsyms)
870   "Coalesce the patches in PATCHSYMS.
871 Interactively, coalesce the marked patches."
872   (interactive (list stgit-marked-patches))
873   (when (< (length patchsyms) 2)
874     (error "Need at least two patches to coalesce"))
875   (let ((edit-buf (get-buffer-create "*StGit edit*"))
876         (dir default-directory))
877     (log-edit 'stgit-confirm-coalesce t nil edit-buf)
878     (set (make-local-variable 'stgit-patchsyms) patchsyms)
879     (setq default-directory dir)
880     (let ((standard-output edit-buf))
881       (apply 'stgit-run-silent "coalesce" "--save-template=-" patchsyms))))
882
883 (defun stgit-confirm-coalesce ()
884   (interactive)
885   (let ((file (make-temp-file "stgit-edit-")))
886     (write-region (point-min) (point-max) file)
887     (stgit-capture-output nil
888       (apply 'stgit-run "coalesce" "-f" file stgit-patchsyms))
889     (with-current-buffer log-edit-parent-buffer
890       (stgit-clear-marks)
891       ;; Go to first marked patch and stay there
892       (goto-char (point-min))
893       (re-search-forward (concat "^[>+-]\\*") nil t)
894       (move-to-column goal-column)
895       (let ((pos (point)))
896         (stgit-reload)
897         (goto-char pos)))))
898
899 (defun stgit-help ()
900   "Display help for the StGit mode."
901   (interactive)
902   (describe-function 'stgit-mode))
903
904 (defun stgit-undo (&optional arg)
905   "Run stg undo.
906 With prefix argument, run it with the --hard flag."
907   (interactive "P")
908   (stgit-capture-output nil
909     (if arg
910         (stgit-run "undo" "--hard")
911       (stgit-run "undo")))
912   (stgit-reload))
913
914 (defun stgit-refresh (&optional arg)
915   "Run stg refresh.
916 With prefix argument, refresh the marked patch or the patch under point."
917   (interactive "P")
918   (let ((patchargs (if arg
919                        (let ((patches (stgit-patches-marked-or-at-point)))
920                          (cond ((null patches)
921                                 (error "No patch to update"))
922                                ((> (length patches) 1)
923                                 (error "Too many patches selected"))
924                                (t
925                                 (cons "-p" patches))))
926                      nil)))
927     (stgit-capture-output nil
928       (apply 'stgit-run "refresh" patchargs))
929     (stgit-refresh-git-status))
930   (stgit-reload))
931
932 (provide 'stgit)