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