stgit.el: Invalidate ewoc node when marking or unmarking instead of modifying buffer...
[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."
8036afdd 570 (setq stgit-marked-patches (cons patchsym stgit-marked-patches)))
6df83d42 571
d51722b7
GH
572(defun stgit-remove-mark (patchsym)
573 "Unmark the patch PATCHSYM."
8036afdd 574 (setq stgit-marked-patches (delq patchsym stgit-marked-patches)))
6df83d42 575
e6b1fdae 576(defun stgit-clear-marks ()
47271f41 577 "Unmark all patches."
e6b1fdae
DK
578 (setq stgit-marked-patches '()))
579
735cb7ec 580(defun stgit-patch-at-point (&optional cause-error)
2c862b07
DK
581 (get-text-property (point) 'patch-data))
582
583(defun stgit-patch-name-at-point (&optional cause-error)
d51722b7 584 "Return the patch name on the current line as a symbol.
735cb7ec 585If CAUSE-ERROR is not nil, signal an error if none found."
2c862b07
DK
586 (let ((patch (stgit-patch-at-point)))
587 (cond (patch
588 (stgit-patch-name patch))
589 (cause-error
590 (error "No patch on this line")))))
378a003d 591
1f60181a
GH
592(defun stgit-patched-file-at-point (&optional both-files)
593 "Returns a cons of the patchsym and file name at point. For
594copies and renames, return the new file if the patch is either
595applied. If BOTH-FILES is non-nil, return a cons of the old and
596the new file names instead of just one name."
d51722b7 597 (let ((patchsym (get-text-property (point) 'stgit-file-patchsym))
1f60181a
GH
598 (file (get-text-property (point) 'stgit-file)))
599 (cond ((not patchsym) nil)
600 (file (cons patchsym file))
601 (both-files
602 (cons patchsym (cons (get-text-property (point) 'stgit-old-file)
603 (get-text-property (point) 'stgit-new-file))))
604 (t
605 (let ((file-sym (save-excursion
606 (stgit-previous-patch)
2c862b07 607 (unless (eq (stgit-patch-name-at-point)
d51722b7 608 patchsym)
1f60181a
GH
609 (error "Cannot find the %s patch" patchsym))
610 (beginning-of-line)
611 (if (= (char-after) ?-)
612 'stgit-old-file
613 'stgit-new-file))))
614 (cons patchsym (get-text-property (point) file-sym)))))))
56d81fe5 615
7755d7f1 616(defun stgit-patches-marked-or-at-point ()
d51722b7 617 "Return the symbols of the marked patches, or the patch on the current line."
7755d7f1 618 (if stgit-marked-patches
d51722b7 619 stgit-marked-patches
2c862b07 620 (let ((patch (stgit-patch-name-at-point)))
7755d7f1
KH
621 (if patch
622 (list patch)
623 '()))))
624
d51722b7
GH
625(defun stgit-goto-patch (patchsym)
626 "Move point to the line containing patch PATCHSYM.
627If that patch cannot be found, return nil."
628 (let ((p (text-property-any (point-min) (point-max)
629 'stgit-patchsym patchsym)))
630 (when p
56d81fe5 631 (goto-char p)
d51722b7 632 (move-to-column goal-column))))
56d81fe5 633
1c2426dc 634(defun stgit-init ()
a53347d9 635 "Run stg init."
1c2426dc
DK
636 (interactive)
637 (stgit-capture-output nil
b0424080 638 (stgit-run "init"))
1f0bf00f 639 (stgit-reload))
1c2426dc 640
6df83d42 641(defun stgit-mark ()
a53347d9 642 "Mark the patch under point."
6df83d42 643 (interactive)
8036afdd
DK
644 (let* ((node (ewoc-locate stgit-ewoc))
645 (patch (ewoc-data node)))
646 (stgit-add-mark (stgit-patch-name patch))
647 (ewoc-invalidate stgit-ewoc node))
378a003d 648 (stgit-next-patch))
6df83d42 649
9b151b27 650(defun stgit-unmark-up ()
a53347d9 651 "Remove mark from the patch on the previous line."
6df83d42 652 (interactive)
378a003d 653 (stgit-previous-patch)
8036afdd
DK
654 (let* ((node (ewoc-locate stgit-ewoc))
655 (patch (ewoc-data node)))
656 (stgit-remove-mark (stgit-patch-name patch))
657 (ewoc-invalidate stgit-ewoc node))
658 (move-to-column (stgit-goal-column)))
9b151b27
GH
659
660(defun stgit-unmark-down ()
a53347d9 661 "Remove mark from the patch on the current line."
9b151b27 662 (interactive)
8036afdd
DK
663 (let* ((node (ewoc-locate stgit-ewoc))
664 (patch (ewoc-data node)))
665 (stgit-remove-mark (stgit-patch-name patch))
666 (ewoc-invalidate stgit-ewoc node))
1288eda2 667 (stgit-next-patch))
6df83d42 668
56d81fe5 669(defun stgit-rename (name)
018fa1ac 670 "Rename the patch under point to NAME."
d51722b7 671 (interactive (list (read-string "Patch name: "
2c862b07
DK
672 (symbol-name (stgit-patch-name-at-point t)))))
673 (let ((old-patchsym (stgit-patch-name-at-point t)))
56d81fe5 674 (stgit-capture-output nil
d51722b7
GH
675 (stgit-run "rename" old-patchsym name))
676 (let ((name-sym (intern name)))
677 (when (memq old-patchsym stgit-expanded-patches)
378a003d 678 (setq stgit-expanded-patches
d51722b7
GH
679 (cons name-sym (delq old-patchsym stgit-expanded-patches))))
680 (when (memq old-patchsym stgit-marked-patches)
378a003d 681 (setq stgit-marked-patches
d51722b7
GH
682 (cons name-sym (delq old-patchsym stgit-marked-patches))))
683 (stgit-reload)
684 (stgit-goto-patch name-sym))))
56d81fe5 685
26201d96 686(defun stgit-repair ()
a53347d9 687 "Run stg repair."
26201d96
DK
688 (interactive)
689 (stgit-capture-output nil
b0424080 690 (stgit-run "repair"))
1f0bf00f 691 (stgit-reload))
26201d96 692
adeef6bc
GH
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
41c1c59c
GH
711(defun stgit-commit (count)
712 "Run stg commit on COUNT commits.
713Interactively, the prefix argument is used as COUNT."
714 (interactive "p")
715 (stgit-capture-output nil (stgit-run "commit" "-n" count))
1f0bf00f 716 (stgit-reload))
c4aad9a7 717
41c1c59c
GH
718(defun stgit-uncommit (count)
719 "Run stg uncommit on COUNT commits.
720Interactively, the prefix argument is used as COUNT."
c4aad9a7 721 (interactive "p")
41c1c59c 722 (stgit-capture-output nil (stgit-run "uncommit" "-n" count))
1f0bf00f 723 (stgit-reload))
c4aad9a7 724
0b661144
DK
725(defun stgit-push-next (npatches)
726 "Push the first unapplied patch.
727With numeric prefix argument, push that many patches."
728 (interactive "p")
d51722b7 729 (stgit-capture-output nil (stgit-run "push" "-n" npatches))
074a4fb0
GH
730 (stgit-reload)
731 (stgit-refresh-git-status))
56d81fe5 732
0b661144
DK
733(defun stgit-pop-next (npatches)
734 "Pop the topmost applied patch.
735With numeric prefix argument, pop that many patches."
736 (interactive "p")
d51722b7 737 (stgit-capture-output nil (stgit-run "pop" "-n" npatches))
074a4fb0
GH
738 (stgit-reload)
739 (stgit-refresh-git-status))
56d81fe5 740
f9182fca
KH
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 ()
a53347d9 748 "Push or pop the patch on the current line."
f9182fca 749 (interactive)
2c862b07 750 (let ((patchsym (stgit-patch-name-at-point t))
f9182fca
KH
751 (applied (stgit-applied-at-point)))
752 (stgit-capture-output nil
d51722b7 753 (stgit-run (if applied "pop" "push") patchsym))
1f0bf00f 754 (stgit-reload)))
f9182fca 755
c7adf5ef 756(defun stgit-goto ()
a53347d9 757 "Go to the patch on the current line."
c7adf5ef 758 (interactive)
2c862b07 759 (let ((patchsym (stgit-patch-name-at-point t)))
c7adf5ef 760 (stgit-capture-output nil
d51722b7 761 (stgit-run "goto" patchsym))
1f0bf00f 762 (stgit-reload)))
c7adf5ef 763
d51722b7 764(defun stgit-id (patchsym)
50d88c67
DK
765 "Return the git commit id for PATCHSYM.
766If PATCHSYM is a keyword, returns PATCHSYM unmodified."
767 (if (keywordp patchsym)
768 patchsym
769 (let ((result (with-output-to-string
770 (stgit-run-silent "id" patchsym))))
771 (unless (string-match "^\\([0-9A-Fa-f]\\{40\\}\\)$" result)
772 (error "Cannot find commit id for %s" patchsym))
773 (match-string 1 result))))
378a003d 774
56d81fe5 775(defun stgit-show ()
a53347d9 776 "Show the patch on the current line."
56d81fe5
DK
777 (interactive)
778 (stgit-capture-output "*StGit patch*"
50d88c67
DK
779 (case (get-text-property (point) 'entry-type)
780 ('file
2c862b07 781 (let ((patchsym (stgit-patch-name-at-point))
50d88c67
DK
782 (patched-file (stgit-patched-file-at-point t)))
783 (let ((id (stgit-id (car patched-file))))
784 (if (consp (cdr patched-file))
785 ;; two files (copy or rename)
786 (stgit-run-git "diff" "-C" "-C" (concat id "^") id "--"
787 (cadr patched-file) (cddr patched-file))
788 ;; just one file
789 (stgit-run-git "diff" (concat id "^") id "--"
790 (cdr patched-file))))))
791 ('patch
2c862b07
DK
792 (stgit-run "show" "-O" "--patch-with-stat" "-O" "-M"
793 (stgit-patch-name-at-point)))
50d88c67
DK
794 (t
795 (error "No patch or file at point")))
796 (with-current-buffer standard-output
797 (goto-char (point-min))
798 (diff-mode))))
0663524d 799
0bca35c8 800(defun stgit-edit ()
a53347d9 801 "Edit the patch on the current line."
0bca35c8 802 (interactive)
2c862b07 803 (let ((patchsym (stgit-patch-name-at-point t))
0780be79 804 (edit-buf (get-buffer-create "*StGit edit*"))
0bca35c8
DK
805 (dir default-directory))
806 (log-edit 'stgit-confirm-edit t nil edit-buf)
d51722b7 807 (set (make-local-variable 'stgit-edit-patchsym) patchsym)
0bca35c8
DK
808 (setq default-directory dir)
809 (let ((standard-output edit-buf))
d51722b7 810 (stgit-run-silent "edit" "--save-template=-" patchsym))))
0bca35c8
DK
811
812(defun stgit-confirm-edit ()
813 (interactive)
814 (let ((file (make-temp-file "stgit-edit-")))
815 (write-region (point-min) (point-max) file)
816 (stgit-capture-output nil
d51722b7 817 (stgit-run "edit" "-f" file stgit-edit-patchsym))
0bca35c8 818 (with-current-buffer log-edit-parent-buffer
1f0bf00f 819 (stgit-reload))))
0bca35c8 820
aa04f831
GH
821(defun stgit-new (add-sign)
822 "Create a new patch.
823With a prefix argument, include a \"Signed-off-by:\" line at the
824end of the patch."
825 (interactive "P")
c5d45b92
GH
826 (let ((edit-buf (get-buffer-create "*StGit edit*"))
827 (dir default-directory))
828 (log-edit 'stgit-confirm-new t nil edit-buf)
aa04f831
GH
829 (setq default-directory dir)
830 (when add-sign
831 (save-excursion
832 (let ((standard-output (current-buffer)))
833 (stgit-run-silent "new" "--sign" "--save-template=-"))))))
64c097a0
DK
834
835(defun stgit-confirm-new ()
836 (interactive)
27b0f9e4 837 (let ((file (make-temp-file "stgit-edit-")))
64c097a0
DK
838 (write-region (point-min) (point-max) file)
839 (stgit-capture-output nil
27b0f9e4 840 (stgit-run "new" "-f" file))
64c097a0 841 (with-current-buffer log-edit-parent-buffer
1f0bf00f 842 (stgit-reload))))
64c097a0
DK
843
844(defun stgit-create-patch-name (description)
845 "Create a patch name from a long description"
846 (let ((patch ""))
847 (while (> (length description) 0)
848 (cond ((string-match "\\`[a-zA-Z_-]+" description)
8439f657
GH
849 (setq patch (downcase (concat patch
850 (match-string 0 description))))
64c097a0
DK
851 (setq description (substring description (match-end 0))))
852 ((string-match "\\` +" description)
853 (setq patch (concat patch "-"))
854 (setq description (substring description (match-end 0))))
855 ((string-match "\\`[^a-zA-Z_-]+" description)
856 (setq description (substring description (match-end 0))))))
857 (cond ((= (length patch) 0)
858 "patch")
859 ((> (length patch) 20)
860 (substring patch 0 20))
861 (t patch))))
0bca35c8 862
9008e45b 863(defun stgit-delete (patchsyms &optional spill-p)
d51722b7 864 "Delete the patches in PATCHSYMS.
9008e45b
GH
865Interactively, delete the marked patches, or the patch at point.
866
867With a prefix argument, or SPILL-P, spill the patch contents to
868the work tree and index."
869 (interactive (list (stgit-patches-marked-or-at-point)
870 current-prefix-arg))
e7231e4f
GH
871 (unless patchsyms
872 (error "No patches to delete"))
d51722b7 873 (let ((npatches (length patchsyms)))
9008e45b 874 (when (yes-or-no-p (format "Really delete %d patch%s%s? "
e7231e4f 875 npatches
9008e45b
GH
876 (if (= 1 npatches) "" "es")
877 (if spill-p
878 " (spilling contents to index)"
879 "")))
880 (let ((args (if spill-p
881 (cons "--spill" patchsyms)
882 patchsyms)))
883 (stgit-capture-output nil
884 (apply 'stgit-run "delete" args))
885 (stgit-reload)))))
d51722b7 886
7cc45294
GH
887(defun stgit-move-patches-target ()
888 "Return the patchsym indicating a target patch for
889`stgit-move-patches'.
890
891This is either the patch at point, or one of :top and :bottom, if
892the point is after or before the applied patches."
893
2c862b07 894 (let ((patchsym (stgit-patch-name-at-point)))
7cc45294
GH
895 (cond (patchsym patchsym)
896 ((save-excursion (re-search-backward "^>" nil t)) :top)
897 (t :bottom))))
898
95369f6c
GH
899(defun stgit-sort-patches (patchsyms)
900 "Returns the list of patches in PATCHSYMS sorted according to
901their position in the patch series, bottommost first.
902
903PATCHSYMS may not contain duplicate entries."
904 (let (sorted-patchsyms
905 (series (with-output-to-string
906 (with-current-buffer standard-output
907 (stgit-run-silent "series" "--noprefix"))))
908 start)
909 (while (string-match "^\\(.+\\)" series start)
910 (let ((patchsym (intern (match-string 1 series))))
911 (when (memq patchsym patchsyms)
912 (setq sorted-patchsyms (cons patchsym sorted-patchsyms))))
913 (setq start (match-end 0)))
914 (setq sorted-patchsyms (nreverse sorted-patchsyms))
915
916 (unless (= (length patchsyms) (length sorted-patchsyms))
917 (error "Internal error"))
918
919 sorted-patchsyms))
920
7cc45294
GH
921(defun stgit-move-patches (patchsyms target-patch)
922 "Move the patches in PATCHSYMS to below TARGET-PATCH.
923If TARGET-PATCH is :bottom or :top, move the patches to the
924bottom or top of the stack, respectively.
925
926Interactively, move the marked patches to where the point is."
927 (interactive (list stgit-marked-patches
928 (stgit-move-patches-target)))
929 (unless patchsyms
930 (error "Need at least one patch to move"))
931
932 (unless target-patch
933 (error "Point not at a patch"))
934
935 (if (eq target-patch :top)
936 (stgit-capture-output nil
937 (apply 'stgit-run "float" patchsyms))
938
939 ;; need to have patchsyms sorted by position in the stack
95369f6c 940 (let ((sorted-patchsyms (stgit-sort-patches patchsyms)))
7cc45294
GH
941 (while sorted-patchsyms
942 (setq sorted-patchsyms
943 (and (stgit-capture-output nil
944 (if (eq target-patch :bottom)
945 (stgit-run "sink" "--" (car sorted-patchsyms))
946 (stgit-run "sink" "--to" target-patch "--"
947 (car sorted-patchsyms))))
948 (cdr sorted-patchsyms))))))
949 (stgit-reload))
950
594aa463
KH
951(defun stgit-squash (patchsyms)
952 "Squash the patches in PATCHSYMS.
693d179b
GH
953Interactively, squash the marked patches.
954
955Unless there are any conflicts, the patches will be merged into
956one patch, which will occupy the same spot in the series as the
957deepest patch had before the squash."
d51722b7
GH
958 (interactive (list stgit-marked-patches))
959 (when (< (length patchsyms) 2)
594aa463 960 (error "Need at least two patches to squash"))
32d7545d
GH
961 (let ((stgit-buffer (current-buffer))
962 (edit-buf (get-buffer-create "*StGit edit*"))
693d179b
GH
963 (dir default-directory)
964 (sorted-patchsyms (stgit-sort-patches patchsyms)))
594aa463 965 (log-edit 'stgit-confirm-squash t nil edit-buf)
693d179b 966 (set (make-local-variable 'stgit-patchsyms) sorted-patchsyms)
ea0def18 967 (setq default-directory dir)
32d7545d
GH
968 (let ((result (let ((standard-output edit-buf))
969 (apply 'stgit-run-silent "squash"
970 "--save-template=-" sorted-patchsyms))))
971
972 ;; stg squash may have reordered the patches or caused conflicts
973 (with-current-buffer stgit-buffer
974 (stgit-reload))
975
976 (unless (eq 0 result)
977 (fundamental-mode)
978 (rename-buffer "*StGit error*")
979 (resize-temp-buffer-window)
980 (switch-to-buffer-other-window stgit-buffer)
981 (error "stg squash failed")))))
ea0def18 982
594aa463 983(defun stgit-confirm-squash ()
ea0def18
DK
984 (interactive)
985 (let ((file (make-temp-file "stgit-edit-")))
986 (write-region (point-min) (point-max) file)
987 (stgit-capture-output nil
594aa463 988 (apply 'stgit-run "squash" "-f" file stgit-patchsyms))
ea0def18 989 (with-current-buffer log-edit-parent-buffer
e6b1fdae
DK
990 (stgit-clear-marks)
991 ;; Go to first marked patch and stay there
992 (goto-char (point-min))
993 (re-search-forward (concat "^[>+-]\\*") nil t)
994 (move-to-column goal-column)
995 (let ((pos (point)))
1f0bf00f 996 (stgit-reload)
e6b1fdae 997 (goto-char pos)))))
ea0def18 998
0663524d
KH
999(defun stgit-help ()
1000 "Display help for the StGit mode."
1001 (interactive)
1002 (describe-function 'stgit-mode))
3a59f3db 1003
83e51dbf
DK
1004(defun stgit-undo (&optional arg)
1005 "Run stg undo.
1006With prefix argument, run it with the --hard flag."
1007 (interactive "P")
1008 (stgit-capture-output nil
1009 (if arg
1010 (stgit-run "undo" "--hard")
1011 (stgit-run "undo")))
1f0bf00f 1012 (stgit-reload))
83e51dbf 1013
4d73c4d8
DK
1014(defun stgit-refresh (&optional arg)
1015 "Run stg refresh.
a53347d9 1016With prefix argument, refresh the marked patch or the patch under point."
4d73c4d8
DK
1017 (interactive "P")
1018 (let ((patchargs (if arg
b0424080
GH
1019 (let ((patches (stgit-patches-marked-or-at-point)))
1020 (cond ((null patches)
df283a8b 1021 (error "No patch to update"))
b0424080 1022 ((> (length patches) 1)
df283a8b 1023 (error "Too many patches selected"))
b0424080
GH
1024 (t
1025 (cons "-p" patches))))
1026 nil)))
4d73c4d8 1027 (stgit-capture-output nil
074a4fb0
GH
1028 (apply 'stgit-run "refresh" patchargs))
1029 (stgit-refresh-git-status))
4d73c4d8
DK
1030 (stgit-reload))
1031
3a59f3db 1032(provide 'stgit)