chiark / gitweb /
Add --stat option to show
[stgit] / contrib / stgit.el
CommitLineData
3a59f3db
KH
1;; stgit.el: An emacs mode for StGit
2;;
3;; Copyright (C) 2007 David Kågedal <davidk@lysator.liu.se>
4;;
5;; To install: put this file on the load-path and place the following
6;; in your .emacs file:
7;;
8;; (require 'stgit)
9;;
10;; To start: `M-x stgit'
11
0f076fe6
GH
12(require 'git nil t)
13
56d81fe5 14(defun stgit (dir)
a53347d9 15 "Manage StGit patches for the tree in DIR."
56d81fe5 16 (interactive "DDirectory: \n")
52144ce5 17 (switch-to-stgit-buffer (git-get-top-dir dir))
1f0bf00f 18 (stgit-reload))
56d81fe5 19
074a4fb0
GH
20(unless (fboundp 'git-get-top-dir)
21 (defun git-get-top-dir (dir)
22 "Retrieve the top-level directory of a git tree."
23 (let ((cdup (with-output-to-string
24 (with-current-buffer standard-output
25 (cd dir)
26 (unless (eq 0 (call-process "git" nil t nil
27 "rev-parse" "--show-cdup"))
df283a8b 28 (error "Cannot find top-level git tree for %s" dir))))))
074a4fb0
GH
29 (expand-file-name (concat (file-name-as-directory dir)
30 (car (split-string cdup "\n")))))))
31
32(defun stgit-refresh-git-status (&optional dir)
33 "If it exists, refresh the `git-status' buffer belonging to
34directory DIR or `default-directory'"
35 (when (and (fboundp 'git-find-status-buffer)
36 (fboundp 'git-refresh-status))
37 (let* ((top-dir (git-get-top-dir (or dir default-directory)))
38 (git-status-buffer (and top-dir (git-find-status-buffer top-dir))))
39 (when git-status-buffer
40 (with-current-buffer git-status-buffer
41 (git-refresh-status))))))
52144ce5 42
56d81fe5 43(defun switch-to-stgit-buffer (dir)
a53347d9 44 "Switch to a (possibly new) buffer displaying StGit patches for DIR."
56d81fe5
DK
45 (setq dir (file-name-as-directory dir))
46 (let ((buffers (buffer-list)))
47 (while (and buffers
48 (not (with-current-buffer (car buffers)
49 (and (eq major-mode 'stgit-mode)
50 (string= default-directory dir)))))
51 (setq buffers (cdr buffers)))
52 (switch-to-buffer (if buffers
53 (car buffers)
54 (create-stgit-buffer dir)))))
55
56(defun create-stgit-buffer (dir)
57 "Create a buffer for showing StGit patches.
58Argument DIR is the repository path."
59 (let ((buf (create-file-buffer (concat dir "*stgit*")))
60 (inhibit-read-only t))
61 (with-current-buffer buf
62 (setq default-directory dir)
63 (stgit-mode)
64 (setq buffer-read-only t))
65 buf))
66
67(defmacro stgit-capture-output (name &rest body)
e558a4ab
GH
68 "Capture StGit output and, if there was any output, show it in a window
69at the end.
70Returns nil if there was no output."
34afb86c
DK
71 `(let ((output-buf (get-buffer-create ,(or name "*StGit output*")))
72 (stgit-dir default-directory)
73 (inhibit-read-only t))
56d81fe5 74 (with-current-buffer output-buf
34afb86c
DK
75 (erase-buffer)
76 (setq default-directory stgit-dir)
77 (setq buffer-read-only t))
56d81fe5
DK
78 (let ((standard-output output-buf))
79 ,@body)
34afb86c
DK
80 (with-current-buffer output-buf
81 (set-buffer-modified-p nil)
82 (setq buffer-read-only t)
83 (if (< (point-min) (point-max))
84 (display-buffer output-buf t)))))
56d81fe5
DK
85(put 'stgit-capture-output 'lisp-indent-function 1)
86
d51722b7
GH
87(defun stgit-make-run-args (args)
88 "Return a copy of ARGS with its elements converted to strings."
89 (mapcar (lambda (x)
90 ;; don't use (format "%s" ...) to limit type errors
91 (cond ((stringp x) x)
92 ((integerp x) (number-to-string x))
93 ((symbolp x) (symbol-name x))
94 (t
95 (error "Bad element in stgit-make-run-args args: %S" x))))
96 args))
97
9aecd505 98(defun stgit-run-silent (&rest args)
d51722b7 99 (setq args (stgit-make-run-args args))
56d81fe5
DK
100 (apply 'call-process "stg" nil standard-output nil args))
101
9aecd505 102(defun stgit-run (&rest args)
d51722b7 103 (setq args (stgit-make-run-args args))
9aecd505
DK
104 (let ((msgcmd (mapconcat #'identity args " ")))
105 (message "Running stg %s..." msgcmd)
106 (apply 'call-process "stg" nil standard-output nil args)
107 (message "Running stg %s...done" msgcmd)))
108
378a003d 109(defun stgit-run-git (&rest args)
d51722b7 110 (setq args (stgit-make-run-args args))
378a003d
GH
111 (let ((msgcmd (mapconcat #'identity args " ")))
112 (message "Running git %s..." msgcmd)
113 (apply 'call-process "git" nil standard-output nil args)
114 (message "Running git %s...done" msgcmd)))
115
1f60181a 116(defun stgit-run-git-silent (&rest args)
d51722b7 117 (setq args (stgit-make-run-args args))
1f60181a
GH
118 (apply 'call-process "git" nil standard-output nil args))
119
1f0bf00f 120(defun stgit-reload ()
a53347d9 121 "Update the contents of the StGit buffer."
56d81fe5
DK
122 (interactive)
123 (let ((inhibit-read-only t)
124 (curline (line-number-at-pos))
125 (curpatch (stgit-patch-at-point)))
126 (erase-buffer)
127 (insert "Branch: ")
9aecd505 128 (stgit-run-silent "branch")
bce79a6a 129 (stgit-run-silent "series" "--description" "--empty")
6df83d42 130 (stgit-rescan)
56d81fe5
DK
131 (if curpatch
132 (stgit-goto-patch curpatch)
074a4fb0
GH
133 (goto-line curline)))
134 (stgit-refresh-git-status))
56d81fe5 135
8f40753a
GH
136(defgroup stgit nil
137 "A user interface for the StGit patch maintenance tool."
138 :group 'tools)
139
07f464e0
DK
140(defface stgit-description-face
141 '((((background dark)) (:foreground "tan"))
142 (((background light)) (:foreground "dark red")))
8f40753a
GH
143 "The face used for StGit descriptions"
144 :group 'stgit)
07f464e0
DK
145
146(defface stgit-top-patch-face
147 '((((background dark)) (:weight bold :foreground "yellow"))
148 (((background light)) (:weight bold :foreground "purple"))
149 (t (:weight bold)))
8f40753a
GH
150 "The face used for the top patch names"
151 :group 'stgit)
07f464e0
DK
152
153(defface stgit-applied-patch-face
154 '((((background dark)) (:foreground "light yellow"))
155 (((background light)) (:foreground "purple"))
156 (t ()))
8f40753a
GH
157 "The face used for applied patch names"
158 :group 'stgit)
07f464e0
DK
159
160(defface stgit-unapplied-patch-face
161 '((((background dark)) (:foreground "gray80"))
162 (((background light)) (:foreground "orchid"))
163 (t ()))
8f40753a
GH
164 "The face used for unapplied patch names"
165 :group 'stgit)
07f464e0 166
1f60181a
GH
167(defface stgit-modified-file-face
168 '((((class color) (background light)) (:foreground "purple"))
169 (((class color) (background dark)) (:foreground "salmon")))
170 "StGit mode face used for modified file status"
171 :group 'stgit)
172
173(defface stgit-unmerged-file-face
174 '((((class color) (background light)) (:foreground "red" :bold t))
175 (((class color) (background dark)) (:foreground "red" :bold t)))
176 "StGit mode face used for unmerged file status"
177 :group 'stgit)
178
179(defface stgit-unknown-file-face
180 '((((class color) (background light)) (:foreground "goldenrod" :bold t))
181 (((class color) (background dark)) (:foreground "goldenrod" :bold t)))
182 "StGit mode face used for unknown file status"
183 :group 'stgit)
184
a6d9a852
GH
185(defface stgit-file-permission-face
186 '((((class color) (background light)) (:foreground "green" :bold t))
187 (((class color) (background dark)) (:foreground "green" :bold t)))
188 "StGit mode face used for permission changes."
189 :group 'stgit)
190
1f60181a
GH
191(defcustom stgit-expand-find-copies-harder
192 nil
193 "Try harder to find copied files when listing patches.
194
195When not nil, runs git diff-tree with the --find-copies-harder
196flag, which reduces performance."
197 :type 'boolean
198 :group 'stgit)
199
200(defconst stgit-file-status-code-strings
201 (mapcar (lambda (arg)
202 (cons (car arg)
a6d9a852
GH
203 (propertize (cadr arg) 'face (car (cddr arg)))))
204 '((add "Added" stgit-modified-file-face)
205 (copy "Copied" stgit-modified-file-face)
206 (delete "Deleted" stgit-modified-file-face)
207 (modify "Modified" stgit-modified-file-face)
208 (rename "Renamed" stgit-modified-file-face)
209 (mode-change "Mode change" stgit-modified-file-face)
210 (unmerged "Unmerged" stgit-unmerged-file-face)
211 (unknown "Unknown" stgit-unknown-file-face)))
1f60181a
GH
212 "Alist of code symbols to description strings")
213
214(defun stgit-file-status-code-as-string (code)
215 "Return stgit status code as string"
a6d9a852
GH
216 (let ((str (assq (if (consp code) (car code) code)
217 stgit-file-status-code-strings)))
218 (when str
219 (format "%-11s "
220 (if (and str (consp code) (/= (cdr code) 100))
221 (format "%s %s" (cdr str)
222 (propertize (format "%d%%" (cdr code))
223 'face 'stgit-description-face))
224 (cdr str))))))
1f60181a 225
a6d9a852 226(defun stgit-file-status-code (str &optional score)
1f60181a
GH
227 "Return stgit status code from git status string"
228 (let ((code (assoc str '(("A" . add)
229 ("C" . copy)
230 ("D" . delete)
231 ("M" . modify)
232 ("R" . rename)
233 ("T" . mode-change)
234 ("U" . unmerged)
235 ("X" . unknown)))))
a6d9a852
GH
236 (setq code (if code (cdr code) 'unknown))
237 (when (stringp score)
238 (if (> (length score) 0)
239 (setq score (string-to-number score))
240 (setq score nil)))
241 (if score (cons code score) code)))
242
243(defconst stgit-file-type-strings
244 '((#o100 . "file")
245 (#o120 . "symlink")
246 (#o160 . "subproject"))
247 "Alist of names of file types")
248
249(defun stgit-file-type-string (type)
47271f41
GH
250 "Return string describing file type TYPE (the high bits of file permission).
251Cf. `stgit-file-type-strings' and `stgit-file-type-change-string'."
a6d9a852
GH
252 (let ((type-str (assoc type stgit-file-type-strings)))
253 (or (and type-str (cdr type-str))
254 (format "unknown type %o" type))))
255
256(defun stgit-file-type-change-string (old-perm new-perm)
47271f41
GH
257 "Return string describing file type change from OLD-PERM to NEW-PERM.
258Cf. `stgit-file-type-string'."
a6d9a852
GH
259 (let ((old-type (lsh old-perm -9))
260 (new-type (lsh new-perm -9)))
261 (cond ((= old-type new-type) "")
262 ((zerop new-type) "")
263 ((zerop old-type)
264 (if (= new-type #o100)
265 ""
266 (format " (%s)" (stgit-file-type-string new-type))))
267 (t (format " (%s -> %s)"
268 (stgit-file-type-string old-type)
269 (stgit-file-type-string new-type))))))
270
271(defun stgit-file-mode-change-string (old-perm new-perm)
47271f41
GH
272 "Return string describing file mode change from OLD-PERM to NEW-PERM.
273Cf. `stgit-file-type-change-string'."
a6d9a852
GH
274 (setq old-perm (logand old-perm #o777)
275 new-perm (logand new-perm #o777))
276 (if (or (= old-perm new-perm)
277 (zerop old-perm)
278 (zerop new-perm))
279 ""
280 (let* ((modified (logxor old-perm new-perm))
281 (not-x-modified (logand (logxor old-perm new-perm) #o666)))
282 (cond ((zerop modified) "")
283 ((and (zerop not-x-modified)
284 (or (and (eq #o111 (logand old-perm #o111))
285 (propertize "-x" 'face 'stgit-file-permission-face))
286 (and (eq #o111 (logand new-perm #o111))
287 (propertize "+x" 'face
288 'stgit-file-permission-face)))))
289 (t (concat (propertize (format "%o" old-perm)
290 'face 'stgit-file-permission-face)
291 (propertize " -> "
292 'face 'stgit-description-face)
293 (propertize (format "%o" new-perm)
294 'face 'stgit-file-permission-face)))))))
1f60181a 295
378a003d 296(defun stgit-expand-patch (patchsym)
47271f41
GH
297 "Expand (show modification of) the patch with name PATCHSYM (a
298symbol) at point.
299`stgit-expand-find-copies-harder' controls how hard to try to
300find copied files."
378a003d
GH
301 (save-excursion
302 (forward-line)
1f60181a
GH
303 (let* ((start (point))
304 (result (with-output-to-string
305 (stgit-run-git "diff-tree" "-r" "-z"
306 (if stgit-expand-find-copies-harder
307 "--find-copies-harder"
308 "-C")
d51722b7 309 (stgit-id patchsym)))))
1f60181a 310 (let (mstart)
a6d9a852 311 (while (string-match "\0:\\([0-7]+\\) \\([0-7]+\\) [0-9A-Fa-f]\\{40\\} [0-9A-Fa-f]\\{40\\} \\(\\([CR]\\)\\([0-9]*\\)\0\\([^\0]*\\)\0\\([^\0]*\\)\\|\\([ABD-QS-Z]\\)\0\\([^\0]*\\)\\)"
1f60181a 312 result mstart)
a6d9a852
GH
313 (let ((copy-or-rename (match-string 4 result))
314 (old-perm (read (format "#o%s" (match-string 1 result))))
315 (new-perm (read (format "#o%s" (match-string 2 result))))
1f60181a 316 (line-start (point))
a6d9a852
GH
317 status
318 change
1f60181a
GH
319 properties)
320 (insert " ")
321 (if copy-or-rename
a6d9a852
GH
322 (let ((cr-score (match-string 5 result))
323 (cr-from-file (match-string 6 result))
324 (cr-to-file (match-string 7 result)))
325 (setq status (stgit-file-status-code copy-or-rename
326 cr-score)
327 properties (list 'stgit-old-file cr-from-file
328 'stgit-new-file cr-to-file)
329 change (concat
330 cr-from-file
331 (propertize " -> "
332 'face 'stgit-description-face)
333 cr-to-file)))
334 (setq status (stgit-file-status-code (match-string 8 result))
335 properties (list 'stgit-file (match-string 9 result))
336 change (match-string 9 result)))
337
338 (let ((mode-change (stgit-file-mode-change-string old-perm
339 new-perm)))
340 (insert (format "%-12s" (stgit-file-status-code-as-string
341 status))
342 mode-change
343 (if (> (length mode-change) 0) " " "")
344 change
345 (propertize (stgit-file-type-change-string old-perm
346 new-perm)
347 'face 'stgit-description-face)
348 ?\n))
1f60181a
GH
349 (add-text-properties line-start (point) properties))
350 (setq mstart (match-end 0))))
351 (when (= start (point))
352 (insert " <no files>\n"))
d51722b7 353 (put-text-property start (point) 'stgit-file-patchsym patchsym))))
378a003d 354
acc5652f
DK
355(defun stgit-collapse-patch (patchsym)
356 "Collapse the patch with name PATCHSYM after the line at point."
357 (save-excursion
358 (forward-line)
359 (let ((start (point)))
360 (while (eq (get-text-property (point) 'stgit-file-patchsym) patchsym)
361 (forward-line))
362 (delete-region start (point)))))
363
6df83d42
DK
364(defun stgit-rescan ()
365 "Rescan the status buffer."
07f464e0 366 (save-excursion
3a1cf814
GH
367 (let ((marked ())
368 found-any)
6df83d42
DK
369 (goto-char (point-min))
370 (while (not (eobp))
371 (cond ((looking-at "Branch: \\(.*\\)")
372 (put-text-property (match-beginning 1) (match-end 1)
373 'face 'bold))
bce79a6a 374 ((looking-at "\\([0 ]\\)\\([>+-]\\)\\( \\)\\([^ ]+\\) *[|#] \\(.*\\)")
3a1cf814 375 (setq found-any t)
bce79a6a
GH
376 (let ((empty (match-string 1))
377 (state (match-string 2))
378 (patchsym (intern (match-string 4))))
6df83d42 379 (put-text-property
bce79a6a 380 (match-beginning 4) (match-end 4) 'face
6df83d42
DK
381 (cond ((string= state ">") 'stgit-top-patch-face)
382 ((string= state "+") 'stgit-applied-patch-face)
383 ((string= state "-") 'stgit-unapplied-patch-face)))
bce79a6a 384 (put-text-property (match-beginning 5) (match-end 5)
6df83d42
DK
385 'face 'stgit-description-face)
386 (when (memq patchsym stgit-marked-patches)
bce79a6a
GH
387 (save-excursion
388 (replace-match "*" nil nil nil 3))
378a003d 389 (setq marked (cons patchsym marked)))
d51722b7
GH
390 (put-text-property (match-beginning 0) (match-end 0)
391 'stgit-patchsym patchsym)
378a003d
GH
392 (when (memq patchsym stgit-expanded-patches)
393 (stgit-expand-patch patchsym))
bce79a6a
GH
394 (when (equal "0" empty)
395 (save-excursion
396 (goto-char (match-beginning 5))
397 (insert "(empty) ")))
398 (delete-char 1)
378a003d 399 ))
ad80ce22
DK
400 ((or (looking-at "stg series: Branch \".*\" not initialised")
401 (looking-at "stg series: .*: branch not initialized"))
3a1cf814 402 (setq found-any t)
1c2426dc
DK
403 (forward-line 1)
404 (insert "Run M-x stgit-init to initialise")))
6df83d42 405 (forward-line 1))
3a1cf814
GH
406 (setq stgit-marked-patches (nreverse marked))
407 (unless found-any
408 (insert "\n "
409 (propertize "no patches in series"
410 'face 'stgit-description-face))))))
07f464e0 411
acc5652f
DK
412(defun stgit-select-file ()
413 (let ((patched-file (stgit-patched-file-at-point)))
414 (unless patched-file
415 (error "No patch or file on the current line"))
416 (let ((filename (expand-file-name (cdr patched-file))))
417 (unless (file-exists-p filename)
418 (error "File does not exist"))
419 (find-file filename))))
420
421(defun stgit-toggle-patch-file-list (curpath)
422 (let ((inhibit-read-only t))
423 (if (memq curpatch stgit-expanded-patches)
424 (save-excursion
425 (setq stgit-expanded-patches (delq curpatch stgit-expanded-patches))
426 (stgit-collapse-patch curpatch))
427 (progn
428 (setq stgit-expanded-patches (cons curpatch stgit-expanded-patches))
429 (stgit-expand-patch curpatch)))))
430
378a003d
GH
431(defun stgit-select ()
432 "Expand or collapse the current entry"
433 (interactive)
434 (let ((curpatch (stgit-patch-at-point)))
acc5652f
DK
435 (if curpatch
436 (stgit-toggle-patch-file-list curpatch)
437 (stgit-select-file))))
438
378a003d
GH
439
440(defun stgit-find-file-other-window ()
441 "Open file at point in other window"
442 (interactive)
443 (let ((patched-file (stgit-patched-file-at-point)))
444 (unless patched-file
445 (error "No file on the current line"))
446 (let ((filename (expand-file-name (cdr patched-file))))
447 (unless (file-exists-p filename)
448 (error "File does not exist"))
449 (find-file-other-window filename))))
450
83327d53 451(defun stgit-quit ()
a53347d9 452 "Hide the stgit buffer."
83327d53
GH
453 (interactive)
454 (bury-buffer))
455
0f076fe6 456(defun stgit-git-status ()
a53347d9 457 "Show status using `git-status'."
0f076fe6
GH
458 (interactive)
459 (unless (fboundp 'git-status)
df283a8b 460 (error "The stgit-git-status command requires git-status"))
0f076fe6
GH
461 (let ((dir default-directory))
462 (save-selected-window
463 (pop-to-buffer nil)
464 (git-status dir))))
465
58f72f16
GH
466(defun stgit-goal-column ()
467 "Return goal column for the current line"
468 (cond ((get-text-property (point) 'stgit-file-patchsym) 4)
469 ((get-text-property (point) 'stgit-patchsym) 2)
470 (t 0)))
471
472(defun stgit-next-line (&optional arg)
378a003d 473 "Move cursor vertically down ARG lines"
58f72f16
GH
474 (interactive "p")
475 (next-line arg)
476 (move-to-column (stgit-goal-column)))
378a003d 477
58f72f16 478(defun stgit-previous-line (&optional arg)
378a003d 479 "Move cursor vertically up ARG lines"
58f72f16
GH
480 (interactive "p")
481 (previous-line arg)
482 (move-to-column (stgit-goal-column)))
378a003d
GH
483
484(defun stgit-next-patch (&optional arg)
485 "Move cursor down ARG patches"
486 (interactive "p")
487 (unless arg
488 (setq arg 1))
489 (if (< arg 0)
490 (stgit-previous-patch (- arg))
491 (while (not (zerop arg))
492 (setq arg (1- arg))
493 (while (progn (stgit-next-line)
494 (not (stgit-patch-at-point)))))))
495
496(defun stgit-previous-patch (&optional arg)
497 "Move cursor up ARG patches"
498 (interactive "p")
499 (unless arg
500 (setq arg 1))
501 (if (< arg 0)
502 (stgit-next-patch (- arg))
503 (while (not (zerop arg))
504 (setq arg (1- arg))
505 (while (progn (stgit-previous-line)
506 (not (stgit-patch-at-point)))))))
507
56d81fe5
DK
508(defvar stgit-mode-hook nil
509 "Run after `stgit-mode' is setup.")
510
511(defvar stgit-mode-map nil
512 "Keymap for StGit major mode.")
513
514(unless stgit-mode-map
515 (setq stgit-mode-map (make-keymap))
516 (suppress-keymap stgit-mode-map)
022a3664
GH
517 (mapc (lambda (arg) (define-key stgit-mode-map (car arg) (cdr arg)))
518 '((" " . stgit-mark)
3dccdc9b 519 ("m" . stgit-mark)
9b151b27
GH
520 ("\d" . stgit-unmark-up)
521 ("u" . stgit-unmark-down)
022a3664
GH
522 ("?" . stgit-help)
523 ("h" . stgit-help)
58f72f16
GH
524 ("\C-p" . stgit-previous-line)
525 ("\C-n" . stgit-next-line)
526 ([up] . stgit-previous-line)
527 ([down] . stgit-next-line)
528 ("p" . stgit-previous-patch)
529 ("n" . stgit-next-patch)
378a003d
GH
530 ("\M-{" . stgit-previous-patch)
531 ("\M-}" . stgit-next-patch)
0f076fe6 532 ("s" . stgit-git-status)
022a3664
GH
533 ("g" . stgit-reload)
534 ("r" . stgit-refresh)
535 ("\C-c\C-r" . stgit-rename)
536 ("e" . stgit-edit)
7cc45294 537 ("M" . stgit-move-patches)
594aa463 538 ("S" . stgit-squash)
022a3664
GH
539 ("N" . stgit-new)
540 ("R" . stgit-repair)
541 ("C" . stgit-commit)
542 ("U" . stgit-uncommit)
378a003d
GH
543 ("\r" . stgit-select)
544 ("o" . stgit-find-file-other-window)
022a3664
GH
545 (">" . stgit-push-next)
546 ("<" . stgit-pop-next)
547 ("P" . stgit-push-or-pop)
548 ("G" . stgit-goto)
549 ("=" . stgit-show)
550 ("D" . stgit-delete)
551 ([(control ?/)] . stgit-undo)
83327d53 552 ("\C-_" . stgit-undo)
adeef6bc
GH
553 ("B" . stgit-branch)
554 ("q" . stgit-quit))))
56d81fe5
DK
555
556(defun stgit-mode ()
557 "Major mode for interacting with StGit.
558Commands:
559\\{stgit-mode-map}"
560 (kill-all-local-variables)
561 (buffer-disable-undo)
562 (setq mode-name "StGit"
563 major-mode 'stgit-mode
564 goal-column 2)
565 (use-local-map stgit-mode-map)
566 (set (make-local-variable 'list-buffers-directory) default-directory)
6df83d42 567 (set (make-local-variable 'stgit-marked-patches) nil)
378a003d 568 (set (make-local-variable 'stgit-expanded-patches) nil)
2870f8b8 569 (set-variable 'truncate-lines 't)
56d81fe5
DK
570 (run-hooks 'stgit-mode-hook))
571
d51722b7
GH
572(defun stgit-add-mark (patchsym)
573 "Mark the patch PATCHSYM."
980ccd21
GH
574 (setq stgit-marked-patches (cons patchsym stgit-marked-patches))
575 (save-excursion
576 (when (stgit-goto-patch patchsym)
577 (move-to-column 1)
578 (let ((inhibit-read-only t))
579 (insert-and-inherit ?*)
580 (delete-char 1)))))
6df83d42 581
d51722b7
GH
582(defun stgit-remove-mark (patchsym)
583 "Unmark the patch PATCHSYM."
980ccd21
GH
584 (setq stgit-marked-patches (delq patchsym stgit-marked-patches))
585 (save-excursion
586 (when (stgit-goto-patch patchsym)
587 (move-to-column 1)
588 (let ((inhibit-read-only t))
589 (insert-and-inherit ? )
590 (delete-char 1)))))
6df83d42 591
e6b1fdae 592(defun stgit-clear-marks ()
47271f41 593 "Unmark all patches."
e6b1fdae
DK
594 (setq stgit-marked-patches '()))
595
378a003d 596(defun stgit-patch-at-point (&optional cause-error allow-file)
d51722b7 597 "Return the patch name on the current line as a symbol.
378a003d
GH
598If CAUSE-ERROR is not nil, signal an error if none found.
599If ALLOW-FILE is not nil, also handle when point is on a file of
600a patch."
d51722b7
GH
601 (or (get-text-property (point) 'stgit-patchsym)
602 (and allow-file
603 (get-text-property (point) 'stgit-file-patchsym))
604 (when cause-error
605 (error "No patch on this line"))))
378a003d 606
1f60181a
GH
607(defun stgit-patched-file-at-point (&optional both-files)
608 "Returns a cons of the patchsym and file name at point. For
609copies and renames, return the new file if the patch is either
610applied. If BOTH-FILES is non-nil, return a cons of the old and
611the new file names instead of just one name."
d51722b7 612 (let ((patchsym (get-text-property (point) 'stgit-file-patchsym))
1f60181a
GH
613 (file (get-text-property (point) 'stgit-file)))
614 (cond ((not patchsym) nil)
615 (file (cons patchsym file))
616 (both-files
617 (cons patchsym (cons (get-text-property (point) 'stgit-old-file)
618 (get-text-property (point) 'stgit-new-file))))
619 (t
620 (let ((file-sym (save-excursion
621 (stgit-previous-patch)
d51722b7
GH
622 (unless (eq (stgit-patch-at-point)
623 patchsym)
1f60181a
GH
624 (error "Cannot find the %s patch" patchsym))
625 (beginning-of-line)
626 (if (= (char-after) ?-)
627 'stgit-old-file
628 'stgit-new-file))))
629 (cons patchsym (get-text-property (point) file-sym)))))))
56d81fe5 630
7755d7f1 631(defun stgit-patches-marked-or-at-point ()
d51722b7 632 "Return the symbols of the marked patches, or the patch on the current line."
7755d7f1 633 (if stgit-marked-patches
d51722b7 634 stgit-marked-patches
7755d7f1
KH
635 (let ((patch (stgit-patch-at-point)))
636 (if patch
637 (list patch)
638 '()))))
639
d51722b7
GH
640(defun stgit-goto-patch (patchsym)
641 "Move point to the line containing patch PATCHSYM.
642If that patch cannot be found, return nil."
643 (let ((p (text-property-any (point-min) (point-max)
644 'stgit-patchsym patchsym)))
645 (when p
56d81fe5 646 (goto-char p)
d51722b7 647 (move-to-column goal-column))))
56d81fe5 648
1c2426dc 649(defun stgit-init ()
a53347d9 650 "Run stg init."
1c2426dc
DK
651 (interactive)
652 (stgit-capture-output nil
b0424080 653 (stgit-run "init"))
1f0bf00f 654 (stgit-reload))
1c2426dc 655
6df83d42 656(defun stgit-mark ()
a53347d9 657 "Mark the patch under point."
6df83d42 658 (interactive)
018fa1ac 659 (let ((patch (stgit-patch-at-point t)))
980ccd21 660 (stgit-add-mark patch))
378a003d 661 (stgit-next-patch))
6df83d42 662
9b151b27 663(defun stgit-unmark-up ()
a53347d9 664 "Remove mark from the patch on the previous line."
6df83d42 665 (interactive)
378a003d 666 (stgit-previous-patch)
980ccd21 667 (stgit-remove-mark (stgit-patch-at-point t)))
9b151b27
GH
668
669(defun stgit-unmark-down ()
a53347d9 670 "Remove mark from the patch on the current line."
9b151b27 671 (interactive)
018fa1ac 672 (stgit-remove-mark (stgit-patch-at-point t))
1288eda2 673 (stgit-next-patch))
6df83d42 674
56d81fe5 675(defun stgit-rename (name)
018fa1ac 676 "Rename the patch under point to NAME."
d51722b7
GH
677 (interactive (list (read-string "Patch name: "
678 (symbol-name (stgit-patch-at-point t)))))
679 (let ((old-patchsym (stgit-patch-at-point t)))
56d81fe5 680 (stgit-capture-output nil
d51722b7
GH
681 (stgit-run "rename" old-patchsym name))
682 (let ((name-sym (intern name)))
683 (when (memq old-patchsym stgit-expanded-patches)
378a003d 684 (setq stgit-expanded-patches
d51722b7
GH
685 (cons name-sym (delq old-patchsym stgit-expanded-patches))))
686 (when (memq old-patchsym stgit-marked-patches)
378a003d 687 (setq stgit-marked-patches
d51722b7
GH
688 (cons name-sym (delq old-patchsym stgit-marked-patches))))
689 (stgit-reload)
690 (stgit-goto-patch name-sym))))
56d81fe5 691
26201d96 692(defun stgit-repair ()
a53347d9 693 "Run stg repair."
26201d96
DK
694 (interactive)
695 (stgit-capture-output nil
b0424080 696 (stgit-run "repair"))
1f0bf00f 697 (stgit-reload))
26201d96 698
adeef6bc
GH
699(defun stgit-available-branches ()
700 "Returns a list of the available stg branches"
701 (let ((output (with-output-to-string
702 (stgit-run "branch" "--list")))
703 (start 0)
704 result)
705 (while (string-match "^>?\\s-+s\\s-+\\(\\S-+\\)" output start)
706 (setq result (cons (match-string 1 output) result))
707 (setq start (match-end 0)))
708 result))
709
710(defun stgit-branch (branch)
711 "Switch to branch BRANCH."
712 (interactive (list (completing-read "Switch to branch: "
713 (stgit-available-branches))))
714 (stgit-capture-output nil (stgit-run "branch" "--" branch))
715 (stgit-reload))
716
41c1c59c
GH
717(defun stgit-commit (count)
718 "Run stg commit on COUNT commits.
719Interactively, the prefix argument is used as COUNT."
720 (interactive "p")
721 (stgit-capture-output nil (stgit-run "commit" "-n" count))
1f0bf00f 722 (stgit-reload))
c4aad9a7 723
41c1c59c
GH
724(defun stgit-uncommit (count)
725 "Run stg uncommit on COUNT commits.
726Interactively, the prefix argument is used as COUNT."
c4aad9a7 727 (interactive "p")
41c1c59c 728 (stgit-capture-output nil (stgit-run "uncommit" "-n" count))
1f0bf00f 729 (stgit-reload))
c4aad9a7 730
0b661144
DK
731(defun stgit-push-next (npatches)
732 "Push the first unapplied patch.
733With numeric prefix argument, push that many patches."
734 (interactive "p")
d51722b7 735 (stgit-capture-output nil (stgit-run "push" "-n" npatches))
074a4fb0
GH
736 (stgit-reload)
737 (stgit-refresh-git-status))
56d81fe5 738
0b661144
DK
739(defun stgit-pop-next (npatches)
740 "Pop the topmost applied patch.
741With numeric prefix argument, pop that many patches."
742 (interactive "p")
d51722b7 743 (stgit-capture-output nil (stgit-run "pop" "-n" npatches))
074a4fb0
GH
744 (stgit-reload)
745 (stgit-refresh-git-status))
56d81fe5 746
f9182fca
KH
747(defun stgit-applied-at-point ()
748 "Is the patch on the current line applied?"
749 (save-excursion
750 (beginning-of-line)
751 (looking-at "[>+]")))
752
753(defun stgit-push-or-pop ()
a53347d9 754 "Push or pop the patch on the current line."
f9182fca 755 (interactive)
d51722b7 756 (let ((patchsym (stgit-patch-at-point t))
f9182fca
KH
757 (applied (stgit-applied-at-point)))
758 (stgit-capture-output nil
d51722b7 759 (stgit-run (if applied "pop" "push") patchsym))
1f0bf00f 760 (stgit-reload)))
f9182fca 761
c7adf5ef 762(defun stgit-goto ()
a53347d9 763 "Go to the patch on the current line."
c7adf5ef 764 (interactive)
d51722b7 765 (let ((patchsym (stgit-patch-at-point t)))
c7adf5ef 766 (stgit-capture-output nil
d51722b7 767 (stgit-run "goto" patchsym))
1f0bf00f 768 (stgit-reload)))
c7adf5ef 769
d51722b7
GH
770(defun stgit-id (patchsym)
771 "Return the git commit id for PATCHSYM."
378a003d 772 (let ((result (with-output-to-string
d51722b7 773 (stgit-run-silent "id" patchsym))))
378a003d 774 (unless (string-match "^\\([0-9A-Fa-f]\\{40\\}\\)$" result)
d51722b7 775 (error "Cannot find commit id for %s" patchsym))
378a003d
GH
776 (match-string 1 result)))
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*"
d51722b7
GH
782 (let ((patchsym (stgit-patch-at-point)))
783 (if (not patchsym)
1f60181a 784 (let ((patched-file (stgit-patched-file-at-point t)))
378a003d
GH
785 (unless patched-file
786 (error "No patch or file at point"))
d51722b7 787 (let ((id (stgit-id (car patched-file))))
5f1b0013
GH
788 (if (consp (cdr patched-file))
789 ;; two files (copy or rename)
790 (stgit-run-git "diff" "-C" "-C" (concat id "^") id "--"
791 (cadr patched-file) (cddr patched-file))
792 ;; just one file
793 (stgit-run-git "diff" (concat id "^") id "--"
794 (cdr patched-file)))))
68048d11 795 (stgit-run "show" "-O" "--patch-with-stat" "-O" "-M" patchsym))
5f1b0013
GH
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)
d51722b7 803 (let ((patchsym (stgit-patch-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
894 (let ((patchsym (stgit-patch-at-point)))
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)