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