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