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