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