chiark / gitweb /
stgit.el: Add "M" for stgit-move-patches
[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-next-line (&optional arg try-vscroll)
467   "Move cursor vertically down ARG lines"
468   (interactive "p\np")
469   (next-line arg try-vscroll)
470   (when (looking-at "  \\S-")
471     (forward-char 2)))
472
473 (defun stgit-previous-line (&optional arg try-vscroll)
474   "Move cursor vertically up ARG lines"
475   (interactive "p\np")
476   (previous-line arg try-vscroll)
477   (when (looking-at "  \\S-")
478     (forward-char 2)))
479
480 (defun stgit-next-patch (&optional arg)
481   "Move cursor down ARG patches"
482   (interactive "p")
483   (unless arg
484     (setq arg 1))
485   (if (< arg 0)
486       (stgit-previous-patch (- arg))
487     (while (not (zerop arg))
488       (setq arg (1- arg))
489       (while (progn (stgit-next-line)
490                     (not (stgit-patch-at-point)))))))
491
492 (defun stgit-previous-patch (&optional arg)
493   "Move cursor up ARG patches"
494   (interactive "p")
495   (unless arg
496     (setq arg 1))
497   (if (< arg 0)
498       (stgit-next-patch (- arg))
499     (while (not (zerop arg))
500       (setq arg (1- arg))
501       (while (progn (stgit-previous-line)
502                     (not (stgit-patch-at-point)))))))
503
504 (defvar stgit-mode-hook nil
505   "Run after `stgit-mode' is setup.")
506
507 (defvar stgit-mode-map nil
508   "Keymap for StGit major mode.")
509
510 (unless stgit-mode-map
511   (setq stgit-mode-map (make-keymap))
512   (suppress-keymap stgit-mode-map)
513   (mapc (lambda (arg) (define-key stgit-mode-map (car arg) (cdr arg)))
514         '((" " .        stgit-mark)
515           ("m" .        stgit-mark)
516           ("\d" .       stgit-unmark-up)
517           ("u" .        stgit-unmark-down)
518           ("?" .        stgit-help)
519           ("h" .        stgit-help)
520           ("p" .        stgit-previous-line)
521           ("n" .        stgit-next-line)
522           ("\C-p" .     stgit-previous-patch)
523           ("\C-n" .     stgit-next-patch)
524           ("\M-{" .     stgit-previous-patch)
525           ("\M-}" .     stgit-next-patch)
526           ("s" .        stgit-git-status)
527           ("g" .        stgit-reload)
528           ("r" .        stgit-refresh)
529           ("\C-c\C-r" . stgit-rename)
530           ("e" .        stgit-edit)
531           ("M" .        stgit-move-patches)
532           ("S" .        stgit-squash)
533           ("N" .        stgit-new)
534           ("R" .        stgit-repair)
535           ("C" .        stgit-commit)
536           ("U" .        stgit-uncommit)
537           ("\r" .       stgit-select)
538           ("o" .        stgit-find-file-other-window)
539           (">" .        stgit-push-next)
540           ("<" .        stgit-pop-next)
541           ("P" .        stgit-push-or-pop)
542           ("G" .        stgit-goto)
543           ("=" .        stgit-show)
544           ("D" .        stgit-delete)
545           ([(control ?/)] . stgit-undo)
546           ("\C-_" .     stgit-undo)
547           ("B" .        stgit-branch)
548           ("q" .        stgit-quit))))
549
550 (defun stgit-mode ()
551   "Major mode for interacting with StGit.
552 Commands:
553 \\{stgit-mode-map}"
554   (kill-all-local-variables)
555   (buffer-disable-undo)
556   (setq mode-name "StGit"
557         major-mode 'stgit-mode
558         goal-column 2)
559   (use-local-map stgit-mode-map)
560   (set (make-local-variable 'list-buffers-directory) default-directory)
561   (set (make-local-variable 'stgit-marked-patches) nil)
562   (set (make-local-variable 'stgit-expanded-patches) nil)
563   (set-variable 'truncate-lines 't)
564   (run-hooks 'stgit-mode-hook))
565
566 (defun stgit-add-mark (patchsym)
567   "Mark the patch PATCHSYM."
568   (setq stgit-marked-patches (cons patchsym stgit-marked-patches))
569   (save-excursion
570     (when (stgit-goto-patch patchsym)
571       (move-to-column 1)
572       (let ((inhibit-read-only t))
573         (insert-and-inherit ?*)
574         (delete-char 1)))))
575
576 (defun stgit-remove-mark (patchsym)
577   "Unmark the patch PATCHSYM."
578   (setq stgit-marked-patches (delq patchsym stgit-marked-patches))
579   (save-excursion
580     (when (stgit-goto-patch patchsym)
581       (move-to-column 1)
582       (let ((inhibit-read-only t))
583         (insert-and-inherit ? )
584         (delete-char 1)))))
585
586 (defun stgit-clear-marks ()
587   "Unmark all patches."
588   (setq stgit-marked-patches '()))
589
590 (defun stgit-patch-at-point (&optional cause-error allow-file)
591   "Return the patch name on the current line as a symbol.
592 If CAUSE-ERROR is not nil, signal an error if none found.
593 If ALLOW-FILE is not nil, also handle when point is on a file of
594 a patch."
595   (or (get-text-property (point) 'stgit-patchsym)
596       (and allow-file
597            (get-text-property (point) 'stgit-file-patchsym))
598       (when cause-error
599         (error "No patch on this line"))))
600
601 (defun stgit-patched-file-at-point (&optional both-files)
602   "Returns a cons of the patchsym and file name at point. For
603 copies and renames, return the new file if the patch is either
604 applied. If BOTH-FILES is non-nil, return a cons of the old and
605 the new file names instead of just one name."
606   (let ((patchsym (get-text-property (point) 'stgit-file-patchsym))
607         (file     (get-text-property (point) 'stgit-file)))
608     (cond ((not patchsym) nil)
609           (file (cons patchsym file))
610           (both-files
611            (cons patchsym (cons (get-text-property (point) 'stgit-old-file)
612                                 (get-text-property (point) 'stgit-new-file))))
613           (t
614            (let ((file-sym (save-excursion
615                              (stgit-previous-patch)
616                              (unless (eq (stgit-patch-at-point)
617                                          patchsym)
618                                (error "Cannot find the %s patch" patchsym))
619                              (beginning-of-line)
620                              (if (= (char-after) ?-)
621                                  'stgit-old-file 
622                                'stgit-new-file))))
623              (cons patchsym (get-text-property (point) file-sym)))))))
624
625 (defun stgit-patches-marked-or-at-point ()
626   "Return the symbols of the marked patches, or the patch on the current line."
627   (if stgit-marked-patches
628       stgit-marked-patches
629     (let ((patch (stgit-patch-at-point)))
630       (if patch
631           (list patch)
632         '()))))
633
634 (defun stgit-goto-patch (patchsym)
635   "Move point to the line containing patch PATCHSYM.
636 If that patch cannot be found, return nil."
637   (let ((p (text-property-any (point-min) (point-max)
638                               'stgit-patchsym patchsym)))
639     (when p
640       (goto-char p)
641       (move-to-column goal-column))))
642
643 (defun stgit-init ()
644   "Run stg init."
645   (interactive)
646   (stgit-capture-output nil
647     (stgit-run "init"))
648   (stgit-reload))
649
650 (defun stgit-mark ()
651   "Mark the patch under point."
652   (interactive)
653   (let ((patch (stgit-patch-at-point t)))
654     (stgit-add-mark patch))
655   (stgit-next-patch))
656
657 (defun stgit-unmark-up ()
658   "Remove mark from the patch on the previous line."
659   (interactive)
660   (stgit-previous-patch)
661   (stgit-remove-mark (stgit-patch-at-point t)))
662
663 (defun stgit-unmark-down ()
664   "Remove mark from the patch on the current line."
665   (interactive)
666   (stgit-remove-mark (stgit-patch-at-point t))
667   (stgit-next-patch))
668
669 (defun stgit-rename (name)
670   "Rename the patch under point to NAME."
671   (interactive (list (read-string "Patch name: "
672                                   (symbol-name (stgit-patch-at-point t)))))
673   (let ((old-patchsym (stgit-patch-at-point t)))
674     (stgit-capture-output nil
675       (stgit-run "rename" old-patchsym name))
676     (let ((name-sym (intern name)))
677       (when (memq old-patchsym stgit-expanded-patches)
678         (setq stgit-expanded-patches
679             (cons name-sym (delq old-patchsym stgit-expanded-patches))))
680       (when (memq old-patchsym stgit-marked-patches)
681         (setq stgit-marked-patches
682             (cons name-sym (delq old-patchsym stgit-marked-patches))))
683       (stgit-reload)
684       (stgit-goto-patch name-sym))))
685
686 (defun stgit-repair ()
687   "Run stg repair."
688   (interactive)
689   (stgit-capture-output nil
690     (stgit-run "repair"))
691   (stgit-reload))
692
693 (defun stgit-available-branches ()
694   "Returns a list of the available stg branches"
695   (let ((output (with-output-to-string
696                   (stgit-run "branch" "--list")))
697         (start 0)
698         result)
699     (while (string-match "^>?\\s-+s\\s-+\\(\\S-+\\)" output start)
700       (setq result (cons (match-string 1 output) result))
701       (setq start (match-end 0)))
702     result))
703
704 (defun stgit-branch (branch)
705   "Switch to branch BRANCH."
706   (interactive (list (completing-read "Switch to branch: "
707                                       (stgit-available-branches))))
708   (stgit-capture-output nil (stgit-run "branch" "--" branch))
709   (stgit-reload))
710
711 (defun stgit-commit (count)
712   "Run stg commit on COUNT commits.
713 Interactively, the prefix argument is used as COUNT."
714   (interactive "p")
715   (stgit-capture-output nil (stgit-run "commit" "-n" count))
716   (stgit-reload))
717
718 (defun stgit-uncommit (count)
719   "Run stg uncommit on COUNT commits.
720 Interactively, the prefix argument is used as COUNT."
721   (interactive "p")
722   (stgit-capture-output nil (stgit-run "uncommit" "-n" count))
723   (stgit-reload))
724
725 (defun stgit-push-next (npatches)
726   "Push the first unapplied patch.
727 With numeric prefix argument, push that many patches."
728   (interactive "p")
729   (stgit-capture-output nil (stgit-run "push" "-n" npatches))
730   (stgit-reload)
731   (stgit-refresh-git-status))
732
733 (defun stgit-pop-next (npatches)
734   "Pop the topmost applied patch.
735 With numeric prefix argument, pop that many patches."
736   (interactive "p")
737   (stgit-capture-output nil (stgit-run "pop" "-n" npatches))
738   (stgit-reload)
739   (stgit-refresh-git-status))
740
741 (defun stgit-applied-at-point ()
742   "Is the patch on the current line applied?"
743   (save-excursion
744     (beginning-of-line)
745     (looking-at "[>+]")))
746
747 (defun stgit-push-or-pop ()
748   "Push or pop the patch on the current line."
749   (interactive)
750   (let ((patchsym (stgit-patch-at-point t))
751         (applied (stgit-applied-at-point)))
752     (stgit-capture-output nil
753       (stgit-run (if applied "pop" "push") patchsym))
754     (stgit-reload)))
755
756 (defun stgit-goto ()
757   "Go to the patch on the current line."
758   (interactive)
759   (let ((patchsym (stgit-patch-at-point t)))
760     (stgit-capture-output nil
761       (stgit-run "goto" patchsym))
762     (stgit-reload)))
763
764 (defun stgit-id (patchsym)
765   "Return the git commit id for PATCHSYM."
766   (let ((result (with-output-to-string
767                   (stgit-run-silent "id" patchsym))))
768     (unless (string-match "^\\([0-9A-Fa-f]\\{40\\}\\)$" result)
769       (error "Cannot find commit id for %s" patchsym))
770     (match-string 1 result)))
771
772 (defun stgit-show ()
773   "Show the patch on the current line."
774   (interactive)
775   (stgit-capture-output "*StGit patch*"
776     (let ((patchsym (stgit-patch-at-point)))
777       (if (not patchsym)
778           (let ((patched-file (stgit-patched-file-at-point t)))
779             (unless patched-file
780               (error "No patch or file at point"))
781             (let ((id (stgit-id (car patched-file))))
782               (if (consp (cdr patched-file))
783                   ;; two files (copy or rename)
784                   (stgit-run-git "diff" "-C" "-C" (concat id "^") id "--"
785                                  (cadr patched-file) (cddr patched-file))
786                 ;; just one file
787                 (stgit-run-git "diff" (concat id "^") id "--"
788                                (cdr patched-file)))))
789         (stgit-run "show" "-O" "--patch-with-stat" patchsym))
790       (with-current-buffer standard-output
791         (goto-char (point-min))
792         (diff-mode)))))
793
794 (defun stgit-edit ()
795   "Edit the patch on the current line."
796   (interactive)
797   (let ((patchsym (stgit-patch-at-point t))
798         (edit-buf (get-buffer-create "*StGit edit*"))
799         (dir default-directory))
800     (log-edit 'stgit-confirm-edit t nil edit-buf)
801     (set (make-local-variable 'stgit-edit-patchsym) patchsym)
802     (setq default-directory dir)
803     (let ((standard-output edit-buf))
804       (stgit-run-silent "edit" "--save-template=-" patchsym))))
805
806 (defun stgit-confirm-edit ()
807   (interactive)
808   (let ((file (make-temp-file "stgit-edit-")))
809     (write-region (point-min) (point-max) file)
810     (stgit-capture-output nil
811       (stgit-run "edit" "-f" file stgit-edit-patchsym))
812     (with-current-buffer log-edit-parent-buffer
813       (stgit-reload))))
814
815 (defun stgit-new (add-sign)
816   "Create a new patch.
817 With a prefix argument, include a \"Signed-off-by:\" line at the
818 end of the patch."
819   (interactive "P")
820   (let ((edit-buf (get-buffer-create "*StGit edit*"))
821         (dir default-directory))
822     (log-edit 'stgit-confirm-new t nil edit-buf)
823     (setq default-directory dir)
824     (when add-sign
825       (save-excursion
826         (let ((standard-output (current-buffer)))
827           (stgit-run-silent "new" "--sign" "--save-template=-"))))))
828
829 (defun stgit-confirm-new ()
830   (interactive)
831   (let ((file (make-temp-file "stgit-edit-")))
832     (write-region (point-min) (point-max) file)
833     (stgit-capture-output nil
834       (stgit-run "new" "-f" file))
835     (with-current-buffer log-edit-parent-buffer
836       (stgit-reload))))
837
838 (defun stgit-create-patch-name (description)
839   "Create a patch name from a long description"
840   (let ((patch ""))
841     (while (> (length description) 0)
842       (cond ((string-match "\\`[a-zA-Z_-]+" description)
843              (setq patch (downcase (concat patch
844                                            (match-string 0 description))))
845              (setq description (substring description (match-end 0))))
846             ((string-match "\\` +" description)
847              (setq patch (concat patch "-"))
848              (setq description (substring description (match-end 0))))
849             ((string-match "\\`[^a-zA-Z_-]+" description)
850              (setq description (substring description (match-end 0))))))
851     (cond ((= (length patch) 0)
852            "patch")
853           ((> (length patch) 20)
854            (substring patch 0 20))
855           (t patch))))
856
857 (defun stgit-delete (patchsyms &optional spill-p)
858   "Delete the patches in PATCHSYMS.
859 Interactively, delete the marked patches, or the patch at point.
860
861 With a prefix argument, or SPILL-P, spill the patch contents to
862 the work tree and index."
863   (interactive (list (stgit-patches-marked-or-at-point)
864                      current-prefix-arg))
865   (unless patchsyms
866     (error "No patches to delete"))
867   (let ((npatches (length patchsyms)))
868     (when (yes-or-no-p (format "Really delete %d patch%s%s? "
869                                npatches
870                                (if (= 1 npatches) "" "es")
871                                (if spill-p
872                                    " (spilling contents to index)"
873                                  "")))
874       (let ((args (if spill-p 
875                       (cons "--spill" patchsyms)
876                     patchsyms)))
877         (stgit-capture-output nil
878           (apply 'stgit-run "delete" args))
879         (stgit-reload)))))
880
881 (defun stgit-move-patches-target ()
882   "Return the patchsym indicating a target patch for
883 `stgit-move-patches'.
884
885 This is either the patch at point, or one of :top and :bottom, if
886 the point is after or before the applied patches."
887
888   (let ((patchsym (stgit-patch-at-point)))
889     (cond (patchsym patchsym)
890           ((save-excursion (re-search-backward "^>" nil t)) :top)
891           (t :bottom))))
892
893 (defun stgit-move-patches (patchsyms target-patch)
894   "Move the patches in PATCHSYMS to below TARGET-PATCH.
895 If TARGET-PATCH is :bottom or :top, move the patches to the
896 bottom or top of the stack, respectively.
897
898 Interactively, move the marked patches to where the point is."
899   (interactive (list stgit-marked-patches
900                      (stgit-move-patches-target)))
901   (unless patchsyms
902     (error "Need at least one patch to move"))
903
904   (unless target-patch
905     (error "Point not at a patch"))
906
907   (if (eq target-patch :top)
908       (stgit-capture-output nil
909         (apply 'stgit-run "float" patchsyms))
910
911     ;; need to have patchsyms sorted by position in the stack
912     (let (sorted-patchsyms
913           (series (with-output-to-string
914                     (with-current-buffer standard-output
915                       (stgit-run-silent "series" "--noprefix"))))
916           start)
917       (while (string-match "^\\(.+\\)" series start)
918         (let ((patchsym (intern (match-string 1 series))))
919           (when (memq patchsym patchsyms)
920             (setq sorted-patchsyms (cons patchsym sorted-patchsyms))))
921         (setq start (match-end 0)))
922       (setq sorted-patchsyms (nreverse sorted-patchsyms))
923     
924       (unless (= (length patchsyms) (length sorted-patchsyms))
925         (error "Internal error"))
926
927       (while sorted-patchsyms
928         (setq sorted-patchsyms
929               (and (stgit-capture-output nil
930                      (if (eq target-patch :bottom)
931                          (stgit-run "sink" "--" (car sorted-patchsyms))
932                        (stgit-run "sink" "--to" target-patch "--"
933                                   (car sorted-patchsyms))))
934                    (cdr sorted-patchsyms))))))
935   (stgit-reload))
936
937 (defun stgit-squash (patchsyms)
938   "Squash the patches in PATCHSYMS.
939 Interactively, squash the marked patches."
940   (interactive (list stgit-marked-patches))
941   (when (< (length patchsyms) 2)
942     (error "Need at least two patches to squash"))
943   (let ((edit-buf (get-buffer-create "*StGit edit*"))
944         (dir default-directory))
945     (log-edit 'stgit-confirm-squash t nil edit-buf)
946     (set (make-local-variable 'stgit-patchsyms) patchsyms)
947     (setq default-directory dir)
948     (let ((standard-output edit-buf))
949       (apply 'stgit-run-silent "squash" "--save-template=-" patchsyms))))
950
951 (defun stgit-confirm-squash ()
952   (interactive)
953   (let ((file (make-temp-file "stgit-edit-")))
954     (write-region (point-min) (point-max) file)
955     (stgit-capture-output nil
956       (apply 'stgit-run "squash" "-f" file stgit-patchsyms))
957     (with-current-buffer log-edit-parent-buffer
958       (stgit-clear-marks)
959       ;; Go to first marked patch and stay there
960       (goto-char (point-min))
961       (re-search-forward (concat "^[>+-]\\*") nil t)
962       (move-to-column goal-column)
963       (let ((pos (point)))
964         (stgit-reload)
965         (goto-char pos)))))
966
967 (defun stgit-help ()
968   "Display help for the StGit mode."
969   (interactive)
970   (describe-function 'stgit-mode))
971
972 (defun stgit-undo (&optional arg)
973   "Run stg undo.
974 With prefix argument, run it with the --hard flag."
975   (interactive "P")
976   (stgit-capture-output nil
977     (if arg
978         (stgit-run "undo" "--hard")
979       (stgit-run "undo")))
980   (stgit-reload))
981
982 (defun stgit-refresh (&optional arg)
983   "Run stg refresh.
984 With prefix argument, refresh the marked patch or the patch under point."
985   (interactive "P")
986   (let ((patchargs (if arg
987                        (let ((patches (stgit-patches-marked-or-at-point)))
988                          (cond ((null patches)
989                                 (error "No patch to update"))
990                                ((> (length patches) 1)
991                                 (error "Too many patches selected"))
992                                (t
993                                 (cons "-p" patches))))
994                      nil)))
995     (stgit-capture-output nil
996       (apply 'stgit-run "refresh" patchargs))
997     (stgit-refresh-git-status))
998   (stgit-reload))
999
1000 (provide 'stgit)