chiark / gitweb /
stgit.el: Manually draw/erase the mark in stgit-{add,remove}-mark
[stgit] / contrib / stgit.el
CommitLineData
3a59f3db
KH
1;; stgit.el: An emacs mode for StGit
2;;
3;; Copyright (C) 2007 David Kågedal <davidk@lysator.liu.se>
4;;
5;; To install: put this file on the load-path and place the following
6;; in your .emacs file:
7;;
8;; (require 'stgit)
9;;
10;; To start: `M-x stgit'
11
0f076fe6
GH
12(require 'git nil t)
13
56d81fe5 14(defun stgit (dir)
a53347d9 15 "Manage StGit patches for the tree in DIR."
56d81fe5 16 (interactive "DDirectory: \n")
52144ce5 17 (switch-to-stgit-buffer (git-get-top-dir dir))
1f0bf00f 18 (stgit-reload))
56d81fe5 19
074a4fb0
GH
20(unless (fboundp 'git-get-top-dir)
21 (defun git-get-top-dir (dir)
22 "Retrieve the top-level directory of a git tree."
23 (let ((cdup (with-output-to-string
24 (with-current-buffer standard-output
25 (cd dir)
26 (unless (eq 0 (call-process "git" nil t nil
27 "rev-parse" "--show-cdup"))
df283a8b 28 (error "Cannot find top-level git tree for %s" dir))))))
074a4fb0
GH
29 (expand-file-name (concat (file-name-as-directory dir)
30 (car (split-string cdup "\n")))))))
31
32(defun stgit-refresh-git-status (&optional dir)
33 "If it exists, refresh the `git-status' buffer belonging to
34directory DIR or `default-directory'"
35 (when (and (fboundp 'git-find-status-buffer)
36 (fboundp 'git-refresh-status))
37 (let* ((top-dir (git-get-top-dir (or dir default-directory)))
38 (git-status-buffer (and top-dir (git-find-status-buffer top-dir))))
39 (when git-status-buffer
40 (with-current-buffer git-status-buffer
41 (git-refresh-status))))))
52144ce5 42
56d81fe5 43(defun switch-to-stgit-buffer (dir)
a53347d9 44 "Switch to a (possibly new) buffer displaying StGit patches for DIR."
56d81fe5
DK
45 (setq dir (file-name-as-directory dir))
46 (let ((buffers (buffer-list)))
47 (while (and buffers
48 (not (with-current-buffer (car buffers)
49 (and (eq major-mode 'stgit-mode)
50 (string= default-directory dir)))))
51 (setq buffers (cdr buffers)))
52 (switch-to-buffer (if buffers
53 (car buffers)
54 (create-stgit-buffer dir)))))
55
56(defun create-stgit-buffer (dir)
57 "Create a buffer for showing StGit patches.
58Argument DIR is the repository path."
59 (let ((buf (create-file-buffer (concat dir "*stgit*")))
60 (inhibit-read-only t))
61 (with-current-buffer buf
62 (setq default-directory dir)
63 (stgit-mode)
64 (setq buffer-read-only t))
65 buf))
66
67(defmacro stgit-capture-output (name &rest body)
a53347d9 68 "Capture StGit output and show it in a window at the end."
34afb86c
DK
69 `(let ((output-buf (get-buffer-create ,(or name "*StGit output*")))
70 (stgit-dir default-directory)
71 (inhibit-read-only t))
56d81fe5 72 (with-current-buffer output-buf
34afb86c
DK
73 (erase-buffer)
74 (setq default-directory stgit-dir)
75 (setq buffer-read-only t))
56d81fe5
DK
76 (let ((standard-output output-buf))
77 ,@body)
34afb86c
DK
78 (with-current-buffer output-buf
79 (set-buffer-modified-p nil)
80 (setq buffer-read-only t)
81 (if (< (point-min) (point-max))
82 (display-buffer output-buf t)))))
56d81fe5
DK
83(put 'stgit-capture-output 'lisp-indent-function 1)
84
d51722b7
GH
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
9aecd505 96(defun stgit-run-silent (&rest args)
d51722b7 97 (setq args (stgit-make-run-args args))
56d81fe5
DK
98 (apply 'call-process "stg" nil standard-output nil args))
99
9aecd505 100(defun stgit-run (&rest args)
d51722b7 101 (setq args (stgit-make-run-args args))
9aecd505
DK
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
378a003d 107(defun stgit-run-git (&rest args)
d51722b7 108 (setq args (stgit-make-run-args args))
378a003d
GH
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
1f60181a 114(defun stgit-run-git-silent (&rest args)
d51722b7 115 (setq args (stgit-make-run-args args))
1f60181a
GH
116 (apply 'call-process "git" nil standard-output nil args))
117
1f0bf00f 118(defun stgit-reload ()
a53347d9 119 "Update the contents of the StGit buffer."
56d81fe5
DK
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: ")
9aecd505 126 (stgit-run-silent "branch")
bce79a6a 127 (stgit-run-silent "series" "--description" "--empty")
6df83d42 128 (stgit-rescan)
56d81fe5
DK
129 (if curpatch
130 (stgit-goto-patch curpatch)
074a4fb0
GH
131 (goto-line curline)))
132 (stgit-refresh-git-status))
56d81fe5 133
8f40753a
GH
134(defgroup stgit nil
135 "A user interface for the StGit patch maintenance tool."
136 :group 'tools)
137
07f464e0
DK
138(defface stgit-description-face
139 '((((background dark)) (:foreground "tan"))
140 (((background light)) (:foreground "dark red")))
8f40753a
GH
141 "The face used for StGit descriptions"
142 :group 'stgit)
07f464e0
DK
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)))
8f40753a
GH
148 "The face used for the top patch names"
149 :group 'stgit)
07f464e0
DK
150
151(defface stgit-applied-patch-face
152 '((((background dark)) (:foreground "light yellow"))
153 (((background light)) (:foreground "purple"))
154 (t ()))
8f40753a
GH
155 "The face used for applied patch names"
156 :group 'stgit)
07f464e0
DK
157
158(defface stgit-unapplied-patch-face
159 '((((background dark)) (:foreground "gray80"))
160 (((background light)) (:foreground "orchid"))
161 (t ()))
8f40753a
GH
162 "The face used for unapplied patch names"
163 :group 'stgit)
07f464e0 164
1f60181a
GH
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
a6d9a852
GH
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
1f60181a
GH
189(defcustom stgit-expand-find-copies-harder
190 nil
191 "Try harder to find copied files when listing patches.
192
193When not nil, runs git diff-tree with the --find-copies-harder
194flag, 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)
a6d9a852
GH
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)))
1f60181a
GH
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"
a6d9a852
GH
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))))))
1f60181a 223
a6d9a852 224(defun stgit-file-status-code (str &optional score)
1f60181a
GH
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)))))
a6d9a852
GH
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)
47271f41
GH
248 "Return string describing file type TYPE (the high bits of file permission).
249Cf. `stgit-file-type-strings' and `stgit-file-type-change-string'."
a6d9a852
GH
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)
47271f41
GH
255 "Return string describing file type change from OLD-PERM to NEW-PERM.
256Cf. `stgit-file-type-string'."
a6d9a852
GH
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)
47271f41
GH
270 "Return string describing file mode change from OLD-PERM to NEW-PERM.
271Cf. `stgit-file-type-change-string'."
a6d9a852
GH
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)))))))
1f60181a 293
378a003d 294(defun stgit-expand-patch (patchsym)
47271f41
GH
295 "Expand (show modification of) the patch with name PATCHSYM (a
296symbol) at point.
297`stgit-expand-find-copies-harder' controls how hard to try to
298find copied files."
378a003d
GH
299 (save-excursion
300 (forward-line)
1f60181a
GH
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")
d51722b7 307 (stgit-id patchsym)))))
1f60181a 308 (let (mstart)
a6d9a852 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]*\\)\\)"
1f60181a 310 result mstart)
a6d9a852
GH
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))))
1f60181a 314 (line-start (point))
a6d9a852
GH
315 status
316 change
1f60181a
GH
317 properties)
318 (insert " ")
319 (if copy-or-rename
a6d9a852
GH
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))
1f60181a
GH
347 (add-text-properties line-start (point) properties))
348 (setq mstart (match-end 0))))
349 (when (= start (point))
350 (insert " <no files>\n"))
d51722b7 351 (put-text-property start (point) 'stgit-file-patchsym patchsym))))
378a003d 352
acc5652f
DK
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
6df83d42
DK
362(defun stgit-rescan ()
363 "Rescan the status buffer."
07f464e0 364 (save-excursion
3a1cf814
GH
365 (let ((marked ())
366 found-any)
6df83d42
DK
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))
bce79a6a 372 ((looking-at "\\([0 ]\\)\\([>+-]\\)\\( \\)\\([^ ]+\\) *[|#] \\(.*\\)")
3a1cf814 373 (setq found-any t)
bce79a6a
GH
374 (let ((empty (match-string 1))
375 (state (match-string 2))
376 (patchsym (intern (match-string 4))))
6df83d42 377 (put-text-property
bce79a6a 378 (match-beginning 4) (match-end 4) 'face
6df83d42
DK
379 (cond ((string= state ">") 'stgit-top-patch-face)
380 ((string= state "+") 'stgit-applied-patch-face)
381 ((string= state "-") 'stgit-unapplied-patch-face)))
bce79a6a 382 (put-text-property (match-beginning 5) (match-end 5)
6df83d42
DK
383 'face 'stgit-description-face)
384 (when (memq patchsym stgit-marked-patches)
bce79a6a
GH
385 (save-excursion
386 (replace-match "*" nil nil nil 3))
378a003d 387 (setq marked (cons patchsym marked)))
d51722b7
GH
388 (put-text-property (match-beginning 0) (match-end 0)
389 'stgit-patchsym patchsym)
378a003d
GH
390 (when (memq patchsym stgit-expanded-patches)
391 (stgit-expand-patch patchsym))
bce79a6a
GH
392 (when (equal "0" empty)
393 (save-excursion
394 (goto-char (match-beginning 5))
395 (insert "(empty) ")))
396 (delete-char 1)
378a003d 397 ))
ad80ce22
DK
398 ((or (looking-at "stg series: Branch \".*\" not initialised")
399 (looking-at "stg series: .*: branch not initialized"))
3a1cf814 400 (setq found-any t)
1c2426dc
DK
401 (forward-line 1)
402 (insert "Run M-x stgit-init to initialise")))
6df83d42 403 (forward-line 1))
3a1cf814
GH
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))))))
07f464e0 409
acc5652f
DK
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
378a003d
GH
429(defun stgit-select ()
430 "Expand or collapse the current entry"
431 (interactive)
432 (let ((curpatch (stgit-patch-at-point)))
acc5652f
DK
433 (if curpatch
434 (stgit-toggle-patch-file-list curpatch)
435 (stgit-select-file))))
436
378a003d
GH
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
83327d53 449(defun stgit-quit ()
a53347d9 450 "Hide the stgit buffer."
83327d53
GH
451 (interactive)
452 (bury-buffer))
453
0f076fe6 454(defun stgit-git-status ()
a53347d9 455 "Show status using `git-status'."
0f076fe6
GH
456 (interactive)
457 (unless (fboundp 'git-status)
df283a8b 458 (error "The stgit-git-status command requires git-status"))
0f076fe6
GH
459 (let ((dir default-directory))
460 (save-selected-window
461 (pop-to-buffer nil)
462 (git-status dir))))
463
378a003d
GH
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
56d81fe5
DK
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)
022a3664
GH
511 (mapc (lambda (arg) (define-key stgit-mode-map (car arg) (cdr arg)))
512 '((" " . stgit-mark)
3dccdc9b 513 ("m" . stgit-mark)
9b151b27
GH
514 ("\d" . stgit-unmark-up)
515 ("u" . stgit-unmark-down)
022a3664
GH
516 ("?" . stgit-help)
517 ("h" . stgit-help)
378a003d
GH
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)
0f076fe6 524 ("s" . stgit-git-status)
022a3664
GH
525 ("g" . stgit-reload)
526 ("r" . stgit-refresh)
527 ("\C-c\C-r" . stgit-rename)
528 ("e" . stgit-edit)
594aa463 529 ("S" . stgit-squash)
022a3664
GH
530 ("N" . stgit-new)
531 ("R" . stgit-repair)
532 ("C" . stgit-commit)
533 ("U" . stgit-uncommit)
378a003d
GH
534 ("\r" . stgit-select)
535 ("o" . stgit-find-file-other-window)
022a3664
GH
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)
83327d53 543 ("\C-_" . stgit-undo)
adeef6bc
GH
544 ("B" . stgit-branch)
545 ("q" . stgit-quit))))
56d81fe5
DK
546
547(defun stgit-mode ()
548 "Major mode for interacting with StGit.
549Commands:
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)
6df83d42 558 (set (make-local-variable 'stgit-marked-patches) nil)
378a003d 559 (set (make-local-variable 'stgit-expanded-patches) nil)
2870f8b8 560 (set-variable 'truncate-lines 't)
56d81fe5
DK
561 (run-hooks 'stgit-mode-hook))
562
d51722b7
GH
563(defun stgit-add-mark (patchsym)
564 "Mark the patch PATCHSYM."
980ccd21
GH
565 (setq stgit-marked-patches (cons patchsym stgit-marked-patches))
566 (save-excursion
567 (when (stgit-goto-patch patchsym)
568 (move-to-column 1)
569 (let ((inhibit-read-only t))
570 (insert-and-inherit ?*)
571 (delete-char 1)))))
6df83d42 572
d51722b7
GH
573(defun stgit-remove-mark (patchsym)
574 "Unmark the patch PATCHSYM."
980ccd21
GH
575 (setq stgit-marked-patches (delq patchsym stgit-marked-patches))
576 (save-excursion
577 (when (stgit-goto-patch patchsym)
578 (move-to-column 1)
579 (let ((inhibit-read-only t))
580 (insert-and-inherit ? )
581 (delete-char 1)))))
6df83d42 582
e6b1fdae 583(defun stgit-clear-marks ()
47271f41 584 "Unmark all patches."
e6b1fdae
DK
585 (setq stgit-marked-patches '()))
586
378a003d 587(defun stgit-patch-at-point (&optional cause-error allow-file)
d51722b7 588 "Return the patch name on the current line as a symbol.
378a003d
GH
589If CAUSE-ERROR is not nil, signal an error if none found.
590If ALLOW-FILE is not nil, also handle when point is on a file of
591a patch."
d51722b7
GH
592 (or (get-text-property (point) 'stgit-patchsym)
593 (and allow-file
594 (get-text-property (point) 'stgit-file-patchsym))
595 (when cause-error
596 (error "No patch on this line"))))
378a003d 597
1f60181a
GH
598(defun stgit-patched-file-at-point (&optional both-files)
599 "Returns a cons of the patchsym and file name at point. For
600copies and renames, return the new file if the patch is either
601applied. If BOTH-FILES is non-nil, return a cons of the old and
602the new file names instead of just one name."
d51722b7 603 (let ((patchsym (get-text-property (point) 'stgit-file-patchsym))
1f60181a
GH
604 (file (get-text-property (point) 'stgit-file)))
605 (cond ((not patchsym) nil)
606 (file (cons patchsym file))
607 (both-files
608 (cons patchsym (cons (get-text-property (point) 'stgit-old-file)
609 (get-text-property (point) 'stgit-new-file))))
610 (t
611 (let ((file-sym (save-excursion
612 (stgit-previous-patch)
d51722b7
GH
613 (unless (eq (stgit-patch-at-point)
614 patchsym)
1f60181a
GH
615 (error "Cannot find the %s patch" patchsym))
616 (beginning-of-line)
617 (if (= (char-after) ?-)
618 'stgit-old-file
619 'stgit-new-file))))
620 (cons patchsym (get-text-property (point) file-sym)))))))
56d81fe5 621
7755d7f1 622(defun stgit-patches-marked-or-at-point ()
d51722b7 623 "Return the symbols of the marked patches, or the patch on the current line."
7755d7f1 624 (if stgit-marked-patches
d51722b7 625 stgit-marked-patches
7755d7f1
KH
626 (let ((patch (stgit-patch-at-point)))
627 (if patch
628 (list patch)
629 '()))))
630
d51722b7
GH
631(defun stgit-goto-patch (patchsym)
632 "Move point to the line containing patch PATCHSYM.
633If that patch cannot be found, return nil."
634 (let ((p (text-property-any (point-min) (point-max)
635 'stgit-patchsym patchsym)))
636 (when p
56d81fe5 637 (goto-char p)
d51722b7 638 (move-to-column goal-column))))
56d81fe5 639
1c2426dc 640(defun stgit-init ()
a53347d9 641 "Run stg init."
1c2426dc
DK
642 (interactive)
643 (stgit-capture-output nil
b0424080 644 (stgit-run "init"))
1f0bf00f 645 (stgit-reload))
1c2426dc 646
6df83d42 647(defun stgit-mark ()
a53347d9 648 "Mark the patch under point."
6df83d42 649 (interactive)
018fa1ac 650 (let ((patch (stgit-patch-at-point t)))
980ccd21 651 (stgit-add-mark patch))
378a003d 652 (stgit-next-patch))
6df83d42 653
9b151b27 654(defun stgit-unmark-up ()
a53347d9 655 "Remove mark from the patch on the previous line."
6df83d42 656 (interactive)
378a003d 657 (stgit-previous-patch)
980ccd21 658 (stgit-remove-mark (stgit-patch-at-point t)))
9b151b27
GH
659
660(defun stgit-unmark-down ()
a53347d9 661 "Remove mark from the patch on the current line."
9b151b27 662 (interactive)
018fa1ac 663 (stgit-remove-mark (stgit-patch-at-point t))
1288eda2 664 (stgit-next-patch))
6df83d42 665
56d81fe5 666(defun stgit-rename (name)
018fa1ac 667 "Rename the patch under point to NAME."
d51722b7
GH
668 (interactive (list (read-string "Patch name: "
669 (symbol-name (stgit-patch-at-point t)))))
670 (let ((old-patchsym (stgit-patch-at-point t)))
56d81fe5 671 (stgit-capture-output nil
d51722b7
GH
672 (stgit-run "rename" old-patchsym name))
673 (let ((name-sym (intern name)))
674 (when (memq old-patchsym stgit-expanded-patches)
378a003d 675 (setq stgit-expanded-patches
d51722b7
GH
676 (cons name-sym (delq old-patchsym stgit-expanded-patches))))
677 (when (memq old-patchsym stgit-marked-patches)
378a003d 678 (setq stgit-marked-patches
d51722b7
GH
679 (cons name-sym (delq old-patchsym stgit-marked-patches))))
680 (stgit-reload)
681 (stgit-goto-patch name-sym))))
56d81fe5 682
26201d96 683(defun stgit-repair ()
a53347d9 684 "Run stg repair."
26201d96
DK
685 (interactive)
686 (stgit-capture-output nil
b0424080 687 (stgit-run "repair"))
1f0bf00f 688 (stgit-reload))
26201d96 689
adeef6bc
GH
690(defun stgit-available-branches ()
691 "Returns a list of the available stg branches"
692 (let ((output (with-output-to-string
693 (stgit-run "branch" "--list")))
694 (start 0)
695 result)
696 (while (string-match "^>?\\s-+s\\s-+\\(\\S-+\\)" output start)
697 (setq result (cons (match-string 1 output) result))
698 (setq start (match-end 0)))
699 result))
700
701(defun stgit-branch (branch)
702 "Switch to branch BRANCH."
703 (interactive (list (completing-read "Switch to branch: "
704 (stgit-available-branches))))
705 (stgit-capture-output nil (stgit-run "branch" "--" branch))
706 (stgit-reload))
707
41c1c59c
GH
708(defun stgit-commit (count)
709 "Run stg commit on COUNT commits.
710Interactively, the prefix argument is used as COUNT."
711 (interactive "p")
712 (stgit-capture-output nil (stgit-run "commit" "-n" count))
1f0bf00f 713 (stgit-reload))
c4aad9a7 714
41c1c59c
GH
715(defun stgit-uncommit (count)
716 "Run stg uncommit on COUNT commits.
717Interactively, the prefix argument is used as COUNT."
c4aad9a7 718 (interactive "p")
41c1c59c 719 (stgit-capture-output nil (stgit-run "uncommit" "-n" count))
1f0bf00f 720 (stgit-reload))
c4aad9a7 721
0b661144
DK
722(defun stgit-push-next (npatches)
723 "Push the first unapplied patch.
724With numeric prefix argument, push that many patches."
725 (interactive "p")
d51722b7 726 (stgit-capture-output nil (stgit-run "push" "-n" npatches))
074a4fb0
GH
727 (stgit-reload)
728 (stgit-refresh-git-status))
56d81fe5 729
0b661144
DK
730(defun stgit-pop-next (npatches)
731 "Pop the topmost applied patch.
732With numeric prefix argument, pop that many patches."
733 (interactive "p")
d51722b7 734 (stgit-capture-output nil (stgit-run "pop" "-n" npatches))
074a4fb0
GH
735 (stgit-reload)
736 (stgit-refresh-git-status))
56d81fe5 737
f9182fca
KH
738(defun stgit-applied-at-point ()
739 "Is the patch on the current line applied?"
740 (save-excursion
741 (beginning-of-line)
742 (looking-at "[>+]")))
743
744(defun stgit-push-or-pop ()
a53347d9 745 "Push or pop the patch on the current line."
f9182fca 746 (interactive)
d51722b7 747 (let ((patchsym (stgit-patch-at-point t))
f9182fca
KH
748 (applied (stgit-applied-at-point)))
749 (stgit-capture-output nil
d51722b7 750 (stgit-run (if applied "pop" "push") patchsym))
1f0bf00f 751 (stgit-reload)))
f9182fca 752
c7adf5ef 753(defun stgit-goto ()
a53347d9 754 "Go to the patch on the current line."
c7adf5ef 755 (interactive)
d51722b7 756 (let ((patchsym (stgit-patch-at-point t)))
c7adf5ef 757 (stgit-capture-output nil
d51722b7 758 (stgit-run "goto" patchsym))
1f0bf00f 759 (stgit-reload)))
c7adf5ef 760
d51722b7
GH
761(defun stgit-id (patchsym)
762 "Return the git commit id for PATCHSYM."
378a003d 763 (let ((result (with-output-to-string
d51722b7 764 (stgit-run-silent "id" patchsym))))
378a003d 765 (unless (string-match "^\\([0-9A-Fa-f]\\{40\\}\\)$" result)
d51722b7 766 (error "Cannot find commit id for %s" patchsym))
378a003d
GH
767 (match-string 1 result)))
768
56d81fe5 769(defun stgit-show ()
a53347d9 770 "Show the patch on the current line."
56d81fe5
DK
771 (interactive)
772 (stgit-capture-output "*StGit patch*"
d51722b7
GH
773 (let ((patchsym (stgit-patch-at-point)))
774 (if (not patchsym)
1f60181a 775 (let ((patched-file (stgit-patched-file-at-point t)))
378a003d
GH
776 (unless patched-file
777 (error "No patch or file at point"))
d51722b7 778 (let ((id (stgit-id (car patched-file))))
5f1b0013
GH
779 (if (consp (cdr patched-file))
780 ;; two files (copy or rename)
781 (stgit-run-git "diff" "-C" "-C" (concat id "^") id "--"
782 (cadr patched-file) (cddr patched-file))
783 ;; just one file
784 (stgit-run-git "diff" (concat id "^") id "--"
785 (cdr patched-file)))))
b557489f 786 (stgit-run "show" "-O" "--patch-with-stat" patchsym))
5f1b0013
GH
787 (with-current-buffer standard-output
788 (goto-char (point-min))
789 (diff-mode)))))
0663524d 790
0bca35c8 791(defun stgit-edit ()
a53347d9 792 "Edit the patch on the current line."
0bca35c8 793 (interactive)
d51722b7 794 (let ((patchsym (stgit-patch-at-point t))
0780be79 795 (edit-buf (get-buffer-create "*StGit edit*"))
0bca35c8
DK
796 (dir default-directory))
797 (log-edit 'stgit-confirm-edit t nil edit-buf)
d51722b7 798 (set (make-local-variable 'stgit-edit-patchsym) patchsym)
0bca35c8
DK
799 (setq default-directory dir)
800 (let ((standard-output edit-buf))
d51722b7 801 (stgit-run-silent "edit" "--save-template=-" patchsym))))
0bca35c8
DK
802
803(defun stgit-confirm-edit ()
804 (interactive)
805 (let ((file (make-temp-file "stgit-edit-")))
806 (write-region (point-min) (point-max) file)
807 (stgit-capture-output nil
d51722b7 808 (stgit-run "edit" "-f" file stgit-edit-patchsym))
0bca35c8 809 (with-current-buffer log-edit-parent-buffer
1f0bf00f 810 (stgit-reload))))
0bca35c8 811
aa04f831
GH
812(defun stgit-new (add-sign)
813 "Create a new patch.
814With a prefix argument, include a \"Signed-off-by:\" line at the
815end of the patch."
816 (interactive "P")
c5d45b92
GH
817 (let ((edit-buf (get-buffer-create "*StGit edit*"))
818 (dir default-directory))
819 (log-edit 'stgit-confirm-new t nil edit-buf)
aa04f831
GH
820 (setq default-directory dir)
821 (when add-sign
822 (save-excursion
823 (let ((standard-output (current-buffer)))
824 (stgit-run-silent "new" "--sign" "--save-template=-"))))))
64c097a0
DK
825
826(defun stgit-confirm-new ()
827 (interactive)
27b0f9e4 828 (let ((file (make-temp-file "stgit-edit-")))
64c097a0
DK
829 (write-region (point-min) (point-max) file)
830 (stgit-capture-output nil
27b0f9e4 831 (stgit-run "new" "-f" file))
64c097a0 832 (with-current-buffer log-edit-parent-buffer
1f0bf00f 833 (stgit-reload))))
64c097a0
DK
834
835(defun stgit-create-patch-name (description)
836 "Create a patch name from a long description"
837 (let ((patch ""))
838 (while (> (length description) 0)
839 (cond ((string-match "\\`[a-zA-Z_-]+" description)
8439f657
GH
840 (setq patch (downcase (concat patch
841 (match-string 0 description))))
64c097a0
DK
842 (setq description (substring description (match-end 0))))
843 ((string-match "\\` +" description)
844 (setq patch (concat patch "-"))
845 (setq description (substring description (match-end 0))))
846 ((string-match "\\`[^a-zA-Z_-]+" description)
847 (setq description (substring description (match-end 0))))))
848 (cond ((= (length patch) 0)
849 "patch")
850 ((> (length patch) 20)
851 (substring patch 0 20))
852 (t patch))))
0bca35c8 853
9008e45b 854(defun stgit-delete (patchsyms &optional spill-p)
d51722b7 855 "Delete the patches in PATCHSYMS.
9008e45b
GH
856Interactively, delete the marked patches, or the patch at point.
857
858With a prefix argument, or SPILL-P, spill the patch contents to
859the work tree and index."
860 (interactive (list (stgit-patches-marked-or-at-point)
861 current-prefix-arg))
e7231e4f
GH
862 (unless patchsyms
863 (error "No patches to delete"))
d51722b7 864 (let ((npatches (length patchsyms)))
9008e45b 865 (when (yes-or-no-p (format "Really delete %d patch%s%s? "
e7231e4f 866 npatches
9008e45b
GH
867 (if (= 1 npatches) "" "es")
868 (if spill-p
869 " (spilling contents to index)"
870 "")))
871 (let ((args (if spill-p
872 (cons "--spill" patchsyms)
873 patchsyms)))
874 (stgit-capture-output nil
875 (apply 'stgit-run "delete" args))
876 (stgit-reload)))))
d51722b7 877
594aa463
KH
878(defun stgit-squash (patchsyms)
879 "Squash the patches in PATCHSYMS.
880Interactively, squash the marked patches."
d51722b7
GH
881 (interactive (list stgit-marked-patches))
882 (when (< (length patchsyms) 2)
594aa463 883 (error "Need at least two patches to squash"))
0780be79 884 (let ((edit-buf (get-buffer-create "*StGit edit*"))
ea0def18 885 (dir default-directory))
594aa463 886 (log-edit 'stgit-confirm-squash t nil edit-buf)
d51722b7 887 (set (make-local-variable 'stgit-patchsyms) patchsyms)
ea0def18
DK
888 (setq default-directory dir)
889 (let ((standard-output edit-buf))
594aa463 890 (apply 'stgit-run-silent "squash" "--save-template=-" patchsyms))))
ea0def18 891
594aa463 892(defun stgit-confirm-squash ()
ea0def18
DK
893 (interactive)
894 (let ((file (make-temp-file "stgit-edit-")))
895 (write-region (point-min) (point-max) file)
896 (stgit-capture-output nil
594aa463 897 (apply 'stgit-run "squash" "-f" file stgit-patchsyms))
ea0def18 898 (with-current-buffer log-edit-parent-buffer
e6b1fdae
DK
899 (stgit-clear-marks)
900 ;; Go to first marked patch and stay there
901 (goto-char (point-min))
902 (re-search-forward (concat "^[>+-]\\*") nil t)
903 (move-to-column goal-column)
904 (let ((pos (point)))
1f0bf00f 905 (stgit-reload)
e6b1fdae 906 (goto-char pos)))))
ea0def18 907
0663524d
KH
908(defun stgit-help ()
909 "Display help for the StGit mode."
910 (interactive)
911 (describe-function 'stgit-mode))
3a59f3db 912
83e51dbf
DK
913(defun stgit-undo (&optional arg)
914 "Run stg undo.
915With prefix argument, run it with the --hard flag."
916 (interactive "P")
917 (stgit-capture-output nil
918 (if arg
919 (stgit-run "undo" "--hard")
920 (stgit-run "undo")))
1f0bf00f 921 (stgit-reload))
83e51dbf 922
4d73c4d8
DK
923(defun stgit-refresh (&optional arg)
924 "Run stg refresh.
a53347d9 925With prefix argument, refresh the marked patch or the patch under point."
4d73c4d8
DK
926 (interactive "P")
927 (let ((patchargs (if arg
b0424080
GH
928 (let ((patches (stgit-patches-marked-or-at-point)))
929 (cond ((null patches)
df283a8b 930 (error "No patch to update"))
b0424080 931 ((> (length patches) 1)
df283a8b 932 (error "Too many patches selected"))
b0424080
GH
933 (t
934 (cons "-p" patches))))
935 nil)))
4d73c4d8 936 (stgit-capture-output nil
074a4fb0
GH
937 (apply 'stgit-run "refresh" patchargs))
938 (stgit-refresh-git-status))
4d73c4d8
DK
939 (stgit-reload))
940
3a59f3db 941(provide 'stgit)