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