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