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