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