stgit.el: Use forward-line instead of goto-line non-interactively
[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
4f7efe0c
GH
12(when (< emacs-major-version 22)
13 (error "Emacs older than 22 is not supported by stgit.el"))
14
0f076fe6 15(require 'git nil t)
50d88c67 16(require 'cl)
a4a01630 17(require 'comint)
98230edd 18(require 'ewoc)
5038381d 19(require 'easymenu)
0b9ea6b8 20(require 'format-spec)
0f076fe6 21
4ba91e80
DK
22(defun stgit-set-default (symbol value)
23 "Set default value of SYMBOL to VALUE using `set-default' and
24reload all StGit buffers."
25 (set-default symbol value)
26 (dolist (buf (buffer-list))
27 (with-current-buffer buf
28 (when (eq major-mode 'stgit-mode)
29 (stgit-reload)))))
30
31(defgroup stgit nil
32 "A user interface for the StGit patch maintenance tool."
33 :group 'tools
34 :link '(function-link stgit)
35 :link '(url-link "http://www.procode.org/stgit/"))
36
37(defcustom stgit-abbreviate-copies-and-renames t
38 "If non-nil, abbreviate copies and renames as \"dir/{old -> new}/file\"
39instead of \"dir/old/file -> dir/new/file\"."
40 :type 'boolean
41 :group 'stgit
42 :set 'stgit-set-default)
43
44(defcustom stgit-default-show-worktree t
45 "Set to non-nil to by default show the working tree in a new stgit buffer.
46
bc11fb08
GH
47Use \\<stgit-mode-map>\\[stgit-toggle-worktree] to toggle this \
48setting in an already-started StGit buffer."
4ba91e80
DK
49 :type 'boolean
50 :group 'stgit
51 :link '(variable-link stgit-show-worktree))
52
bc11fb08
GH
53(defcustom stgit-default-show-unknown nil
54 "Set to non-nil to by default show unknown files a new stgit buffer.
55
56Use \\<stgit-mode-map>\\[stgit-toggle-unknown] to toggle this \
57setting in an already-started StGit buffer."
58 :type 'boolean
59 :group 'stgit
60 :link '(variable-link stgit-show-unknown))
61
62(defcustom stgit-default-show-ignored nil
63 "Set to non-nil to by default show ignored files a new stgit buffer.
64
65Use \\<stgit-mode-map>\\[stgit-toggle-ignored] to toggle this \
66setting in an already-started StGit buffer."
67 :type 'boolean
68 :group 'stgit
69 :link '(variable-link stgit-show-ignored))
70
4ba91e80
DK
71(defcustom stgit-find-copies-harder nil
72 "Try harder to find copied files when listing patches.
73
74When not nil, runs git diff-tree with the --find-copies-harder
75flag, which reduces performance."
76 :type 'boolean
77 :group 'stgit
78 :set 'stgit-set-default)
79
80(defcustom stgit-show-worktree-mode 'center
81 "This variable controls where the \"Index\" and \"Work tree\"
82will be shown on in the buffer.
83
84It can be set to 'top (above all patches), 'center (show between
85applied and unapplied patches), and 'bottom (below all patches)."
86 :type '(radio (const :tag "above all patches (top)" top)
87 (const :tag "between applied and unapplied patches (center)"
88 center)
89 (const :tag "below all patches (bottom)" bottom))
90 :group 'stgit
91 :link '(variable-link stgit-show-worktree)
92 :set 'stgit-set-default)
93
0b9ea6b8
DK
94(defcustom stgit-patch-line-format "%s%m%-30n %e%d"
95 "The format string used to format patch lines.
96The format string is passed to `format-spec' and the following
97format characters are recognized:
98
99 %s - A '+', '-', '>' or space, depending on whether the patch is
100 applied, unapplied, top, or something else.
101
102 %m - An asterisk if the patch is marked, and a space otherwise.
103
104 %n - The patch name.
105
106 %e - The string \"(empty) \" if the patch is empty.
107
a0045b87
DK
108 %d - The short patch description.
109
110 %D - The short patch description, or the patch name.
111
112When `stgit-show-patch-names' is non-nil, the `stgit-noname-patch-line-format'
113variable is used instead."
114 :type 'string
115 :group 'stgit
116 :set 'stgit-set-default)
117
118(defcustom stgit-noname-patch-line-format "%s%m%e%D"
119 "The alternate format string used to format patch lines.
120It has the same semantics as `stgit-patch-line-format', and the
f3dbdbc0
GH
121display can be toggled between the two formats using \
122\\<stgit-mode-map>\\[stgit-toggle-patch-names].
a0045b87
DK
123
124The alternate form is used when the patch name is hidden."
0b9ea6b8
DK
125 :type 'string
126 :group 'stgit
127 :set 'stgit-set-default)
128
bf7e391c
GH
129(defcustom stgit-default-show-committed nil
130 "Set to nil to inhibit showing of historical git commits by default.
131
132Use \\<stgit-mode-map>\\[stgit-toggle-committed] \
133to toggle this setting and to control how many commits are
134shown."
135 :type 'boolean
136 :group 'stgit
137 :link '(variable-link stgit-show-committed))
138
139(defcustom stgit-default-committed-count 5
140 "The number of historical commits to show when `stgit-show-committed'
141is enabled."
142 :type 'number
143 :link '(variable-link stgit-committed-count))
144
a0045b87
DK
145(defcustom stgit-default-show-patch-names t
146 "If non-nil, default to showing patch names in a new stgit buffer.
147
f3dbdbc0
GH
148Use \\<stgit-mode-map>\\[stgit-toggle-patch-names] \
149to toggle the this setting in an already-started StGit buffer."
a0045b87
DK
150 :type 'boolean
151 :group 'stgit
152 :link '(variable-link stgit-show-patch-names))
153
43ee50b6
DK
154(defcustom stgit-file-line-format " %-11s %-2m %n %c"
155 "The format string used to format file lines.
156The format string is passed to `format-spec' and the following
157format characters are recognized:
158
159 %s - A string describing the status of the file.
160
161 %m - Mode change information
162
163 %n - The file name.
164
165 %c - A description of file changes."
166 :type 'string
167 :group 'stgit
168 :set 'stgit-set-default)
169
4ba91e80
DK
170(defface stgit-branch-name-face
171 '((t :inherit bold))
172 "The face used for the StGit branch name"
173 :group 'stgit)
174
175(defface stgit-top-patch-face
176 '((((background dark)) (:weight bold :foreground "yellow"))
177 (((background light)) (:weight bold :foreground "purple"))
178 (t (:weight bold)))
179 "The face used for the top patch names"
180 :group 'stgit)
181
182(defface stgit-applied-patch-face
183 '((((background dark)) (:foreground "light yellow"))
184 (((background light)) (:foreground "purple"))
185 (t ()))
186 "The face used for applied patch names"
187 :group 'stgit)
188
189(defface stgit-unapplied-patch-face
190 '((((background dark)) (:foreground "gray80"))
191 (((background light)) (:foreground "orchid"))
192 (t ()))
193 "The face used for unapplied patch names"
194 :group 'stgit)
195
bf7e391c
GH
196(defface stgit-committed-patch-face
197 '((((background dark)) (:foreground "gray50"))
198 (((background light)) (:foreground "gray50"))
199 (t ()))
200 "The face used for already committed patch names"
201 :group 'stgit)
202
4ba91e80
DK
203(defface stgit-description-face
204 '((((background dark)) (:foreground "tan"))
205 (((background light)) (:foreground "dark red")))
206 "The face used for StGit descriptions"
207 :group 'stgit)
208
209(defface stgit-index-work-tree-title-face
210 '((((supports :slant italic)) :slant italic)
211 (t :inherit bold))
212 "StGit mode face used for the \"Index\" and \"Work tree\" titles"
213 :group 'stgit)
214
215(defface stgit-unmerged-file-face
216 '((((class color) (background light)) (:foreground "red" :bold t))
217 (((class color) (background dark)) (:foreground "red" :bold t)))
218 "StGit mode face used for unmerged file status"
219 :group 'stgit)
220
221(defface stgit-unknown-file-face
222 '((((class color) (background light)) (:foreground "goldenrod" :bold t))
223 (((class color) (background dark)) (:foreground "goldenrod" :bold t)))
224 "StGit mode face used for unknown file status"
225 :group 'stgit)
226
227(defface stgit-ignored-file-face
228 '((((class color) (background light)) (:foreground "grey60"))
229 (((class color) (background dark)) (:foreground "grey40")))
230 "StGit mode face used for ignored files")
231
232(defface stgit-file-permission-face
233 '((((class color) (background light)) (:foreground "green" :bold t))
234 (((class color) (background dark)) (:foreground "green" :bold t)))
235 "StGit mode face used for permission changes."
236 :group 'stgit)
237
238(defface stgit-modified-file-face
239 '((((class color) (background light)) (:foreground "purple"))
240 (((class color) (background dark)) (:foreground "salmon")))
241 "StGit mode face used for modified file status"
242 :group 'stgit)
243
56d81fe5 244(defun stgit (dir)
fdf5e327
GH
245 "Manage StGit patches for the tree in DIR.
246
247See `stgit-mode' for commands available."
56d81fe5 248 (interactive "DDirectory: \n")
52144ce5 249 (switch-to-stgit-buffer (git-get-top-dir dir))
1f0bf00f 250 (stgit-reload))
56d81fe5 251
9d04c657
GH
252(defun stgit-assert-mode ()
253 "Signal an error if not in an StGit buffer."
254 (assert (derived-mode-p 'stgit-mode) nil "Not an StGit buffer"))
255
074a4fb0
GH
256(unless (fboundp 'git-get-top-dir)
257 (defun git-get-top-dir (dir)
258 "Retrieve the top-level directory of a git tree."
259 (let ((cdup (with-output-to-string
260 (with-current-buffer standard-output
261 (cd dir)
262 (unless (eq 0 (call-process "git" nil t nil
263 "rev-parse" "--show-cdup"))
df283a8b 264 (error "Cannot find top-level git tree for %s" dir))))))
074a4fb0
GH
265 (expand-file-name (concat (file-name-as-directory dir)
266 (car (split-string cdup "\n")))))))
267
268(defun stgit-refresh-git-status (&optional dir)
269 "If it exists, refresh the `git-status' buffer belonging to
270directory DIR or `default-directory'"
271 (when (and (fboundp 'git-find-status-buffer)
272 (fboundp 'git-refresh-status))
273 (let* ((top-dir (git-get-top-dir (or dir default-directory)))
274 (git-status-buffer (and top-dir (git-find-status-buffer top-dir))))
275 (when git-status-buffer
276 (with-current-buffer git-status-buffer
277 (git-refresh-status))))))
52144ce5 278
b894e680
DK
279(defun stgit-find-buffer (dir)
280 "Return the buffer displaying StGit patches for DIR, or nil if none."
56d81fe5
DK
281 (setq dir (file-name-as-directory dir))
282 (let ((buffers (buffer-list)))
283 (while (and buffers
284 (not (with-current-buffer (car buffers)
285 (and (eq major-mode 'stgit-mode)
286 (string= default-directory dir)))))
287 (setq buffers (cdr buffers)))
b894e680
DK
288 (and buffers (car buffers))))
289
290(defun switch-to-stgit-buffer (dir)
291 "Switch to a (possibly new) buffer displaying StGit patches for DIR."
292 (setq dir (file-name-as-directory dir))
293 (let ((buffer (stgit-find-buffer dir)))
294 (switch-to-buffer (or buffer
295 (create-stgit-buffer dir)))))
296
413f9909
GH
297(defstruct (stgit-patch
298 (:conc-name stgit-patch->))
3164eec6 299 status name desc empty files-ewoc)
56d81fe5 300
0b9ea6b8 301(defun stgit-patch-display-name (patch)
413f9909 302 (let ((name (stgit-patch->name patch)))
0b9ea6b8
DK
303 (case name
304 (:index "Index")
305 (:work "Work Tree")
306 (t (symbol-name name)))))
307
da30db2a
GH
308(defun stgit-insert-without-trailing-whitespace (text)
309 "Insert TEXT in buffer using `insert', without trailing whitespace.
310A newline is appended."
311 (unless (string-match "\\(.*?\\) *$" text)
312 (error))
313 (insert (match-string 1 text) ?\n))
314
6fdc3546
GH
315(defun stgit-line-format ()
316 "Return the current line format; one of
317`stgit-patch-line-format' and `stgit-noname-patch-line-format'"
318 (if stgit-show-patch-names
319 stgit-patch-line-format
320 stgit-noname-patch-line-format))
321
98230edd 322(defun stgit-patch-pp (patch)
413f9909 323 (let* ((status (stgit-patch->status patch))
9153ce3a 324 (start (point))
413f9909 325 (name (stgit-patch->name patch))
0b9ea6b8 326 (face (cdr (assq status stgit-patch-status-face-alist)))
6fdc3546 327 (fmt (stgit-line-format))
0b9ea6b8
DK
328 (spec (format-spec-make
329 ?s (case status
330 ('applied "+")
331 ('top ">")
332 ('unapplied "-")
333 (t " "))
334 ?m (if (memq name stgit-marked-patches)
335 "*" " ")
336 ?n (propertize (stgit-patch-display-name patch)
337 'face face
338 'syntax-table (string-to-syntax "w"))
413f9909
GH
339 ?e (if (stgit-patch->empty patch) "(empty) " "")
340 ?d (propertize (or (stgit-patch->desc patch) "")
a0045b87 341 'face 'stgit-description-face)
48047215
GH
342 ?D (propertize (let ((desc (stgit-patch->desc patch)))
343 (if (zerop (length desc))
344 (stgit-patch-display-name patch)
345 desc))
da30db2a
GH
346 'face face)))
347 (text (format-spec fmt spec)))
0b9ea6b8 348
da30db2a 349 (stgit-insert-without-trailing-whitespace text)
f9b82d36 350 (put-text-property start (point) 'entry-type 'patch)
98230edd 351 (when (memq name stgit-expanded-patches)
0de6881a 352 (stgit-insert-patch-files patch))
98230edd
DK
353 (put-text-property start (point) 'patch-data patch)))
354
56d81fe5
DK
355(defun create-stgit-buffer (dir)
356 "Create a buffer for showing StGit patches.
357Argument DIR is the repository path."
358 (let ((buf (create-file-buffer (concat dir "*stgit*")))
359 (inhibit-read-only t))
360 (with-current-buffer buf
361 (setq default-directory dir)
362 (stgit-mode)
98230edd 363 (set (make-local-variable 'stgit-ewoc)
4f7ff561 364 (ewoc-create #'stgit-patch-pp "Branch:\n\n" "--\n" t))
56d81fe5
DK
365 (setq buffer-read-only t))
366 buf))
367
072e96c5
GH
368(def-edebug-spec stgit-capture-output
369 (form body))
56d81fe5 370(defmacro stgit-capture-output (name &rest body)
e558a4ab
GH
371 "Capture StGit output and, if there was any output, show it in a window
372at the end.
373Returns nil if there was no output."
94baef5a
DK
374 (declare (debug ([&or stringp null] body))
375 (indent 1))
34afb86c
DK
376 `(let ((output-buf (get-buffer-create ,(or name "*StGit output*")))
377 (stgit-dir default-directory)
378 (inhibit-read-only t))
56d81fe5 379 (with-current-buffer output-buf
277b52af 380 (buffer-disable-undo)
34afb86c
DK
381 (erase-buffer)
382 (setq default-directory stgit-dir)
383 (setq buffer-read-only t))
56d81fe5
DK
384 (let ((standard-output output-buf))
385 ,@body)
34afb86c
DK
386 (with-current-buffer output-buf
387 (set-buffer-modified-p nil)
388 (setq buffer-read-only t)
389 (if (< (point-min) (point-max))
390 (display-buffer output-buf t)))))
56d81fe5 391
d51722b7
GH
392(defun stgit-make-run-args (args)
393 "Return a copy of ARGS with its elements converted to strings."
394 (mapcar (lambda (x)
395 ;; don't use (format "%s" ...) to limit type errors
396 (cond ((stringp x) x)
397 ((integerp x) (number-to-string x))
398 ((symbolp x) (symbol-name x))
399 (t
400 (error "Bad element in stgit-make-run-args args: %S" x))))
401 args))
402
9aecd505 403(defun stgit-run-silent (&rest args)
d51722b7 404 (setq args (stgit-make-run-args args))
56d81fe5
DK
405 (apply 'call-process "stg" nil standard-output nil args))
406
9aecd505 407(defun stgit-run (&rest args)
d51722b7 408 (setq args (stgit-make-run-args args))
9aecd505
DK
409 (let ((msgcmd (mapconcat #'identity args " ")))
410 (message "Running stg %s..." msgcmd)
411 (apply 'call-process "stg" nil standard-output nil args)
412 (message "Running stg %s...done" msgcmd)))
413
378a003d 414(defun stgit-run-git (&rest args)
d51722b7 415 (setq args (stgit-make-run-args args))
378a003d
GH
416 (let ((msgcmd (mapconcat #'identity args " ")))
417 (message "Running git %s..." msgcmd)
418 (apply 'call-process "git" nil standard-output nil args)
419 (message "Running git %s...done" msgcmd)))
420
1f60181a 421(defun stgit-run-git-silent (&rest args)
d51722b7 422 (setq args (stgit-make-run-args args))
1f60181a
GH
423 (apply 'call-process "git" nil standard-output nil args))
424
b894e680
DK
425(defun stgit-index-empty-p ()
426 "Returns non-nil if the index contains no changes from HEAD."
427 (zerop (stgit-run-git-silent "diff-index" "--cached" "--quiet" "HEAD")))
428
1629f59f
GH
429(defun stgit-work-tree-empty-p ()
430 "Returns non-nil if the work tree contains no changes from index."
431 (zerop (stgit-run-git-silent "diff-files" "--quiet")))
432
2ecb05c8
GH
433(defvar stgit-index-node)
434(defvar stgit-worktree-node)
210a2a52 435
e44674e3
GH
436(defvar stgit-did-advise nil
437 "Set to non-nil if appropriate (non-stgit) git functions have
438been advised to update the stgit status when necessary.")
439
3e528fb0
GH
440(defconst stgit-allowed-branch-name-re
441 ;; Disallow control characters, space, del, and "/:@^{}~" in
442 ;; "/"-separated parts; parts may not start with a period (.)
443 "^[^\0- ./:@^{}~\177][^\0- /:@^{}~\177]*\
444\\(/[^\0- ./:@^{}~\177][^\0- /:@^{}~\177]*\\)*$"
445 "Regular expression that (new) branch names must match.")
446
210a2a52
DK
447(defun stgit-refresh-index ()
448 (when stgit-index-node
449 (ewoc-invalidate (car stgit-index-node) (cdr stgit-index-node))))
450
451(defun stgit-refresh-worktree ()
452 (when stgit-worktree-node
453 (ewoc-invalidate (car stgit-worktree-node) (cdr stgit-worktree-node))))
454
8f702de4
GH
455(defun stgit-run-series-insert-index (ewoc)
456 (setq index-node (cons ewoc (ewoc-enter-last ewoc
457 (make-stgit-patch
458 :status 'index
459 :name :index
460 :desc nil
461 :empty nil)))
462 worktree-node (cons ewoc (ewoc-enter-last ewoc
463 (make-stgit-patch
464 :status 'work
465 :name :work
466 :desc nil
467 :empty nil)))))
468
98230edd 469(defun stgit-run-series (ewoc)
8f702de4
GH
470 (setq stgit-index-node nil
471 stgit-worktree-node nil)
bf7e391c
GH
472 (let (all-patchsyms)
473 (when stgit-show-committed
474 (let* ((base (stgit-id "{base}"))
475 (range (format "%s~%d..%s" base stgit-committed-count base)))
476 (with-temp-buffer
477 (let* ((standard-output (current-buffer))
478 (fmt (stgit-line-format))
479 (commit-abbrev (when (string-match "%-\\([0-9]+\\)n" fmt)
480 (list (format "--abbrev=%s"
481 (match-string 1 fmt)))))
482 (exit-status (apply 'stgit-run-git-silent
483 "--no-pager"
484 "log" "--reverse" "--pretty=oneline"
485 "--abbrev-commit"
486 `(,@commit-abbrev
487 ,range))))
488 (goto-char (point-min))
489 (if (not (zerop exit-status))
490 (message "Failed to run git log")
491 (while (not (eobp))
492 (unless (looking-at
493 "\\([0-9a-f]+\\)\\(\\.\\.\\.\\)? \\(.*\\)")
494 (error "Syntax error in output from git log"))
495 (let* ((state 'committed)
496 (name (intern (match-string 1)))
497 (desc (match-string 3))
498 (empty nil))
499 (setq all-patchsyms (cons name all-patchsyms))
500 (ewoc-enter-last ewoc
501 (make-stgit-patch
502 :status state
503 :name name
504 :desc desc
505 :empty empty)))
506 (forward-line 1)))))))
507 (let ((inserted-index (not stgit-show-worktree))
508 index-node
509 worktree-node)
510 (with-temp-buffer
511 (let* ((standard-output (current-buffer))
512 (exit-status (stgit-run-silent "series"
513 "--description" "--empty")))
514 (goto-char (point-min))
515 (if (not (zerop exit-status))
516 (cond ((looking-at "stg series: \\(.*\\)")
517 (setq inserted-index t)
518 (ewoc-set-hf ewoc (car (ewoc-get-hf ewoc))
519 (substitute-command-keys
520 "-- not initialized; run \\[stgit-init]")))
521 ((looking-at ".*")
522 (error "Error running stg: %s"
523 (match-string 0))))
524 (while (not (eobp))
525 (unless (looking-at
526 "\\([0 ]\\)\\([>+-]\\)\\( \\)\\([^ ]+\\) *[|#] \\(.*\\)")
527 (error "Syntax error in output from stg series"))
528 (let* ((state-str (match-string 2))
529 (state (cond ((string= state-str ">") 'top)
530 ((string= state-str "+") 'applied)
531 ((string= state-str "-") 'unapplied)))
532 (name (intern (match-string 4)))
533 (desc (match-string 5))
534 (empty (string= (match-string 1) "0")))
535 (unless inserted-index
536 (when (or (eq stgit-show-worktree-mode 'top)
537 (and (eq stgit-show-worktree-mode 'center)
538 (eq state 'unapplied)))
539 (setq inserted-index t)
540 (stgit-run-series-insert-index ewoc)))
541 (setq all-patchsyms (cons name all-patchsyms))
542 (ewoc-enter-last ewoc
543 (make-stgit-patch
544 :status state
545 :name name
546 :desc desc
547 :empty empty)))
548 (forward-line 1)))))
8f702de4 549 (unless inserted-index
bf7e391c
GH
550 (stgit-run-series-insert-index ewoc))
551 (setq stgit-index-node index-node
552 stgit-worktree-node worktree-node
553 stgit-marked-patches (intersection stgit-marked-patches
554 all-patchsyms)))))
98230edd 555
3e528fb0
GH
556(defun stgit-current-branch ()
557 "Return the name of the current branch."
558 (substring (with-output-to-string
559 (stgit-run-silent "branch"))
560 0 -1))
561
1f0bf00f 562(defun stgit-reload ()
a53347d9 563 "Update the contents of the StGit buffer."
56d81fe5 564 (interactive)
9d04c657 565 (stgit-assert-mode)
56d81fe5
DK
566 (let ((inhibit-read-only t)
567 (curline (line-number-at-pos))
a9089e68
GH
568 (curpatch (stgit-patch-name-at-point))
569 (curfile (stgit-patched-file-at-point)))
98230edd
DK
570 (ewoc-filter stgit-ewoc #'(lambda (x) nil))
571 (ewoc-set-hf stgit-ewoc
572 (concat "Branch: "
3e528fb0
GH
573 (propertize (stgit-current-branch)
574 'face 'stgit-branch-name-face)
4f7ff561 575 "\n\n")
ce3b6130
DK
576 (if stgit-show-worktree
577 "--"
578 (propertize
579 (substitute-command-keys "--\n\"\\[stgit-toggle-worktree]\"\
580 shows the working tree\n")
6a73154a 581 'face 'stgit-description-face)))
98230edd 582 (stgit-run-series stgit-ewoc)
2570f6e5
GH
583 (unless (and curpatch
584 (stgit-goto-patch curpatch
585 (and curfile (stgit-file->file curfile))))
79473ca5
GH
586 (goto-char (point-min))
587 (forward-line (1- curline))
588 (move-to-column (stgit-goal-column))))
074a4fb0 589 (stgit-refresh-git-status))
56d81fe5 590
1f60181a
GH
591(defconst stgit-file-status-code-strings
592 (mapcar (lambda (arg)
593 (cons (car arg)
a6d9a852
GH
594 (propertize (cadr arg) 'face (car (cddr arg)))))
595 '((add "Added" stgit-modified-file-face)
596 (copy "Copied" stgit-modified-file-face)
597 (delete "Deleted" stgit-modified-file-face)
598 (modify "Modified" stgit-modified-file-face)
599 (rename "Renamed" stgit-modified-file-face)
600 (mode-change "Mode change" stgit-modified-file-face)
601 (unmerged "Unmerged" stgit-unmerged-file-face)
d9473917
GH
602 (unknown "Unknown" stgit-unknown-file-face)
603 (ignore "Ignored" stgit-ignored-file-face)))
1f60181a
GH
604 "Alist of code symbols to description strings")
605
000f337c
GH
606(defconst stgit-patch-status-face-alist
607 '((applied . stgit-applied-patch-face)
608 (top . stgit-top-patch-face)
609 (unapplied . stgit-unapplied-patch-face)
bf7e391c 610 (committed . stgit-committed-patch-face)
9153ce3a
GH
611 (index . stgit-index-work-tree-title-face)
612 (work . stgit-index-work-tree-title-face))
000f337c
GH
613 "Alist of face to use for a given patch status")
614
3164eec6
DK
615(defun stgit-file-status-code-as-string (file)
616 "Return stgit status code for FILE as a string"
413f9909 617 (let* ((code (assq (stgit-file->status file)
3164eec6 618 stgit-file-status-code-strings))
413f9909 619 (score (stgit-file->cr-score file)))
3164eec6 620 (when code
43ee50b6
DK
621 (if (and score (/= score 100))
622 (format "%s %s" (cdr code)
623 (propertize (format "%d%%" score)
624 'face 'stgit-description-face))
625 (cdr code)))))
1f60181a 626
a6d9a852 627(defun stgit-file-status-code (str &optional score)
1f60181a
GH
628 "Return stgit status code from git status string"
629 (let ((code (assoc str '(("A" . add)
630 ("C" . copy)
631 ("D" . delete)
d9473917 632 ("I" . ignore)
1f60181a
GH
633 ("M" . modify)
634 ("R" . rename)
635 ("T" . mode-change)
636 ("U" . unmerged)
637 ("X" . unknown)))))
a6d9a852
GH
638 (setq code (if code (cdr code) 'unknown))
639 (when (stringp score)
640 (if (> (length score) 0)
641 (setq score (string-to-number score))
642 (setq score nil)))
643 (if score (cons code score) code)))
644
645(defconst stgit-file-type-strings
646 '((#o100 . "file")
647 (#o120 . "symlink")
648 (#o160 . "subproject"))
649 "Alist of names of file types")
650
651(defun stgit-file-type-string (type)
47271f41
GH
652 "Return string describing file type TYPE (the high bits of file permission).
653Cf. `stgit-file-type-strings' and `stgit-file-type-change-string'."
a6d9a852
GH
654 (let ((type-str (assoc type stgit-file-type-strings)))
655 (or (and type-str (cdr type-str))
656 (format "unknown type %o" type))))
657
658(defun stgit-file-type-change-string (old-perm new-perm)
47271f41
GH
659 "Return string describing file type change from OLD-PERM to NEW-PERM.
660Cf. `stgit-file-type-string'."
a6d9a852
GH
661 (let ((old-type (lsh old-perm -9))
662 (new-type (lsh new-perm -9)))
663 (cond ((= old-type new-type) "")
664 ((zerop new-type) "")
665 ((zerop old-type)
666 (if (= new-type #o100)
667 ""
43ee50b6
DK
668 (format "(%s)" (stgit-file-type-string new-type))))
669 (t (format "(%s -> %s)"
a6d9a852
GH
670 (stgit-file-type-string old-type)
671 (stgit-file-type-string new-type))))))
672
673(defun stgit-file-mode-change-string (old-perm new-perm)
47271f41
GH
674 "Return string describing file mode change from OLD-PERM to NEW-PERM.
675Cf. `stgit-file-type-change-string'."
a6d9a852
GH
676 (setq old-perm (logand old-perm #o777)
677 new-perm (logand new-perm #o777))
678 (if (or (= old-perm new-perm)
679 (zerop old-perm)
680 (zerop new-perm))
681 ""
682 (let* ((modified (logxor old-perm new-perm))
683 (not-x-modified (logand (logxor old-perm new-perm) #o666)))
684 (cond ((zerop modified) "")
685 ((and (zerop not-x-modified)
686 (or (and (eq #o111 (logand old-perm #o111))
687 (propertize "-x" 'face 'stgit-file-permission-face))
688 (and (eq #o111 (logand new-perm #o111))
689 (propertize "+x" 'face
690 'stgit-file-permission-face)))))
691 (t (concat (propertize (format "%o" old-perm)
692 'face 'stgit-file-permission-face)
693 (propertize " -> "
694 'face 'stgit-description-face)
695 (propertize (format "%o" new-perm)
696 'face 'stgit-file-permission-face)))))))
1f60181a 697
413f9909
GH
698(defstruct (stgit-file
699 (:conc-name stgit-file->))
0de6881a
DK
700 old-perm new-perm copy-or-rename cr-score cr-from cr-to status file)
701
ca027a87 702(defun stgit-describe-copy-or-rename (file)
6a73154a
GH
703 (let ((arrow (concat " " (propertize "->" 'face 'stgit-description-face) " "))
704 from to common-head common-tail)
ca027a87
GH
705
706 (when stgit-abbreviate-copies-and-renames
413f9909
GH
707 (setq from (split-string (stgit-file->cr-from file) "/")
708 to (split-string (stgit-file->cr-to file) "/"))
ca027a87
GH
709
710 (while (and from to (cdr from) (cdr to)
711 (string-equal (car from) (car to)))
712 (setq common-head (cons (car from) common-head)
713 from (cdr from)
714 to (cdr to)))
715 (setq common-head (nreverse common-head)
716 from (nreverse from)
717 to (nreverse to))
718 (while (and from to (cdr from) (cdr to)
719 (string-equal (car from) (car to)))
720 (setq common-tail (cons (car from) common-tail)
721 from (cdr from)
722 to (cdr to)))
723 (setq from (nreverse from)
724 to (nreverse to)))
725
726 (if (or common-head common-tail)
727 (concat (if common-head
728 (mapconcat #'identity common-head "/")
729 "")
730 (if common-head "/" "")
731 (propertize "{" 'face 'stgit-description-face)
732 (mapconcat #'identity from "/")
733 arrow
734 (mapconcat #'identity to "/")
735 (propertize "}" 'face 'stgit-description-face)
736 (if common-tail "/" "")
737 (if common-tail
738 (mapconcat #'identity common-tail "/")
739 ""))
413f9909 740 (concat (stgit-file->cr-from file) arrow (stgit-file->cr-to file)))))
ca027a87 741
3164eec6 742(defun stgit-file-pp (file)
43ee50b6
DK
743 (let ((start (point))
744 (spec (format-spec-make
745 ?s (stgit-file-status-code-as-string file)
746 ?m (stgit-file-mode-change-string
413f9909
GH
747 (stgit-file->old-perm file)
748 (stgit-file->new-perm file))
749 ?n (if (stgit-file->copy-or-rename file)
43ee50b6 750 (stgit-describe-copy-or-rename file)
413f9909 751 (stgit-file->file file))
43ee50b6 752 ?c (propertize (stgit-file-type-change-string
413f9909
GH
753 (stgit-file->old-perm file)
754 (stgit-file->new-perm file))
43ee50b6 755 'face 'stgit-description-face))))
da30db2a
GH
756 (stgit-insert-without-trailing-whitespace
757 (format-spec stgit-file-line-format spec))
0de6881a 758 (add-text-properties start (point)
3164eec6
DK
759 (list 'entry-type 'file
760 'file-data file))))
0de6881a 761
7567401c
GH
762(defun stgit-find-copies-harder-diff-arg ()
763 "Return the flag to use with `git-diff' depending on the
b6df231c
GH
764`stgit-find-copies-harder' flag."
765 (if stgit-find-copies-harder "--find-copies-harder" "-C"))
7567401c 766
d9473917
GH
767(defun stgit-insert-ls-files (args file-flag)
768 (let ((start (point)))
769 (apply 'stgit-run-git
770 (append '("ls-files" "--exclude-standard" "-z") args))
771 (goto-char start)
772 (while (looking-at "\\([^\0]*\\)\0")
773 (let ((name-len (- (match-end 0) (match-beginning 0))))
774 (insert ":0 0 0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 " file-flag "\0")
775 (forward-char name-len)))))
776
7f972e9b
GH
777(defun stgit-process-files (callback)
778 (goto-char (point-min))
779 (when (looking-at "[0-9A-Fa-f]\\{40\\}\0")
780 (goto-char (match-end 0)))
781 (while (looking-at ":\\([0-7]+\\) \\([0-7]+\\) [0-9A-Fa-f]\\{40\\} [0-9A-Fa-f]\\{40\\} ")
782 (let ((old-perm (string-to-number (match-string 1) 8))
783 (new-perm (string-to-number (match-string 2) 8)))
784 (goto-char (match-end 0))
785 (let ((file
786 (cond ((looking-at
787 "\\([CR]\\)\\([0-9]*\\)\0\\([^\0]*\\)\0\\([^\0]*\\)\0")
788 (let* ((patch-status (stgit-patch->status patch))
789 (file-subexp (if (eq patch-status 'unapplied)
790 3
791 4))
792 (file (match-string file-subexp)))
793 (make-stgit-file
794 :old-perm old-perm
795 :new-perm new-perm
796 :copy-or-rename t
797 :cr-score (string-to-number (match-string 2))
798 :cr-from (match-string 3)
799 :cr-to (match-string 4)
800 :status (stgit-file-status-code
801 (match-string 1))
802 :file file)))
803 ((looking-at "\\([ABD-QS-Z]\\)\0\\([^\0]*\\)\0")
804 (make-stgit-file
805 :old-perm old-perm
806 :new-perm new-perm
807 :copy-or-rename nil
808 :cr-score nil
809 :cr-from nil
810 :cr-to nil
811 :status (stgit-file-status-code
812 (match-string 1))
813 :file (match-string 2))))))
814 (goto-char (match-end 0))
815 (funcall callback file)))))
816
817
0de6881a 818(defun stgit-insert-patch-files (patch)
88134ff7
GH
819 "Expand (show modification of) the patch PATCH after the line
820at point."
d2efc9d5
GH
821 (let* ((patchsym (stgit-patch->name patch))
822 (end (point-marker))
823 (args (list "-z" (stgit-find-copies-harder-diff-arg)))
824 (ewoc (ewoc-create #'stgit-file-pp nil nil t))
825 (show-ignored stgit-show-ignored)
826 (show-unknown stgit-show-unknown))
0434bec1 827 (set-marker-insertion-type end t)
413f9909 828 (setf (stgit-patch->files-ewoc patch) ewoc)
0de6881a 829 (with-temp-buffer
ea305902
GH
830 (let ((standard-output (current-buffer)))
831 (apply 'stgit-run-git
832 (cond ((eq patchsym :work)
0b71b4dc
GH
833 (let (standard-output)
834 (stgit-run-git "update-index" "--refresh"))
ea305902
GH
835 `("diff-files" "-0" ,@args))
836 ((eq patchsym :index)
837 `("diff-index" ,@args "--cached" "HEAD"))
838 (t
839 `("diff-tree" ,@args "-r" ,(stgit-id patchsym)))))
840
841 (when (and (eq patchsym :work))
d2efc9d5 842 (when show-ignored
ea305902 843 (stgit-insert-ls-files '("--ignored" "--others") "I"))
d2efc9d5 844 (when show-unknown
7f972e9b
GH
845 (stgit-insert-ls-files '("--directory" "--no-empty-directory"
846 "--others")
847 "X"))
ea305902
GH
848 (sort-regexp-fields nil ":[^\0]*\0\\([^\0]*\\)\0" "\\1"
849 (point-min) (point-max)))
850
7f972e9b 851 (stgit-process-files (lambda (file) (ewoc-enter-last ewoc file)))
ea305902
GH
852
853 (unless (ewoc-nth ewoc 0)
854 (ewoc-set-hf ewoc ""
855 (concat " "
856 (propertize "<no files>"
857 'face 'stgit-description-face)
858 "\n")))))
0434bec1 859 (goto-char end)))
07f464e0 860
030f0535
GH
861(defun stgit-find-file (&optional other-window)
862 (let* ((file (or (stgit-patched-file-at-point)
863 (error "No file at point")))
413f9909 864 (filename (expand-file-name (stgit-file->file file))))
0de6881a
DK
865 (unless (file-exists-p filename)
866 (error "File does not exist"))
030f0535
GH
867 (funcall (if other-window 'find-file-other-window 'find-file)
868 filename)
413f9909 869 (when (eq (stgit-file->status file) 'unmerged)
030f0535 870 (smerge-mode 1))))
acc5652f 871
afbf766b 872(defun stgit-expand (&optional patches collapse)
fd64ee57 873 "Show the contents of marked patches, or the patch at point.
afbf766b
GH
874
875See also `stgit-collapse'.
876
877Non-interactively, operate on PATCHES, and collapse instead of
878expand if COLLAPSE is not nil."
beac0f14 879 (interactive (list (stgit-patches-marked-or-at-point t)))
9d04c657 880 (stgit-assert-mode)
afbf766b
GH
881 (let ((patches-diff (funcall (if collapse #'intersection #'set-difference)
882 patches stgit-expanded-patches)))
883 (setq stgit-expanded-patches
884 (if collapse
885 (set-difference stgit-expanded-patches patches-diff)
886 (append stgit-expanded-patches patches-diff)))
887 (ewoc-map #'(lambda (patch)
413f9909 888 (memq (stgit-patch->name patch) patches-diff))
afbf766b
GH
889 stgit-ewoc))
890 (move-to-column (stgit-goal-column)))
891
892(defun stgit-collapse (&optional patches)
fd64ee57 893 "Hide the contents of marked patches, or the patch at point.
afbf766b
GH
894
895See also `stgit-expand'."
beac0f14 896 (interactive (list (stgit-patches-marked-or-at-point t)))
9d04c657 897 (stgit-assert-mode)
afbf766b
GH
898 (stgit-expand patches t))
899
50d88c67 900(defun stgit-select-patch ()
98230edd 901 (let ((patchname (stgit-patch-name-at-point)))
afbf766b
GH
902 (stgit-expand (list patchname)
903 (memq patchname stgit-expanded-patches))))
acc5652f 904
7f972e9b
GH
905(defun stgit-expand-directory (file)
906 (let* ((patch (stgit-patch-at-point))
907 (ewoc (stgit-patch->files-ewoc patch))
908 (node (ewoc-locate ewoc))
909 (filename (stgit-file->file file))
910 (start (make-marker))
911 (end (make-marker)))
912
913 (save-excursion
914 (forward-line 1)
915 (set-marker start (point))
916 (set-marker end (point))
917 (set-marker-insertion-type end t))
918
919 (assert (string-match "/$" filename))
920 ;; remove trailing "/"
921 (setf (stgit-file->file file) (substring filename 0 -1))
922 (ewoc-invalidate ewoc node)
923
924 (with-temp-buffer
925 (let ((standard-output (current-buffer)))
926 (stgit-insert-ls-files (list "--directory" "--others"
927 "--no-empty-directory" "--"
928 filename)
929 "X")
930 (stgit-process-files (lambda (f)
931 (setq node (ewoc-enter-after ewoc node f))))))
932
62548eec
GH
933 (move-to-column (stgit-goal-column))
934
7f972e9b
GH
935 (let ((inhibit-read-only t))
936 (put-text-property start end 'patch-data patch))))
937
938(defun stgit-select-file ()
939 (let* ((file (or (stgit-patched-file-at-point)
940 (error "No file at point")))
941 (filename (stgit-file->file file)))
942 (if (string-match "/$" filename)
943 (stgit-expand-directory file)
944 (stgit-find-file))))
945
378a003d 946(defun stgit-select ()
da01a29b
GH
947 "With point on a patch, toggle showing files in the patch.
948
949With point on a file, open the associated file. Opens the target
950file for (applied) copies and renames."
378a003d 951 (interactive)
9d04c657 952 (stgit-assert-mode)
50d88c67
DK
953 (case (get-text-property (point) 'entry-type)
954 ('patch
955 (stgit-select-patch))
956 ('file
7f972e9b 957 (stgit-select-file))
50d88c67
DK
958 (t
959 (error "No patch or file on line"))))
378a003d
GH
960
961(defun stgit-find-file-other-window ()
962 "Open file at point in other window"
963 (interactive)
9d04c657 964 (stgit-assert-mode)
030f0535 965 (stgit-find-file t))
378a003d 966
d9b954c7
GH
967(defun stgit-find-file-merge ()
968 "Open file at point and merge it using `smerge-ediff'."
969 (interactive)
9d04c657 970 (stgit-assert-mode)
d9b954c7
GH
971 (stgit-find-file t)
972 (smerge-ediff))
973
83327d53 974(defun stgit-quit ()
a53347d9 975 "Hide the stgit buffer."
83327d53 976 (interactive)
9d04c657 977 (stgit-assert-mode)
83327d53
GH
978 (bury-buffer))
979
0f076fe6 980(defun stgit-git-status ()
a53347d9 981 "Show status using `git-status'."
0f076fe6 982 (interactive)
9d04c657 983 (stgit-assert-mode)
0f076fe6 984 (unless (fboundp 'git-status)
df283a8b 985 (error "The stgit-git-status command requires git-status"))
0f076fe6
GH
986 (let ((dir default-directory))
987 (save-selected-window
988 (pop-to-buffer nil)
989 (git-status dir))))
990
58f72f16
GH
991(defun stgit-goal-column ()
992 "Return goal column for the current line"
50d88c67
DK
993 (case (get-text-property (point) 'entry-type)
994 ('patch 2)
995 ('file 4)
996 (t 0)))
58f72f16
GH
997
998(defun stgit-next-line (&optional arg)
378a003d 999 "Move cursor vertically down ARG lines"
58f72f16 1000 (interactive "p")
9d04c657 1001 (stgit-assert-mode)
58f72f16
GH
1002 (next-line arg)
1003 (move-to-column (stgit-goal-column)))
378a003d 1004
58f72f16 1005(defun stgit-previous-line (&optional arg)
378a003d 1006 "Move cursor vertically up ARG lines"
58f72f16 1007 (interactive "p")
9d04c657 1008 (stgit-assert-mode)
58f72f16
GH
1009 (previous-line arg)
1010 (move-to-column (stgit-goal-column)))
378a003d
GH
1011
1012(defun stgit-next-patch (&optional arg)
98230edd 1013 "Move cursor down ARG patches."
378a003d 1014 (interactive "p")
9d04c657 1015 (stgit-assert-mode)
98230edd
DK
1016 (ewoc-goto-next stgit-ewoc (or arg 1))
1017 (move-to-column goal-column))
378a003d
GH
1018
1019(defun stgit-previous-patch (&optional arg)
98230edd 1020 "Move cursor up ARG patches."
378a003d 1021 (interactive "p")
9d04c657 1022 (stgit-assert-mode)
98230edd
DK
1023 (ewoc-goto-prev stgit-ewoc (or arg 1))
1024 (move-to-column goal-column))
378a003d 1025
56d81fe5
DK
1026(defvar stgit-mode-hook nil
1027 "Run after `stgit-mode' is setup.")
1028
1029(defvar stgit-mode-map nil
1030 "Keymap for StGit major mode.")
1031
1032(unless stgit-mode-map
5038381d
GH
1033 (let ((diff-map (make-sparse-keymap))
1034 (toggle-map (make-sparse-keymap)))
d9b954c7
GH
1035 (mapc (lambda (arg) (define-key diff-map (car arg) (cdr arg)))
1036 '(("b" . stgit-diff-base)
1037 ("c" . stgit-diff-combined)
1038 ("m" . stgit-find-file-merge)
1039 ("o" . stgit-diff-ours)
e02b46e5 1040 ("r" . stgit-diff-range)
d9b954c7 1041 ("t" . stgit-diff-theirs)))
ce3b6130 1042 (mapc (lambda (arg) (define-key toggle-map (car arg) (cdr arg)))
a0045b87
DK
1043 '(("n" . stgit-toggle-patch-names)
1044 ("t" . stgit-toggle-worktree)
bf7e391c 1045 ("h" . stgit-toggle-committed)
d9473917
GH
1046 ("i" . stgit-toggle-ignored)
1047 ("u" . stgit-toggle-unknown)))
ce3b6130
DK
1048 (setq stgit-mode-map (make-keymap))
1049 (suppress-keymap stgit-mode-map)
1050 (mapc (lambda (arg) (define-key stgit-mode-map (car arg) (cdr arg)))
d11e0621
GH
1051 `((" " . stgit-mark-down)
1052 ("m" . stgit-mark-down)
ce3b6130
DK
1053 ("\d" . stgit-unmark-up)
1054 ("u" . stgit-unmark-down)
1055 ("?" . stgit-help)
1056 ("h" . stgit-help)
1057 ("\C-p" . stgit-previous-line)
1058 ("\C-n" . stgit-next-line)
1059 ([up] . stgit-previous-line)
1060 ([down] . stgit-next-line)
1061 ("p" . stgit-previous-patch)
1062 ("n" . stgit-next-patch)
1063 ("\M-{" . stgit-previous-patch)
1064 ("\M-}" . stgit-next-patch)
1065 ("s" . stgit-git-status)
408fa7cb 1066 ("g" . stgit-reload-or-repair)
ce3b6130
DK
1067 ("r" . stgit-refresh)
1068 ("\C-c\C-r" . stgit-rename)
1069 ("e" . stgit-edit)
1070 ("M" . stgit-move-patches)
1071 ("S" . stgit-squash)
1072 ("N" . stgit-new)
2acb7116 1073 ("c" . stgit-new-and-refresh)
e9fdd4ea
GH
1074 ("\C-c\C-c" . stgit-commit)
1075 ("\C-c\C-u" . stgit-uncommit)
1629f59f 1076 ("U" . stgit-revert)
51783171 1077 ("R" . stgit-resolve-file)
ce3b6130 1078 ("\r" . stgit-select)
afbf766b
GH
1079 ("+" . stgit-expand)
1080 ("-" . stgit-collapse)
ce3b6130 1081 ("o" . stgit-find-file-other-window)
dde3ab4d 1082 ("i" . stgit-toggle-index)
ce3b6130
DK
1083 (">" . stgit-push-next)
1084 ("<" . stgit-pop-next)
1085 ("P" . stgit-push-or-pop)
1086 ("G" . stgit-goto)
d9b954c7 1087 ("=" . stgit-diff)
ce3b6130 1088 ("D" . stgit-delete)
b8463f1d 1089 ([?\C-/] . stgit-undo)
ce3b6130 1090 ("\C-_" . stgit-undo)
b8463f1d
GH
1091 ([?\C-c ?\C-/] . stgit-redo)
1092 ("\C-c\C-_" . stgit-redo)
ce3b6130 1093 ("B" . stgit-branch)
380a021f 1094 ("\C-c\C-b" . stgit-rebase)
ce3b6130 1095 ("t" . ,toggle-map)
d9b954c7 1096 ("d" . ,diff-map)
e8faa44b
DK
1097 ("q" . stgit-quit)
1098 ("!" . stgit-execute))))
5038381d
GH
1099
1100 (let ((at-unmerged-file '(let ((file (stgit-patched-file-at-point)))
413f9909 1101 (and file (eq (stgit-file->status file)
5038381d
GH
1102 'unmerged))))
1103 (patch-collapsed-p '(lambda (p) (not (memq p stgit-expanded-patches)))))
1104 (easy-menu-define stgit-menu stgit-mode-map
1105 "StGit Menu"
1106 `("StGit"
1107 ["Reload" stgit-reload-or-repair
1108 :help "Reload StGit status from disk"]
1109 ["Repair" stgit-repair
1110 :keys "\\[universal-argument] \\[stgit-reload-or-repair]"
1111 :help "Repair StGit metadata"]
1112 "-"
1113 ["Undo" stgit-undo t]
1114 ["Redo" stgit-redo t]
1115 "-"
1116 ["Git status" stgit-git-status :active (fboundp 'git-status)]
1117 "-"
1118 ["New patch" stgit-new-and-refresh
1119 :help "Create a new patch from changes in index or work tree"
1120 :active (not (and (stgit-index-empty-p) (stgit-work-tree-empty-p)))]
1121 ["New empty patch" stgit-new
1122 :help "Create a new, empty patch"]
1123 ["(Un)mark patch" stgit-toggle-mark
1124 :label (if (memq (stgit-patch-name-at-point nil t)
1125 stgit-marked-patches)
1126 "Unmark patch" "Mark patch")
1127 :active (stgit-patch-name-at-point nil t)]
1128 ["Expand/collapse patch"
1129 (let ((patches (stgit-patches-marked-or-at-point)))
1130 (if (member-if ,patch-collapsed-p patches)
1131 (stgit-expand patches)
1132 (stgit-collapse patches)))
1133 :label (if (member-if ,patch-collapsed-p
1134 (stgit-patches-marked-or-at-point))
1135 "Expand patches"
1136 "Collapse patches")
1137 :active (stgit-patches-marked-or-at-point)]
1138 ["Edit patch" stgit-edit
1139 :help "Edit patch comment"
1140 :active (stgit-patch-name-at-point nil t)]
1141 ["Rename patch" stgit-rename :active (stgit-patch-name-at-point nil t)]
1142 ["Push/pop patch" stgit-push-or-pop
7c11b754
GH
1143 :label (if (subsetp (stgit-patches-marked-or-at-point nil t)
1144 (stgit-applied-patchsyms t))
1145 "Pop patches" "Push patches")]
beac0f14
GH
1146 ["Delete patches" stgit-delete
1147 :active (stgit-patches-marked-or-at-point nil t)]
5038381d
GH
1148 "-"
1149 ["Move patches" stgit-move-patches
1150 :active stgit-marked-patches
fd64ee57 1151 :help "Move marked patch(es) to point"]
5038381d
GH
1152 ["Squash patches" stgit-squash
1153 :active (> (length stgit-marked-patches) 1)
fd64ee57 1154 :help "Merge marked patches into one"]
5038381d
GH
1155 "-"
1156 ["Refresh top patch" stgit-refresh
1157 :active (not (and (stgit-index-empty-p) (stgit-work-tree-empty-p)))
1158 :help "Refresh the top patch with changes in index or work tree"]
1159 ["Refresh this patch" (stgit-refresh t)
1160 :keys "\\[universal-argument] \\[stgit-refresh]"
fd64ee57 1161 :help "Refresh marked patch with changes in index or work tree"
5038381d
GH
1162 :active (and (not (and (stgit-index-empty-p)
1163 (stgit-work-tree-empty-p)))
1164 (stgit-patch-name-at-point nil t))]
1165 "-"
1166 ["Find file" stgit-select
1167 :active (eq (get-text-property (point) 'entry-type) 'file)]
1168 ["Open file" stgit-find-file-other-window
1169 :active (eq (get-text-property (point) 'entry-type) 'file)]
1170 ["Toggle file index" stgit-toggle-index
1171 :active (and (eq (get-text-property (point) 'entry-type) 'file)
1172 (memq (stgit-patch-name-at-point) '(:work :index)))
1173 :label (if (eq (stgit-patch-name-at-point) :work)
1174 "Move change to index"
1175 "Move change to work tree")]
1176 "-"
1177 ["Show diff" stgit-diff
1178 :active (get-text-property (point) 'entry-type)]
e02b46e5
KW
1179 ["Show diff for range of applied patches" stgit-diff-range
1180 :active (= (length stgit-marked-patches) 1)]
5038381d
GH
1181 ("Merge"
1182 :active (stgit-git-index-unmerged-p)
1183 ["Combined diff" stgit-diff-combined
1184 :active (memq (stgit-patch-name-at-point nil nil) '(:work :index))]
1185 ["Diff against base" stgit-diff-base
1186 :help "Show diff against the common base"
1187 :active (memq (stgit-patch-name-at-point nil nil) '(:work :index))]
1188 ["Diff against ours" stgit-diff-ours
1189 :help "Show diff against our branch"
1190 :active (memq (stgit-patch-name-at-point nil nil) '(:work :index))]
1191 ["Diff against theirs" stgit-diff-theirs
1192 :help "Show diff against their branch"
1193 :active (memq (stgit-patch-name-at-point nil nil) '(:work :index))]
1194 "-"
1195 ["Interactive merge" stgit-find-file-merge
1196 :help "Interactively merge the file"
1197 :active ,at-unmerged-file]
1198 ["Resolve file" stgit-resolve-file
1199 :help "Mark file conflict as resolved"
1200 :active ,at-unmerged-file]
1201 )
1202 "-"
1203 ["Show index & work tree" stgit-toggle-worktree :style toggle
1204 :selected stgit-show-worktree]
1205 ["Show unknown files" stgit-toggle-unknown :style toggle
1206 :selected stgit-show-unknown :active stgit-show-worktree]
1207 ["Show ignored files" stgit-toggle-ignored :style toggle
1208 :selected stgit-show-ignored :active stgit-show-worktree]
a0045b87
DK
1209 ["Show patch names" stgit-toggle-patch-names :style toggle
1210 :selected stgit-show-patch-names]
bf7e391c
GH
1211 ["Show recent commits" stgit-toggle-committed :style toggle
1212 :selected stgit-show-committed]
5038381d
GH
1213 "-"
1214 ["Switch branches" stgit-branch t
3e528fb0 1215 :help "Switch to or create another branch"]
5038381d
GH
1216 ["Rebase branch" stgit-rebase t
1217 :help "Rebase the current branch"]
1218 ))))
1219
1220;; disable tool bar editing buttons
1221(put 'stgit-mode 'mode-class 'special)
56d81fe5
DK
1222
1223(defun stgit-mode ()
1224 "Major mode for interacting with StGit.
fdf5e327
GH
1225
1226Start StGit using \\[stgit].
1227
1228Basic commands:
1229\\<stgit-mode-map>\
1230\\[stgit-help] Show this help text
1231\\[stgit-quit] Hide the StGit buffer
1232\\[describe-bindings] Show all key bindings
1233
1234\\[stgit-reload-or-repair] Reload the StGit buffer
1235\\[universal-argument] \\[stgit-reload-or-repair] Repair StGit metadata
1236
1237\\[stgit-undo] Undo most recent StGit operation
1238\\[stgit-redo] Undo recent undo
1239
1240\\[stgit-git-status] Run `git-status' (if available)
1241
e8faa44b
DK
1242\\[stgit-execute] Run an stg shell command
1243
fdf5e327
GH
1244Movement commands:
1245\\[stgit-previous-line] Move to previous line
1246\\[stgit-next-line] Move to next line
1247\\[stgit-previous-patch] Move to previous patch
1248\\[stgit-next-patch] Move to next patch
1249
d11e0621 1250\\[stgit-mark-down] Mark patch and move down
fdf5e327
GH
1251\\[stgit-unmark-up] Unmark patch and move up
1252\\[stgit-unmark-down] Unmark patch and move down
1253
1254Commands for patches:
1255\\[stgit-select] Toggle showing changed files in patch
1256\\[stgit-refresh] Refresh patch with changes in index or work tree
1257\\[stgit-diff] Show the patch log and diff
1258
fd64ee57
GH
1259\\[stgit-expand] Show changes in marked patches
1260\\[stgit-collapse] Hide changes in marked patches
afbf766b 1261
2acb7116 1262\\[stgit-new-and-refresh] Create a new patch from index or work tree
c20b20a5
GH
1263\\[stgit-new] Create a new, empty patch
1264
fdf5e327
GH
1265\\[stgit-rename] Rename patch
1266\\[stgit-edit] Edit patch description
1267\\[stgit-delete] Delete patch(es)
1268
1629f59f 1269\\[stgit-revert] Revert all changes in index or work tree
dde3ab4d 1270\\[stgit-toggle-index] Toggle all changes between index and work tree
1629f59f 1271
fdf5e327
GH
1272\\[stgit-push-next] Push next patch onto stack
1273\\[stgit-pop-next] Pop current patch from stack
c20b20a5
GH
1274\\[stgit-push-or-pop] Push or pop marked patches
1275\\[stgit-goto] Make patch at point current by popping or pushing
fdf5e327
GH
1276
1277\\[stgit-squash] Squash (meld together) patches
c20b20a5 1278\\[stgit-move-patches] Move marked patches to point
fdf5e327
GH
1279
1280\\[stgit-commit] Commit patch(es)
1281\\[stgit-uncommit] Uncommit patch(es)
1282
1283Commands for files:
1284\\[stgit-select] Open the file in this window
1285\\[stgit-find-file-other-window] Open the file in another window
1286\\[stgit-diff] Show the file's diff
1287
dde3ab4d 1288\\[stgit-toggle-index] Toggle change between index and work tree
1629f59f 1289\\[stgit-revert] Revert changes to file
fdf5e327
GH
1290
1291Display commands:
a0045b87 1292\\[stgit-toggle-patch-names] Toggle showing patch names
fdf5e327
GH
1293\\[stgit-toggle-worktree] Toggle showing index and work tree
1294\\[stgit-toggle-unknown] Toggle showing unknown files
1295\\[stgit-toggle-ignored] Toggle showing ignored files
bf7e391c 1296\\[stgit-toggle-committed] Toggle showing recent commits
fdf5e327
GH
1297
1298Commands for diffs:
1299\\[stgit-diff] Show diff of patch or file
e02b46e5 1300\\[stgit-diff-range] Show diff for range of patches
fdf5e327
GH
1301\\[stgit-diff-base] Show diff against the merge base
1302\\[stgit-diff-ours] Show diff against our branch
1303\\[stgit-diff-theirs] Show diff against their branch
1304
1305 With one prefix argument (e.g., \\[universal-argument] \\[stgit-diff]), \
1306ignore space changes.
1307 With two prefix arguments (e.g., \\[universal-argument] \
1308\\[universal-argument] \\[stgit-diff]), ignore all space changes.
1309
1310Commands for merge conflicts:
1311\\[stgit-find-file-merge] Resolve conflicts using `smerge-ediff'
1312\\[stgit-resolve-file] Mark unmerged file as resolved
1313
1314Commands for branches:
3e528fb0 1315\\[stgit-branch] Switch to or create another branch
380a021f 1316\\[stgit-rebase] Rebase the current branch
fdf5e327
GH
1317
1318Customization variables:
1319`stgit-abbreviate-copies-and-renames'
bc11fb08 1320`stgit-default-show-ignored'
a0045b87 1321`stgit-default-show-patch-names'
bc11fb08 1322`stgit-default-show-unknown'
fdf5e327 1323`stgit-default-show-worktree'
bf7e391c
GH
1324`stgit-default-show-committed'
1325`stgit-default-committed-count'
fdf5e327
GH
1326`stgit-find-copies-harder'
1327`stgit-show-worktree-mode'
1328
1329See also \\[customize-group] for the \"stgit\" group."
56d81fe5
DK
1330 (kill-all-local-variables)
1331 (buffer-disable-undo)
1332 (setq mode-name "StGit"
1333 major-mode 'stgit-mode
1334 goal-column 2)
1335 (use-local-map stgit-mode-map)
a0689e11
GH
1336 (mapc (lambda (x) (set (make-local-variable (car x)) (cdr x)))
1337 `((list-buffers-directory . ,default-directory)
1338 (parse-sexp-lookup-properties . t)
1339 (stgit-expanded-patches . (:work :index))
1340 (stgit-index-node . nil)
1341 (stgit-worktree-node . nil)
1342 (stgit-marked-patches . nil)
bf7e391c
GH
1343 (stgit-committed-count . ,stgit-default-committed-count)
1344 (stgit-show-committed . ,stgit-default-show-committed)
a0689e11
GH
1345 (stgit-show-ignored . ,stgit-default-show-ignored)
1346 (stgit-show-patch-names . ,stgit-default-show-patch-names)
1347 (stgit-show-unknown . ,stgit-default-show-unknown)
1348 (stgit-show-worktree . ,stgit-default-show-worktree)))
2870f8b8 1349 (set-variable 'truncate-lines 't)
e44674e3
GH
1350 (add-hook 'after-save-hook 'stgit-update-stgit-for-buffer)
1351 (unless stgit-did-advise
1352 (stgit-advise)
1353 (setq stgit-did-advise t))
56d81fe5
DK
1354 (run-hooks 'stgit-mode-hook))
1355
e44674e3
GH
1356(defun stgit-advise-funlist (funlist)
1357 "Add advice to the functions in FUNLIST so we can refresh the
1358stgit buffers as the git status of files change."
1359 (mapc (lambda (sym)
1360 (when (fboundp sym)
1361 (eval `(defadvice ,sym (after stgit-update-stgit-for-buffer)
1362 (stgit-update-stgit-for-buffer t)))
1363 (ad-activate sym)))
1364 funlist))
1365
1366(defun stgit-advise ()
1367 "Add advice to appropriate (non-stgit) git functions so we can
1368refresh the stgit buffers as the git status of files change."
1369 (mapc (lambda (arg)
1370 (let ((feature (car arg))
1371 (funlist (cdr arg)))
1372 (if (featurep feature)
1373 (stgit-advise-funlist funlist)
1374 (add-to-list 'after-load-alist
1375 `(,feature (stgit-advise-funlist
1376 (quote ,funlist)))))))
e9a9f3fa 1377 ;; lists of (<feature> <function> <function> ...) to be advised
e44674e3 1378 '((vc-git vc-git-rename-file vc-git-revert vc-git-register)
e9a9f3fa
GH
1379 (git git-add-file git-checkout git-revert-file git-remove-file)
1380 (dired dired-delete-file))))
e44674e3 1381
455c9f7e
GH
1382(defvar stgit-pending-refresh-buffers nil
1383 "Alist of (cons `buffer' `refresh-index') of buffers that need
1384to be refreshed. `refresh-index' is non-nil if both work tree
1385and index need to be refreshed.")
1386
1387(defun stgit-run-pending-refreshs ()
1388 "Run all pending stgit buffer updates as posted by `stgit-post-refresh'."
1389 (let ((buffers stgit-pending-refresh-buffers)
1390 (stgit-inhibit-messages t))
1391 (setq stgit-pending-refresh-buffers nil)
1392 (while buffers
1393 (let* ((elem (car buffers))
1394 (buffer (car elem))
1395 (refresh-index (cdr elem)))
1396 (when (buffer-name buffer)
1397 (with-current-buffer buffer
1398 (stgit-refresh-worktree)
1399 (when refresh-index (stgit-refresh-index)))))
1400 (setq buffers (cdr buffers)))))
1401
1402(defun stgit-post-refresh (buffer refresh-index)
1403 "Update worktree status in BUFFER when Emacs becomes idle. If
1404REFRESH-INDEX is non-nil, also update the index."
1405 (unless stgit-pending-refresh-buffers
1406 (run-with-idle-timer 0.1 nil 'stgit-run-pending-refreshs))
1407 (let ((elem (assq buffer stgit-pending-refresh-buffers)))
1408 (if elem
1409 ;; if buffer is already present, set its refresh-index flag if
1410 ;; necessary
1411 (when refresh-index
1412 (setcdr elem t))
1413 ;; new entry
1414 (setq stgit-pending-refresh-buffers
1415 (cons (cons buffer refresh-index)
1416 stgit-pending-refresh-buffers)))))
1417
e44674e3 1418(defun stgit-update-stgit-for-buffer (&optional refresh-index)
455c9f7e
GH
1419 "When Emacs becomes idle, refresh worktree status in any
1420`stgit-mode' buffer that shows the status of the current buffer.
e44674e3 1421
455c9f7e 1422If REFRESH-INDEX is non-nil, also update the index."
e9a9f3fa 1423 (let* ((dir (cond ((derived-mode-p 'stgit-status-mode 'dired-mode)
e44674e3
GH
1424 default-directory)
1425 (buffer-file-name
1426 (file-name-directory
1427 (expand-file-name buffer-file-name)))))
1428 (gitdir (and dir (condition-case nil (git-get-top-dir dir)
1429 (error nil))))
b894e680
DK
1430 (buffer (and gitdir (stgit-find-buffer gitdir))))
1431 (when buffer
455c9f7e 1432 (stgit-post-refresh buffer refresh-index))))
b894e680 1433
d51722b7
GH
1434(defun stgit-add-mark (patchsym)
1435 "Mark the patch PATCHSYM."
8036afdd 1436 (setq stgit-marked-patches (cons patchsym stgit-marked-patches)))
6df83d42 1437
d51722b7
GH
1438(defun stgit-remove-mark (patchsym)
1439 "Unmark the patch PATCHSYM."
8036afdd 1440 (setq stgit-marked-patches (delq patchsym stgit-marked-patches)))
6df83d42 1441
e6b1fdae 1442(defun stgit-clear-marks ()
47271f41 1443 "Unmark all patches."
e6b1fdae
DK
1444 (setq stgit-marked-patches '()))
1445
735cb7ec 1446(defun stgit-patch-at-point (&optional cause-error)
2c862b07
DK
1447 (get-text-property (point) 'patch-data))
1448
bf7e391c 1449(defun stgit-patch-name-at-point (&optional cause-error types)
d51722b7 1450 "Return the patch name on the current line as a symbol.
64ada6f5 1451If CAUSE-ERROR is not nil, signal an error if none found.
bf7e391c
GH
1452
1453TYPES controls which types of commits and patches can be returned.
1454If it is t, only allow stgit patches; if 'allow-committed, also
1455allow historical commits; if nil, also allow work tree and index."
2c862b07 1456 (let ((patch (stgit-patch-at-point)))
64ada6f5 1457 (and patch
bf7e391c
GH
1458 (memq (stgit-patch->status patch)
1459 (case types
1460 ((nil) nil)
1461 ((allow-committed) '(work index))
1462 ((t) '(work index committed))
1463 (t (error "Bad value"))))
64ada6f5 1464 (setq patch nil))
2c862b07 1465 (cond (patch
413f9909 1466 (stgit-patch->name patch))
2c862b07
DK
1467 (cause-error
1468 (error "No patch on this line")))))
378a003d 1469
3164eec6
DK
1470(defun stgit-patched-file-at-point ()
1471 (get-text-property (point) 'file-data))
56d81fe5 1472
bf7e391c 1473(defun stgit-patches-marked-or-at-point (&optional cause-error types)
beac0f14
GH
1474 "Return the symbols of the marked patches, or the patch on the current line.
1475If CAUSE-ERRROR is not nil, signal an error if none found.
bf7e391c
GH
1476
1477TYPES controls which types of commits and patches can be returned.
1478If it is t, only allow stgit patches; if 'allow-committed, also
1479allow historical commits; if nil, also allow work tree and index."
7755d7f1 1480 (if stgit-marked-patches
d51722b7 1481 stgit-marked-patches
bf7e391c 1482 (let ((patch (stgit-patch-name-at-point nil types)))
beac0f14
GH
1483 (cond (patch (list patch))
1484 (cause-error (error "No patches marked or at this line"))
1485 (t nil)))))
7755d7f1 1486
a9089e68 1487(defun stgit-goto-patch (patchsym &optional file)
2570f6e5
GH
1488 "Move point to the line containing patch PATCHSYM and return non-nil.
1489If that patch cannot be found, do nothing and return nil.
a9089e68
GH
1490
1491If the patch was found and FILE is not nil, instead move to that
1492file's line. If FILE cannot be found, stay on the line of
1493PATCHSYM."
f9b82d36 1494 (let ((node (ewoc-nth stgit-ewoc 0)))
413f9909 1495 (while (and node (not (eq (stgit-patch->name (ewoc-data node))
f9b82d36
DK
1496 patchsym)))
1497 (setq node (ewoc-next stgit-ewoc node)))
a9089e68 1498 (when (and node file)
413f9909 1499 (let* ((file-ewoc (stgit-patch->files-ewoc (ewoc-data node)))
a9089e68 1500 (file-node (ewoc-nth file-ewoc 0)))
ea696de9
GH
1501 (while (and file-node
1502 (not (equal (stgit-file->file (ewoc-data file-node))
1503 file)))
a9089e68
GH
1504 (setq file-node (ewoc-next file-ewoc file-node)))
1505 (when file-node
1506 (ewoc-goto-node file-ewoc file-node)
1507 (move-to-column (stgit-goal-column))
1508 (setq node nil))))
f9b82d36
DK
1509 (when node
1510 (ewoc-goto-node stgit-ewoc node)
d51722b7 1511 (move-to-column goal-column))))
56d81fe5 1512
1c2426dc 1513(defun stgit-init ()
a53347d9 1514 "Run stg init."
1c2426dc 1515 (interactive)
9d04c657 1516 (stgit-assert-mode)
1c2426dc 1517 (stgit-capture-output nil
b0424080 1518 (stgit-run "init"))
1f0bf00f 1519 (stgit-reload))
1c2426dc 1520
d11e0621
GH
1521(defun stgit-toggle-mark ()
1522 "Toggle mark on the patch under point."
1523 (interactive)
1524 (stgit-assert-mode)
1525 (if (memq (stgit-patch-name-at-point t t) stgit-marked-patches)
1526 (stgit-unmark)
1527 (stgit-mark)))
1528
6df83d42 1529(defun stgit-mark ()
a53347d9 1530 "Mark the patch under point."
6df83d42 1531 (interactive)
9d04c657 1532 (stgit-assert-mode)
8036afdd 1533 (let* ((node (ewoc-locate stgit-ewoc))
bf7e391c
GH
1534 (patch (ewoc-data node)))
1535 (case (stgit-patch->status patch)
1536 (work (error "Cannot mark the work tree"))
1537 (index (error "Cannot mark the index"))
1538 (committed (error "Cannot mark a committed patch")))
413f9909 1539 (stgit-add-mark (stgit-patch->name patch))
d11e0621
GH
1540 (let ((column (current-column)))
1541 (ewoc-invalidate stgit-ewoc node)
1542 (move-to-column column))))
1543
1544(defun stgit-mark-down ()
1545 "Mark the patch under point and move to the next patch."
1546 (interactive)
1547 (stgit-mark)
378a003d 1548 (stgit-next-patch))
6df83d42 1549
d11e0621
GH
1550(defun stgit-unmark ()
1551 "Remove mark from the patch on the current line."
6df83d42 1552 (interactive)
9d04c657 1553 (stgit-assert-mode)
8036afdd
DK
1554 (let* ((node (ewoc-locate stgit-ewoc))
1555 (patch (ewoc-data node)))
413f9909 1556 (stgit-remove-mark (stgit-patch->name patch))
d11e0621
GH
1557 (let ((column (current-column)))
1558 (ewoc-invalidate stgit-ewoc node)
1559 (move-to-column column))))
1560
1561(defun stgit-unmark-up ()
1562 "Remove mark from the patch on the previous line."
1563 (interactive)
1564 (stgit-assert-mode)
1565 (stgit-previous-patch)
1566 (stgit-unmark))
9b151b27
GH
1567
1568(defun stgit-unmark-down ()
a53347d9 1569 "Remove mark from the patch on the current line."
9b151b27 1570 (interactive)
9d04c657 1571 (stgit-assert-mode)
d11e0621 1572 (stgit-unmark)
1288eda2 1573 (stgit-next-patch))
6df83d42 1574
56d81fe5 1575(defun stgit-rename (name)
018fa1ac 1576 "Rename the patch under point to NAME."
64ada6f5
GH
1577 (interactive (list
1578 (read-string "Patch name: "
1579 (symbol-name (stgit-patch-name-at-point t t)))))
9d04c657 1580 (stgit-assert-mode)
64ada6f5 1581 (let ((old-patchsym (stgit-patch-name-at-point t t)))
56d81fe5 1582 (stgit-capture-output nil
69db9714 1583 (stgit-run "rename" "--" old-patchsym name))
d51722b7
GH
1584 (let ((name-sym (intern name)))
1585 (when (memq old-patchsym stgit-expanded-patches)
378a003d 1586 (setq stgit-expanded-patches
6a73154a 1587 (cons name-sym (delq old-patchsym stgit-expanded-patches))))
d51722b7 1588 (when (memq old-patchsym stgit-marked-patches)
378a003d 1589 (setq stgit-marked-patches
6a73154a 1590 (cons name-sym (delq old-patchsym stgit-marked-patches))))
d51722b7
GH
1591 (stgit-reload)
1592 (stgit-goto-patch name-sym))))
56d81fe5 1593
408fa7cb
GH
1594(defun stgit-reload-or-repair (repair)
1595 "Update the contents of the StGit buffer (`stgit-reload').
1596
1597With a prefix argument, repair the StGit metadata if the branch
1598was modified with git commands (`stgit-repair')."
1599 (interactive "P")
9d04c657 1600 (stgit-assert-mode)
408fa7cb
GH
1601 (if repair
1602 (stgit-repair)
1603 (stgit-reload)))
1604
26201d96 1605(defun stgit-repair ()
a53347d9 1606 "Run stg repair."
26201d96 1607 (interactive)
9d04c657 1608 (stgit-assert-mode)
26201d96 1609 (stgit-capture-output nil
b0424080 1610 (stgit-run "repair"))
1f0bf00f 1611 (stgit-reload))
26201d96 1612
3e528fb0
GH
1613(defun stgit-available-branches (&optional all)
1614 "Returns a list of the names of the available stg branches as strings.
1615
1616If ALL is not nil, also return non-stgit branches."
adeef6bc
GH
1617 (let ((output (with-output-to-string
1618 (stgit-run "branch" "--list")))
3e528fb0
GH
1619 (pattern (format "^>?\\s-+%c\\s-+\\(\\S-+\\)"
1620 (if all ?. ?s)))
adeef6bc
GH
1621 (start 0)
1622 result)
3e528fb0 1623 (while (string-match pattern output start)
adeef6bc
GH
1624 (setq result (cons (match-string 1 output) result))
1625 (setq start (match-end 0)))
1626 result))
1627
1628(defun stgit-branch (branch)
3e528fb0 1629 "Switch to or create branch BRANCH."
adeef6bc
GH
1630 (interactive (list (completing-read "Switch to branch: "
1631 (stgit-available-branches))))
9d04c657 1632 (stgit-assert-mode)
3e528fb0
GH
1633 (when (cond ((equal branch (stgit-current-branch))
1634 (error "Branch is already current"))
1635 ((member branch (stgit-available-branches t))
1636 (stgit-capture-output nil (stgit-run "branch" "--" branch))
1637 t)
1638 ((not (string-match stgit-allowed-branch-name-re branch))
1639 (error "Invalid branch name"))
1640 ((yes-or-no-p (format "Create branch \"%s\"? " branch))
84e1850a
GH
1641 (let ((branch-point (completing-read
1642 "Branch from (default current branch): "
1643 (stgit-available-branches))))
1644 (stgit-capture-output nil
1645 (apply 'stgit-run
1646 `("branch" "--create" "--"
1647 ,branch
1648 ,@(unless (zerop (length branch-point))
1649 (list branch-point)))))
1650 t)))
3e528fb0 1651 (stgit-reload)))
adeef6bc 1652
380a021f
GH
1653(defun stgit-available-refs (&optional omit-stgit)
1654 "Returns a list of the available git refs.
1655If OMIT-STGIT is not nil, filter out \"resf/heads/*.stgit\"."
1656 (let* ((output (with-output-to-string
1657 (stgit-run-git-silent "for-each-ref" "--format=%(refname)"
1658 "refs/tags" "refs/heads"
1659 "refs/remotes")))
1660 (result (split-string output "\n" t)))
1661 (mapcar (lambda (s)
1662 (if (string-match "^refs/\\(heads\\|tags\\|remotes\\)/" s)
1663 (substring s (match-end 0))
1664 s))
1665 (if omit-stgit
1666 (delete-if (lambda (s)
1667 (string-match "^refs/heads/.*\\.stgit$" s))
1668 result)
1669 result))))
1670
d6e17ce0
GH
1671(defun stgit-parent-branch ()
1672 "Return the parent branch of the current stg branch as per
1673git-config setting branch.<branch>.stgit.parentbranch."
1674 (let ((output (with-output-to-string
1675 (stgit-run-git-silent "config"
1676 (format "branch.%s.stgit.parentbranch"
1677 (stgit-current-branch))))))
1678 (when (string-match ".*" output)
1679 (match-string 0 output))))
1680
380a021f 1681(defun stgit-rebase (new-base)
d6e17ce0
GH
1682 "Rebase the current branch to NEW-BASE.
1683
1684Interactively, first ask which branch to rebase to. Defaults to
1685what git-config branch.<branch>.stgit.parentbranch is set to."
380a021f 1686 (interactive (list (completing-read "Rebase to: "
d6e17ce0
GH
1687 (stgit-available-refs t)
1688 nil nil
1689 (stgit-parent-branch))))
9d04c657 1690 (stgit-assert-mode)
69db9714 1691 (stgit-capture-output nil (stgit-run "rebase" "--" new-base))
380a021f
GH
1692 (stgit-reload))
1693
41c1c59c
GH
1694(defun stgit-commit (count)
1695 "Run stg commit on COUNT commits.
e552cb5f
GH
1696Interactively, the prefix argument is used as COUNT.
1697A negative COUNT will uncommit instead."
41c1c59c 1698 (interactive "p")
9d04c657 1699 (stgit-assert-mode)
e552cb5f
GH
1700 (if (< count 0)
1701 (stgit-uncommit (- count))
1702 (stgit-capture-output nil (stgit-run "commit" "-n" count))
1703 (stgit-reload)))
1704
1705(defun stgit-uncommit (count)
1706 "Run stg uncommit on COUNT commits.
1707Interactively, the prefix argument is used as COUNT.
1708A negative COUNT will commit instead."
1709 (interactive "p")
9d04c657 1710 (stgit-assert-mode)
e552cb5f
GH
1711 (if (< count 0)
1712 (stgit-commit (- count))
1713 (stgit-capture-output nil (stgit-run "uncommit" "-n" count))
1714 (stgit-reload)))
c4aad9a7 1715
556345d3
GH
1716(defun stgit-neighbour-file ()
1717 "Return the file name of the next file after point, or the
1718previous file if point is at the last file within a patch."
1719 (let ((old-point (point))
1720 neighbour-file)
1721 (and (zerop (forward-line 1))
1722 (let ((f (stgit-patched-file-at-point)))
413f9909 1723 (and f (setq neighbour-file (stgit-file->file f)))))
556345d3
GH
1724 (goto-char old-point)
1725 (unless neighbour-file
1726 (and (zerop (forward-line -1))
1727 (let ((f (stgit-patched-file-at-point)))
413f9909 1728 (and f (setq neighbour-file (stgit-file->file f)))))
556345d3
GH
1729 (goto-char old-point))
1730 neighbour-file))
1731
3959a095
GH
1732(defun stgit-revert-file ()
1733 "Revert the file at point, which must be in the index or the
1734working tree."
1735 (interactive)
9d04c657 1736 (stgit-assert-mode)
3959a095
GH
1737 (let* ((patched-file (or (stgit-patched-file-at-point)
1738 (error "No file on the current line")))
1739 (patch-name (stgit-patch-name-at-point))
413f9909
GH
1740 (file-status (stgit-file->status patched-file))
1741 (rm-file (cond ((stgit-file->copy-or-rename patched-file)
1742 (stgit-file->cr-to patched-file))
3959a095 1743 ((eq file-status 'add)
413f9909 1744 (stgit-file->file patched-file))))
3959a095 1745 (co-file (cond ((eq file-status 'rename)
413f9909 1746 (stgit-file->cr-from patched-file))
3959a095 1747 ((not (memq file-status '(copy add)))
413f9909 1748 (stgit-file->file patched-file))))
556345d3 1749 (next-file (stgit-neighbour-file)))
3959a095
GH
1750
1751 (unless (memq patch-name '(:work :index))
1752 (error "No index or working tree file on this line"))
1753
d9473917
GH
1754 (when (eq file-status 'ignore)
1755 (error "Cannot revert ignored files"))
1756
1757 (when (eq file-status 'unknown)
1758 (error "Cannot revert unknown files"))
1759
3959a095
GH
1760 (let ((nfiles (+ (if rm-file 1 0) (if co-file 1 0))))
1761 (when (yes-or-no-p (format "Revert %d file%s? "
1762 nfiles
1763 (if (= nfiles 1) "" "s")))
1764 (stgit-capture-output nil
1765 (when rm-file
1766 (stgit-run-git "rm" "-f" "-q" "--" rm-file))
1767 (when co-file
1768 (stgit-run-git "checkout" "HEAD" co-file)))
556345d3
GH
1769 (stgit-reload)
1770 (stgit-goto-patch patch-name next-file)))))
1629f59f
GH
1771
1772(defun stgit-revert ()
1773 "Revert the change at point, which must be the index, the work
1774tree, or a single change in either."
1775 (interactive)
9d04c657 1776 (stgit-assert-mode)
1629f59f
GH
1777 (let ((patched-file (stgit-patched-file-at-point)))
1778 (if patched-file
1779 (stgit-revert-file)
1780 (let* ((patch-name (or (stgit-patch-name-at-point)
1781 (error "No patch or file at point")))
1782 (patch-desc (case patch-name
1783 (:index "index")
1784 (:work "work tree")
1785 (t (error (substitute-command-keys
1786 "Use \\[stgit-delete] to delete a patch"))))))
1787 (when (if (eq patch-name :work)
1788 (stgit-work-tree-empty-p)
1789 (stgit-index-empty-p))
1790 (error (format "There are no changes in the %s to revert"
1791 patch-desc)))
1792 (and (eq patch-name :index)
1793 (not (stgit-work-tree-empty-p))
1794 (error "Cannot revert index as work tree contains unstaged changes"))
1795
1796 (when (yes-or-no-p (format "Revert all changes in the %s? "
1797 patch-desc))
1798 (if (eq patch-name :index)
1799 (stgit-run-git-silent "reset" "--hard" "-q")
1800 (stgit-run-git-silent "checkout" "--" "."))
1801 (stgit-refresh-index)
1802 (stgit-refresh-worktree)
1803 (stgit-goto-patch patch-name))))))
3959a095 1804
51783171
GH
1805(defun stgit-resolve-file ()
1806 "Resolve conflict in the file at point."
1807 (interactive)
9d04c657 1808 (stgit-assert-mode)
51783171
GH
1809 (let* ((patched-file (stgit-patched-file-at-point))
1810 (patch (stgit-patch-at-point))
413f9909
GH
1811 (patch-name (and patch (stgit-patch->name patch)))
1812 (status (and patched-file (stgit-file->status patched-file))))
51783171
GH
1813
1814 (unless (memq patch-name '(:work :index))
1815 (error "No index or working tree file on this line"))
1816
1817 (unless (eq status 'unmerged)
1818 (error "No conflict to resolve at the current line"))
1819
1820 (stgit-capture-output nil
413f9909 1821 (stgit-move-change-to-index (stgit-file->file patched-file)))
51783171
GH
1822
1823 (stgit-reload)))
1824
d47ee133
GH
1825(defun stgit-push-or-pop-patches (do-push npatches)
1826 "Push (if DO-PUSH is not nil) or pop (if DO-PUSH is nil)
1827NPATCHES patches, or all patches if NPATCHES is t."
1828 (stgit-assert-mode)
1829 (stgit-capture-output nil
1830 (apply 'stgit-run
1831 (if do-push "push" "pop")
1832 (if (eq npatches t)
1833 '("--all")
1834 (list "-n" npatches))))
1835 (stgit-reload)
1836 (stgit-refresh-git-status))
1837
0b661144
DK
1838(defun stgit-push-next (npatches)
1839 "Push the first unapplied patch.
1840With numeric prefix argument, push that many patches."
1841 (interactive "p")
d47ee133 1842 (stgit-push-or-pop-patches t npatches))
56d81fe5 1843
0b661144
DK
1844(defun stgit-pop-next (npatches)
1845 "Pop the topmost applied patch.
d47ee133
GH
1846With numeric prefix argument, pop that many patches.
1847
1848If NPATCHES is t, pop all patches."
0b661144 1849 (interactive "p")
d47ee133 1850 (stgit-push-or-pop-patches nil npatches))
56d81fe5 1851
7c11b754
GH
1852(defun stgit-applied-patches (&optional only-patches)
1853 "Return a list of the applied patches.
1854
1855If ONLY-PATCHES is not nil, exclude index and work tree."
1856 (let ((states (if only-patches
1857 '(applied top)
1858 '(applied top index work)))
1859 result)
9aa61946
GH
1860 (ewoc-map (lambda (patch)
1861 (when (memq (stgit-patch->status patch) states)
1862 (setq result (cons patch result)))
1863 nil)
7c11b754
GH
1864 stgit-ewoc)
1865 result))
1866
1867(defun stgit-applied-patchsyms (&optional only-patches)
1868 "Return a list of the symbols of the applied patches.
1869
1870If ONLY-PATCHES is not nil, exclude index and work tree."
413f9909 1871 (mapcar #'stgit-patch->name (stgit-applied-patches only-patches)))
f9182fca
KH
1872
1873(defun stgit-push-or-pop ()
7c11b754 1874 "Push or pop the marked patches."
f9182fca 1875 (interactive)
9d04c657 1876 (stgit-assert-mode)
7c11b754
GH
1877 (let* ((patchsyms (stgit-patches-marked-or-at-point t t))
1878 (applied-syms (stgit-applied-patchsyms t))
1879 (unapplied (set-difference patchsyms applied-syms)))
f9182fca 1880 (stgit-capture-output nil
7c11b754
GH
1881 (apply 'stgit-run
1882 (if unapplied "push" "pop")
1883 "--"
1884 (stgit-sort-patches (if unapplied unapplied patchsyms)))))
1885 (stgit-reload))
f9182fca 1886
d47ee133 1887(defun stgit-goto-target ()
bf7e391c 1888 "Return the goto target at point: a patchsym, :top,
d47ee133 1889or :bottom."
bf7e391c
GH
1890 (let ((patch (stgit-patch-at-point)))
1891 (cond (patch
1892 (case (stgit-patch->status patch)
1893 ((work index) nil)
1894 ((committed) :bottom)
1895 (t (stgit-patch->name patch))))
d47ee133
GH
1896 ((not (next-single-property-change (point) 'patch-data))
1897 :top)
1898 ((not (previous-single-property-change (point) 'patch-data))
1899 :bottom))))
1900
c7adf5ef 1901(defun stgit-goto ()
48d0a850
GH
1902 "Go to the patch on the current line.
1903
d47ee133
GH
1904Push or pop patches to make this patch topmost. Push or pop all
1905patches if used on a line after or before all patches."
c7adf5ef 1906 (interactive)
9d04c657 1907 (stgit-assert-mode)
d47ee133
GH
1908 (let ((patchsym (stgit-goto-target)))
1909 (unless patchsym
1910 (error "No patch to go to on this line"))
1911 (case patchsym
1912 (:top (stgit-push-or-pop-patches t t))
1913 (:bottom (stgit-push-or-pop-patches nil t))
1914 (t (stgit-capture-output nil
69db9714 1915 (stgit-run "goto" "--" patchsym))
d47ee133 1916 (stgit-reload)))))
c7adf5ef 1917
d51722b7 1918(defun stgit-id (patchsym)
50d88c67
DK
1919 "Return the git commit id for PATCHSYM.
1920If PATCHSYM is a keyword, returns PATCHSYM unmodified."
1921 (if (keywordp patchsym)
1922 patchsym
1923 (let ((result (with-output-to-string
69db9714 1924 (stgit-run-silent "id" "--" patchsym))))
50d88c67
DK
1925 (unless (string-match "^\\([0-9A-Fa-f]\\{40\\}\\)$" result)
1926 (error "Cannot find commit id for %s" patchsym))
1927 (match-string 1 result))))
378a003d 1928
015a6dfa
KW
1929(defun stgit-whitespace-diff-arg (arg)
1930 (when (numberp arg)
1931 (cond ((> arg 4) "--ignore-all-space")
1932 ((> arg 1) "--ignore-space-change"))))
1933
1aece5c0 1934(defun stgit-show-patch (unmerged-stage ignore-whitespace)
d9b954c7
GH
1935 "Show the patch on the current line.
1936
1937UNMERGED-STAGE is the argument to `git-diff' that that selects
1938which stage to diff against in the case of unmerged files."
015a6dfa 1939 (let ((space-arg (stgit-whitespace-diff-arg ignore-whitespace))
1aece5c0
GH
1940 (patch-name (stgit-patch-name-at-point t)))
1941 (stgit-capture-output "*StGit patch*"
1942 (case (get-text-property (point) 'entry-type)
1943 ('file
1944 (let* ((patched-file (stgit-patched-file-at-point))
1945 (patch-id (let ((id (stgit-id patch-name)))
1946 (if (and (eq id :index)
413f9909 1947 (eq (stgit-file->status patched-file)
1aece5c0
GH
1948 'unmerged))
1949 :work
1950 id)))
1951 (args (append (and space-arg (list space-arg))
413f9909 1952 (and (stgit-file->cr-from patched-file)
1aece5c0
GH
1953 (list (stgit-find-copies-harder-diff-arg)))
1954 (cond ((eq patch-id :index)
1955 '("--cached"))
1956 ((eq patch-id :work)
1957 (list unmerged-stage))
1958 (t
1959 (list (concat patch-id "^") patch-id)))
1960 '("--")
413f9909
GH
1961 (if (stgit-file->copy-or-rename patched-file)
1962 (list (stgit-file->cr-from patched-file)
1963 (stgit-file->cr-to patched-file))
1964 (list (stgit-file->file patched-file))))))
1aece5c0
GH
1965 (apply 'stgit-run-git "diff" args)))
1966 ('patch
1967 (let* ((patch-id (stgit-id patch-name)))
1968 (if (or (eq patch-id :index) (eq patch-id :work))
1969 (apply 'stgit-run-git "diff"
1970 (stgit-find-copies-harder-diff-arg)
1971 (append (and space-arg (list space-arg))
1972 (if (eq patch-id :index)
1973 '("--cached")
1974 (list unmerged-stage))))
1975 (let ((args (append '("show" "-O" "--patch-with-stat" "-O" "-M")
1976 (and space-arg (list "-O" space-arg))
69db9714 1977 '("--")
1aece5c0
GH
1978 (list (stgit-patch-name-at-point)))))
1979 (apply 'stgit-run args)))))
6a73154a
GH
1980 (t
1981 (error "No patch or file at point")))
1aece5c0
GH
1982 (with-current-buffer standard-output
1983 (goto-char (point-min))
1984 (diff-mode)))))
1985
1986(defmacro stgit-define-diff (name diff-arg &optional unmerged-action)
1987 `(defun ,name (&optional ignore-whitespace)
1988 ,(format "Show the patch on the current line.
1989
1990%sWith a prefix argument, ignore whitespace. With a prefix argument
1991greater than four (e.g., \\[universal-argument] \
1992\\[universal-argument] \\[%s]), ignore all whitespace."
1993 (if unmerged-action
1994 (format "For unmerged files, %s.\n\n" unmerged-action)
1995 "")
1996 name)
1997 (interactive "p")
9d04c657 1998 (stgit-assert-mode)
1aece5c0
GH
1999 (stgit-show-patch ,diff-arg ignore-whitespace)))
2000
2001(stgit-define-diff stgit-diff
2002 "--ours" nil)
2003(stgit-define-diff stgit-diff-ours
2004 "--ours"
2005 "diff against our branch")
2006(stgit-define-diff stgit-diff-theirs
2007 "--theirs"
2008 "diff against their branch")
2009(stgit-define-diff stgit-diff-base
2010 "--base"
2011 "diff against the merge base")
2012(stgit-define-diff stgit-diff-combined
2013 "--cc"
2014 "show a combined diff")
d9b954c7 2015
e02b46e5
KW
2016(defun stgit-diff-range (&optional ignore-whitespace)
2017 "Show diff for the range of patches between point and the marked patch.
2018
2019With a prefix argument, ignore whitespace. With a prefix argument
2020greater than four (e.g., \\[universal-argument] \
2021\\[universal-argument] \\[stgit-diff-range]), ignore all whitespace."
2022 (interactive "p")
2023 (stgit-assert-mode)
2024 (unless (= (length stgit-marked-patches) 1)
2025 (error "Need exactly one patch marked"))
bf7e391c
GH
2026 (let* ((patches (stgit-sort-patches
2027 (cons (stgit-patch-name-at-point t 'allow-committed)
2028 stgit-marked-patches)
2029 t))
e02b46e5
KW
2030 (first-patch (car patches))
2031 (second-patch (if (cdr patches) (cadr patches) first-patch))
2032 (whitespace-arg (stgit-whitespace-diff-arg ignore-whitespace))
2033 (applied (stgit-applied-patchsyms t)))
2034 (unless (and (memq first-patch applied) (memq second-patch applied))
2035 (error "Can only show diff range for applied patches"))
2036 (stgit-capture-output (format "*StGit diff %s..%s*"
2037 first-patch second-patch)
2038 (apply 'stgit-run-git (append '("diff" "--patch-with-stat")
2039 (and whitespace-arg (list whitespace-arg))
2040 (list (format "%s^" (stgit-id first-patch))
2041 (stgit-id second-patch))))
2042 (with-current-buffer standard-output
2043 (goto-char (point-min))
2044 (diff-mode)))))
2045
f87c2e22
GH
2046(defun stgit-move-change-to-index (file &optional force)
2047 "Copies the work tree state of FILE to index, using git add or git rm.
2048
2049If FORCE is not nil, use --force."
306b37a6
GH
2050 (let ((op (if (or (file-exists-p file) (file-symlink-p file))
2051 '("add") '("rm" "-q"))))
37cb5766 2052 (stgit-capture-output "*git output*"
f87c2e22
GH
2053 (apply 'stgit-run-git (append op (and force '("--force"))
2054 '("--") (list file))))))
37cb5766 2055
fd9fe574 2056(defun stgit-remove-change-from-index (file)
37cb5766
DK
2057 "Unstages the change in FILE from the index"
2058 (stgit-capture-output "*git output*"
2059 (stgit-run-git "reset" "-q" "--" file)))
2060
dde3ab4d
GH
2061(defun stgit-git-index-unmerged-p ()
2062 (let (result)
2063 (with-output-to-string
2064 (setq result (not (zerop (stgit-run-git-silent "diff-index" "--cached"
2065 "--diff-filter=U"
2066 "--quiet" "HEAD")))))
2067 result))
2068
37cb5766 2069(defun stgit-file-toggle-index ()
a9089e68
GH
2070 "Move modified file in or out of the index.
2071
2072Leaves the point where it is, but moves the mark to where the
2073file ended up. You can then jump to the file with \
2074\\[exchange-point-and-mark]."
37cb5766 2075 (interactive)
9d04c657 2076 (stgit-assert-mode)
612f999a
GH
2077 (let* ((patched-file (or (stgit-patched-file-at-point)
2078 (error "No file on the current line")))
413f9909 2079 (patched-status (stgit-file->status patched-file)))
612f999a 2080 (when (eq patched-status 'unmerged)
51783171 2081 (error (substitute-command-keys "Use \\[stgit-resolve-file] to move an unmerged file to the index")))
a9089e68 2082 (let* ((patch (stgit-patch-at-point))
413f9909 2083 (patch-name (stgit-patch->name patch))
612f999a 2084 (mark-file (if (eq patched-status 'rename)
413f9909
GH
2085 (stgit-file->cr-to patched-file)
2086 (stgit-file->file patched-file)))
612f999a 2087 (point-file (if (eq patched-status 'rename)
413f9909 2088 (stgit-file->cr-from patched-file)
6a73154a 2089 (stgit-neighbour-file))))
a9089e68 2090
37cb5766 2091 (cond ((eq patch-name :work)
413f9909 2092 (stgit-move-change-to-index (stgit-file->file patched-file)
f87c2e22 2093 (eq patched-status 'ignore)))
37cb5766 2094 ((eq patch-name :index)
413f9909 2095 (stgit-remove-change-from-index (stgit-file->file patched-file)))
37cb5766 2096 (t
612f999a 2097 (error "Can only move files between working tree and index")))
a9089e68
GH
2098 (stgit-refresh-worktree)
2099 (stgit-refresh-index)
612f999a 2100 (stgit-goto-patch (if (eq patch-name :index) :work :index) mark-file)
a9089e68 2101 (push-mark nil t t)
612f999a 2102 (stgit-goto-patch patch-name point-file))))
37cb5766 2103
dde3ab4d
GH
2104(defun stgit-toggle-index ()
2105 "Move change in or out of the index.
2106
2107Works on index and work tree, as well as files in either.
2108
2109Leaves the point where it is, but moves the mark to where the
2110file ended up. You can then jump to the file with \
2111\\[exchange-point-and-mark]."
2112 (interactive)
9d04c657 2113 (stgit-assert-mode)
dde3ab4d
GH
2114 (if (stgit-patched-file-at-point)
2115 (stgit-file-toggle-index)
2116 (let ((patch-name (stgit-patch-name-at-point)))
2117 (unless (memq patch-name '(:index :work))
2118 (error "Can only move changes between working tree and index"))
2119 (when (stgit-git-index-unmerged-p)
2120 (error "Resolve unmerged changes with \\[stgit-resolve-file] first"))
2121 (if (if (eq patch-name :index)
2122 (stgit-index-empty-p)
2123 (stgit-work-tree-empty-p))
2124 (message "No changes to be moved")
2125 (stgit-capture-output nil
2126 (if (eq patch-name :work)
2127 (stgit-run-git "add" "--update")
2128 (stgit-run-git "reset" "--mixed" "-q")))
2129 (stgit-refresh-worktree)
2130 (stgit-refresh-index))
2131 (stgit-goto-patch (if (eq patch-name :index) :work :index)))))
2132
0bca35c8 2133(defun stgit-edit ()
a53347d9 2134 "Edit the patch on the current line."
0bca35c8 2135 (interactive)
9d04c657 2136 (stgit-assert-mode)
64ada6f5 2137 (let ((patchsym (stgit-patch-name-at-point t t))
0780be79 2138 (edit-buf (get-buffer-create "*StGit edit*"))
0bca35c8
DK
2139 (dir default-directory))
2140 (log-edit 'stgit-confirm-edit t nil edit-buf)
d51722b7 2141 (set (make-local-variable 'stgit-edit-patchsym) patchsym)
0bca35c8
DK
2142 (setq default-directory dir)
2143 (let ((standard-output edit-buf))
655a3977 2144 (save-excursion
69db9714 2145 (stgit-run-silent "edit" "--save-template=-" "--" patchsym)))))
0bca35c8
DK
2146
2147(defun stgit-confirm-edit ()
2148 (interactive)
2149 (let ((file (make-temp-file "stgit-edit-")))
2150 (write-region (point-min) (point-max) file)
2151 (stgit-capture-output nil
69db9714 2152 (stgit-run "edit" "-f" file "--" stgit-edit-patchsym))
0bca35c8 2153 (with-current-buffer log-edit-parent-buffer
1f0bf00f 2154 (stgit-reload))))
0bca35c8 2155
2acb7116 2156(defun stgit-new (add-sign &optional refresh)
aa04f831
GH
2157 "Create a new patch.
2158With a prefix argument, include a \"Signed-off-by:\" line at the
2159end of the patch."
2160 (interactive "P")
9d04c657 2161 (stgit-assert-mode)
c5d45b92
GH
2162 (let ((edit-buf (get-buffer-create "*StGit edit*"))
2163 (dir default-directory))
2164 (log-edit 'stgit-confirm-new t nil edit-buf)
aa04f831 2165 (setq default-directory dir)
2acb7116 2166 (set (make-local-variable 'stgit-refresh-after-new) refresh)
aa04f831
GH
2167 (when add-sign
2168 (save-excursion
2169 (let ((standard-output (current-buffer)))
2170 (stgit-run-silent "new" "--sign" "--save-template=-"))))))
64c097a0
DK
2171
2172(defun stgit-confirm-new ()
2173 (interactive)
2acb7116
DK
2174 (let ((file (make-temp-file "stgit-edit-"))
2175 (refresh stgit-refresh-after-new))
64c097a0
DK
2176 (write-region (point-min) (point-max) file)
2177 (stgit-capture-output nil
27b0f9e4 2178 (stgit-run "new" "-f" file))
64c097a0 2179 (with-current-buffer log-edit-parent-buffer
2acb7116
DK
2180 (if refresh
2181 (stgit-refresh)
2182 (stgit-reload)))))
2183
2184(defun stgit-new-and-refresh (add-sign)
2185 "Create a new patch and refresh it with the current changes.
2186
2187With a prefix argument, include a \"Signed-off-by:\" line at the
2188end of the patch.
2189
2190This works just like running `stgit-new' followed by `stgit-refresh'."
2191 (interactive "P")
9d04c657 2192 (stgit-assert-mode)
2acb7116 2193 (stgit-new add-sign t))
64c097a0
DK
2194
2195(defun stgit-create-patch-name (description)
2196 "Create a patch name from a long description"
2197 (let ((patch ""))
2198 (while (> (length description) 0)
2199 (cond ((string-match "\\`[a-zA-Z_-]+" description)
8439f657
GH
2200 (setq patch (downcase (concat patch
2201 (match-string 0 description))))
64c097a0
DK
2202 (setq description (substring description (match-end 0))))
2203 ((string-match "\\` +" description)
2204 (setq patch (concat patch "-"))
2205 (setq description (substring description (match-end 0))))
2206 ((string-match "\\`[^a-zA-Z_-]+" description)
2207 (setq description (substring description (match-end 0))))))
2208 (cond ((= (length patch) 0)
2209 "patch")
2210 ((> (length patch) 20)
2211 (substring patch 0 20))
2212 (t patch))))
0bca35c8 2213
9008e45b 2214(defun stgit-delete (patchsyms &optional spill-p)
d51722b7 2215 "Delete the patches in PATCHSYMS.
9008e45b
GH
2216Interactively, delete the marked patches, or the patch at point.
2217
2218With a prefix argument, or SPILL-P, spill the patch contents to
2219the work tree and index."
beac0f14 2220 (interactive (list (stgit-patches-marked-or-at-point t t)
9008e45b 2221 current-prefix-arg))
9d04c657 2222 (stgit-assert-mode)
e7231e4f
GH
2223 (unless patchsyms
2224 (error "No patches to delete"))
64ada6f5
GH
2225 (when (memq :index patchsyms)
2226 (error "Cannot delete the index"))
2227 (when (memq :work patchsyms)
2228 (error "Cannot delete the work tree"))
2229
d51722b7 2230 (let ((npatches (length patchsyms)))
9008e45b 2231 (when (yes-or-no-p (format "Really delete %d patch%s%s? "
e7231e4f 2232 npatches
9008e45b
GH
2233 (if (= 1 npatches) "" "es")
2234 (if spill-p
2235 " (spilling contents to index)"
2236 "")))
69db9714
GH
2237 (let ((args (append (when spill-p '("--spill"))
2238 '("--")
2239 patchsyms)))
9008e45b
GH
2240 (stgit-capture-output nil
2241 (apply 'stgit-run "delete" args))
2242 (stgit-reload)))))
d51722b7 2243
7cc45294
GH
2244(defun stgit-move-patches-target ()
2245 "Return the patchsym indicating a target patch for
2246`stgit-move-patches'.
2247
2547179e
GH
2248This is either the first unmarked patch at or after point, or one
2249of :top and :bottom if the point is after or before the applied
2250patches."
2251
2252 (save-excursion
2253 (let (result)
2254 (while (not result)
2255 (let ((patchsym (stgit-patch-name-at-point)))
2256 (cond ((memq patchsym '(:work :index)) (setq result :top))
2257 (patchsym (if (memq patchsym stgit-marked-patches)
2258 (stgit-next-patch)
2259 (setq result patchsym)))
2260 ((re-search-backward "^>" nil t) (setq result :top))
2261 (t (setq result :bottom)))))
2262 result)))
7cc45294 2263
c1412832 2264(defun stgit-sort-patches (patchsyms &optional allow-duplicates)
95369f6c
GH
2265 "Returns the list of patches in PATCHSYMS sorted according to
2266their position in the patch series, bottommost first.
2267
c1412832
KW
2268PATCHSYMS must not contain duplicate entries, unless
2269ALLOW-DUPLICATES is not nil."
95369f6c
GH
2270 (let (sorted-patchsyms
2271 (series (with-output-to-string
2272 (with-current-buffer standard-output
2273 (stgit-run-silent "series" "--noprefix"))))
2274 start)
2275 (while (string-match "^\\(.+\\)" series start)
2276 (let ((patchsym (intern (match-string 1 series))))
2277 (when (memq patchsym patchsyms)
2278 (setq sorted-patchsyms (cons patchsym sorted-patchsyms))))
2279 (setq start (match-end 0)))
2280 (setq sorted-patchsyms (nreverse sorted-patchsyms))
2281
c1412832
KW
2282 (unless allow-duplicates
2283 (unless (= (length patchsyms) (length sorted-patchsyms))
2284 (error "Internal error")))
95369f6c
GH
2285
2286 sorted-patchsyms))
2287
7cc45294
GH
2288(defun stgit-move-patches (patchsyms target-patch)
2289 "Move the patches in PATCHSYMS to below TARGET-PATCH.
2290If TARGET-PATCH is :bottom or :top, move the patches to the
2291bottom or top of the stack, respectively.
2292
2293Interactively, move the marked patches to where the point is."
2294 (interactive (list stgit-marked-patches
2295 (stgit-move-patches-target)))
9d04c657 2296 (stgit-assert-mode)
7cc45294
GH
2297 (unless patchsyms
2298 (error "Need at least one patch to move"))
2299
2300 (unless target-patch
2301 (error "Point not at a patch"))
2302
2547179e
GH
2303 ;; need to have patchsyms sorted by position in the stack
2304 (let ((sorted-patchsyms (stgit-sort-patches patchsyms)))
2305 (stgit-capture-output nil
2306 (if (eq target-patch :top)
69db9714 2307 (apply 'stgit-run "float" "--" sorted-patchsyms)
2547179e
GH
2308 (apply 'stgit-run
2309 "sink"
2310 (append (unless (eq target-patch :bottom)
2311 (list "--to" target-patch))
2312 '("--")
2313 sorted-patchsyms)))))
7cc45294
GH
2314 (stgit-reload))
2315
594aa463
KH
2316(defun stgit-squash (patchsyms)
2317 "Squash the patches in PATCHSYMS.
693d179b
GH
2318Interactively, squash the marked patches.
2319
2320Unless there are any conflicts, the patches will be merged into
2321one patch, which will occupy the same spot in the series as the
2322deepest patch had before the squash."
d51722b7 2323 (interactive (list stgit-marked-patches))
9d04c657 2324 (stgit-assert-mode)
d51722b7 2325 (when (< (length patchsyms) 2)
594aa463 2326 (error "Need at least two patches to squash"))
32d7545d
GH
2327 (let ((stgit-buffer (current-buffer))
2328 (edit-buf (get-buffer-create "*StGit edit*"))
693d179b
GH
2329 (dir default-directory)
2330 (sorted-patchsyms (stgit-sort-patches patchsyms)))
594aa463 2331 (log-edit 'stgit-confirm-squash t nil edit-buf)
693d179b 2332 (set (make-local-variable 'stgit-patchsyms) sorted-patchsyms)
ea0def18 2333 (setq default-directory dir)
32d7545d 2334 (let ((result (let ((standard-output edit-buf))
655a3977
GH
2335 (save-excursion
2336 (apply 'stgit-run-silent "squash"
69db9714 2337 "--save-template=-" "--" sorted-patchsyms)))))
32d7545d
GH
2338
2339 ;; stg squash may have reordered the patches or caused conflicts
2340 (with-current-buffer stgit-buffer
2341 (stgit-reload))
2342
2343 (unless (eq 0 result)
2344 (fundamental-mode)
2345 (rename-buffer "*StGit error*")
2346 (resize-temp-buffer-window)
2347 (switch-to-buffer-other-window stgit-buffer)
2348 (error "stg squash failed")))))
ea0def18 2349
594aa463 2350(defun stgit-confirm-squash ()
ea0def18
DK
2351 (interactive)
2352 (let ((file (make-temp-file "stgit-edit-")))
2353 (write-region (point-min) (point-max) file)
2354 (stgit-capture-output nil
69db9714 2355 (apply 'stgit-run "squash" "-f" file "--" stgit-patchsyms))
ea0def18 2356 (with-current-buffer log-edit-parent-buffer
e6b1fdae
DK
2357 (stgit-clear-marks)
2358 ;; Go to first marked patch and stay there
2359 (goto-char (point-min))
2360 (re-search-forward (concat "^[>+-]\\*") nil t)
2361 (move-to-column goal-column)
2362 (let ((pos (point)))
1f0bf00f 2363 (stgit-reload)
e6b1fdae 2364 (goto-char pos)))))
ea0def18 2365
0663524d
KH
2366(defun stgit-help ()
2367 "Display help for the StGit mode."
2368 (interactive)
2369 (describe-function 'stgit-mode))
3a59f3db 2370
e8faa44b
DK
2371(defun stgit-execute-process-sentinel (process sentinel)
2372 (let (old-sentinel stgit-buf)
2373 (with-current-buffer (process-buffer process)
2374 (setq old-sentinel old-process-sentinel
2375 stgit-buf stgit-buffer))
2376 (and (memq (process-status process) '(exit signal))
2377 (buffer-live-p stgit-buf)
2378 (with-current-buffer stgit-buf
2379 (stgit-reload)))
2380 (funcall old-sentinel process sentinel)))
2381
a4a01630
GH
2382(defun stgit-execute-process-filter (process output)
2383 (with-current-buffer (process-buffer process)
2384 (let* ((old-point (point))
2385 (pmark (process-mark process))
2386 (insert-at (marker-position pmark))
2387 (at-pmark (= insert-at old-point)))
2388 (goto-char insert-at)
2389 (insert-before-markers output)
2390 (comint-carriage-motion insert-at (point))
2391 (set-marker pmark (point))
2392 (unless at-pmark
2393 (goto-char old-point)))))
2394
e8faa44b
DK
2395(defun stgit-execute ()
2396 "Prompt for an stg command to execute in a shell.
2397
2398The names of any marked patches or the patch at point are
2399inserted in the command to be executed.
2400
2401If the command ends in an ampersand, run it asynchronously.
2402
2403When the command has finished, reload the stgit buffer."
2404 (interactive)
2405 (stgit-assert-mode)
bf7e391c 2406 (let* ((patches (stgit-patches-marked-or-at-point nil 'allow-committed))
e8faa44b
DK
2407 (patch-names (mapcar 'symbol-name patches))
2408 (hyphens (find-if (lambda (s) (string-match "^-" s)) patch-names))
2409 (defaultcmd (if patches
2410 (concat "stg "
2411 (and hyphens "-- ")
2412 (mapconcat 'identity patch-names " "))
2413 "stg "))
2414 (cmd (read-from-minibuffer "Shell command: " (cons defaultcmd 5)
2415 nil nil 'shell-command-history))
2416 (async (string-match "&[ \t]*\\'" cmd))
2417 (buffer (get-buffer-create
2418 (if async
2419 "*Async Shell Command*"
2420 "*Shell Command Output*"))))
2421 ;; cannot use minibuffer as stgit-reload would overwrite it; if we
2422 ;; show the buffer, shell-command will not use the minibuffer
2423 (display-buffer buffer)
2424 (shell-command cmd)
2425 (if async
2426 (let ((old-buffer (current-buffer)))
2427 (with-current-buffer buffer
2428 (let ((process (get-buffer-process buffer)))
2429 (set (make-local-variable 'old-process-sentinel)
2430 (process-sentinel process))
2431 (set (make-local-variable 'stgit-buffer)
2432 old-buffer)
a4a01630 2433 (set-process-filter process 'stgit-execute-process-filter)
e8faa44b 2434 (set-process-sentinel process 'stgit-execute-process-sentinel))))
a4a01630
GH
2435 (with-current-buffer buffer
2436 (comint-carriage-motion (point-min) (point-max)))
e8faa44b
DK
2437 (shrink-window-if-larger-than-buffer (get-buffer-window buffer))
2438 (stgit-reload))))
2439
6c2d4962
GH
2440(defun stgit-undo-or-redo (redo hard)
2441 "Run stg undo or, if REDO is non-nil, stg redo.
2442
2443If HARD is non-nil, use the --hard flag."
2444 (stgit-assert-mode)
2445 (let ((cmd (if redo "redo" "undo")))
2446 (stgit-capture-output nil
2447 (if arg
2448 (when (or (and (stgit-index-empty-p)
2449 (stgit-work-tree-empty-p))
2450 (y-or-n-p (format "Hard %s may overwrite index/work tree changes. Continue? "
2451 cmd)))
2452 (stgit-run cmd "--hard"))
2453 (stgit-run cmd))))
2454 (stgit-reload))
2455
83e51dbf
DK
2456(defun stgit-undo (&optional arg)
2457 "Run stg undo.
b8463f1d
GH
2458With prefix argument, run it with the --hard flag.
2459
2460See also `stgit-redo'."
83e51dbf 2461 (interactive "P")
6c2d4962 2462 (stgit-undo-or-redo nil arg))
83e51dbf 2463
b8463f1d
GH
2464(defun stgit-redo (&optional arg)
2465 "Run stg redo.
2466With prefix argument, run it with the --hard flag.
2467
2468See also `stgit-undo'."
2469 (interactive "P")
6c2d4962 2470 (stgit-undo-or-redo t arg))
b8463f1d 2471
4d73c4d8
DK
2472(defun stgit-refresh (&optional arg)
2473 "Run stg refresh.
36a4eacd
GH
2474If the index contains any changes, only refresh from index.
2475
a53347d9 2476With prefix argument, refresh the marked patch or the patch under point."
4d73c4d8 2477 (interactive "P")
9d04c657 2478 (stgit-assert-mode)
4d73c4d8 2479 (let ((patchargs (if arg
b51a910f 2480 (let ((patches (stgit-patches-marked-or-at-point t t)))
beac0f14
GH
2481 (when (> (length patches) 1)
2482 (error "Too many patches marked"))
2483 (cons "-p" patches))
b0424080 2484 nil)))
36a4eacd
GH
2485 (unless (stgit-index-empty-p)
2486 (setq patchargs (cons "--index" patchargs)))
4d73c4d8 2487 (stgit-capture-output nil
074a4fb0
GH
2488 (apply 'stgit-run "refresh" patchargs))
2489 (stgit-refresh-git-status))
4d73c4d8
DK
2490 (stgit-reload))
2491
ce3b6130 2492(defvar stgit-show-worktree nil
8f702de4 2493 "If nil, inhibit showing work tree and index in the stgit buffer.
ce3b6130 2494
8f702de4 2495See also `stgit-show-worktree-mode'.")
ce3b6130 2496
d9473917
GH
2497(defvar stgit-show-ignored nil
2498 "If nil, inhibit showing files ignored by git.")
2499
2500(defvar stgit-show-unknown nil
2501 "If nil, inhibit showing files not registered with git.")
2502
a0045b87
DK
2503(defvar stgit-show-patch-names t
2504 "If nil, inhibit showing patch names.")
2505
bf7e391c
GH
2506(defvar stgit-show-committed nil
2507 "If nil, inhibit showing recent commits.")
2508
2509(defvar stgit-committed-count nil
2510 "The number of recent commits to show.")
2511
8a619f55
GH
2512(defmacro stgit-define-toggle-view (sym help)
2513 (declare (indent 1))
2514 (let* ((name (symbol-name sym))
2515 (fun (intern (concat "stgit-toggle-" name)))
2516 (flag (intern (concat "stgit-show-" name))))
2517 ;; make help-follow find the correct function
2518 `(put (quote ,fun) 'definition-name 'stgit-define-toggle-view)
2519 `(defun ,fun (&optional arg)
2520 ,help
2521 (interactive "P")
2522 (stgit-assert-mode)
2523 (setq ,flag (if arg
2524 (> (prefix-numeric-value arg) 0)
2525 (not ,flag)))
2526 (stgit-reload))))
2527
2528(stgit-define-toggle-view worktree
ce3b6130 2529 "Toggle the visibility of the work tree.
2d7bcbd9 2530With ARG, show the work tree if ARG is positive.
ce3b6130 2531
8f702de4
GH
2532Its initial setting is controlled by `stgit-default-show-worktree'.
2533
2534`stgit-show-worktree-mode' controls where on screen the index and
8a619f55 2535work tree will show up.")
ce3b6130 2536
8a619f55 2537(stgit-define-toggle-view ignored
d9473917
GH
2538 "Toggle the visibility of files ignored by git in the work
2539tree. With ARG, show these files if ARG is positive.
2540
bc11fb08
GH
2541Its initial setting is controlled by `stgit-default-show-ignored'.
2542
8a619f55 2543Use \\[stgit-toggle-worktree] to show the work tree.")
d9473917 2544
8a619f55 2545(stgit-define-toggle-view unknown
d9473917
GH
2546 "Toggle the visibility of files not registered with git in the
2547work tree. With ARG, show these files if ARG is positive.
2548
bc11fb08
GH
2549Its initial setting is controlled by `stgit-default-show-unknown'.
2550
8a619f55 2551Use \\[stgit-toggle-worktree] to show the work tree.")
d9473917 2552
8a619f55 2553(stgit-define-toggle-view patch-names
a0045b87
DK
2554 "Toggle the visibility of patch names. With ARG, show patch names
2555if ARG is positive.
2556
8a619f55 2557The initial setting is controlled by `stgit-default-show-patch-names'.")
a0045b87 2558
bf7e391c
GH
2559(defun stgit-toggle-committed (&optional arg)
2560 "Toggle the visibility of historical git commits.
2561With ARG, set the number of commits to show to ARG, and disable
2562them if ARG is zero.
2563
2564The initial setting is controlled by `stgit-default-show-committed'."
2565 (interactive "P")
2566 (stgit-assert-mode)
2567 (if (null arg)
2568 (setq stgit-show-committed (not stgit-show-committed))
2569 (let ((n (prefix-numeric-value arg)))
2570 (setq stgit-show-committed (> n 0))
2571 (setq stgit-committed-count n)))
2572 (stgit-reload))
2573
3a59f3db 2574(provide 'stgit)