stgit.el: Add stgit-redo as C-c C-_ and C-c C-/
[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
0f076fe6 12(require 'git nil t)
50d88c67 13(require 'cl)
98230edd 14(require 'ewoc)
0f076fe6 15
56d81fe5 16(defun stgit (dir)
a53347d9 17 "Manage StGit patches for the tree in DIR."
56d81fe5 18 (interactive "DDirectory: \n")
52144ce5 19 (switch-to-stgit-buffer (git-get-top-dir dir))
1f0bf00f 20 (stgit-reload))
56d81fe5 21
074a4fb0
GH
22(unless (fboundp 'git-get-top-dir)
23 (defun git-get-top-dir (dir)
24 "Retrieve the top-level directory of a git tree."
25 (let ((cdup (with-output-to-string
26 (with-current-buffer standard-output
27 (cd dir)
28 (unless (eq 0 (call-process "git" nil t nil
29 "rev-parse" "--show-cdup"))
df283a8b 30 (error "Cannot find top-level git tree for %s" dir))))))
074a4fb0
GH
31 (expand-file-name (concat (file-name-as-directory dir)
32 (car (split-string cdup "\n")))))))
33
34(defun stgit-refresh-git-status (&optional dir)
35 "If it exists, refresh the `git-status' buffer belonging to
36directory DIR or `default-directory'"
37 (when (and (fboundp 'git-find-status-buffer)
38 (fboundp 'git-refresh-status))
39 (let* ((top-dir (git-get-top-dir (or dir default-directory)))
40 (git-status-buffer (and top-dir (git-find-status-buffer top-dir))))
41 (when git-status-buffer
42 (with-current-buffer git-status-buffer
43 (git-refresh-status))))))
52144ce5 44
b894e680
DK
45(defun stgit-find-buffer (dir)
46 "Return the buffer displaying StGit patches for DIR, or nil if none."
56d81fe5
DK
47 (setq dir (file-name-as-directory dir))
48 (let ((buffers (buffer-list)))
49 (while (and buffers
50 (not (with-current-buffer (car buffers)
51 (and (eq major-mode 'stgit-mode)
52 (string= default-directory dir)))))
53 (setq buffers (cdr buffers)))
b894e680
DK
54 (and buffers (car buffers))))
55
56(defun switch-to-stgit-buffer (dir)
57 "Switch to a (possibly new) buffer displaying StGit patches for DIR."
58 (setq dir (file-name-as-directory dir))
59 (let ((buffer (stgit-find-buffer dir)))
60 (switch-to-buffer (or buffer
61 (create-stgit-buffer dir)))))
62
2c862b07 63(defstruct (stgit-patch)
3164eec6 64 status name desc empty files-ewoc)
56d81fe5 65
98230edd 66(defun stgit-patch-pp (patch)
9153ce3a
GH
67 (let* ((status (stgit-patch-status patch))
68 (start (point))
69 (name (stgit-patch-name patch))
70 (face (cdr (assq status stgit-patch-status-face-alist))))
71 (insert (case status
72 ('applied "+")
73 ('top ">")
74 ('unapplied "-")
75 (t " "))
76 (if (memq name stgit-marked-patches)
77 "*" " "))
78 (if (memq status '(index work))
79 (insert (propertize (if (eq status 'index) "Index" "Work tree")
80 'face face))
81 (insert (format "%-30s"
224ef1ec
GH
82 (propertize (symbol-name name)
83 'face face
84 'syntax-table (string-to-syntax "w")))
9153ce3a
GH
85 " "
86 (if (stgit-patch-empty patch) "(empty) " "")
87 (propertize (or (stgit-patch-desc patch) "")
88 'face 'stgit-description-face)))
4f7ff561 89 (insert "\n")
f9b82d36 90 (put-text-property start (point) 'entry-type 'patch)
98230edd 91 (when (memq name stgit-expanded-patches)
0de6881a 92 (stgit-insert-patch-files patch))
98230edd
DK
93 (put-text-property start (point) 'patch-data patch)))
94
56d81fe5
DK
95(defun create-stgit-buffer (dir)
96 "Create a buffer for showing StGit patches.
97Argument DIR is the repository path."
98 (let ((buf (create-file-buffer (concat dir "*stgit*")))
99 (inhibit-read-only t))
100 (with-current-buffer buf
101 (setq default-directory dir)
102 (stgit-mode)
98230edd 103 (set (make-local-variable 'stgit-ewoc)
4f7ff561 104 (ewoc-create #'stgit-patch-pp "Branch:\n\n" "--\n" t))
56d81fe5
DK
105 (setq buffer-read-only t))
106 buf))
107
108(defmacro stgit-capture-output (name &rest body)
e558a4ab
GH
109 "Capture StGit output and, if there was any output, show it in a window
110at the end.
111Returns nil if there was no output."
94baef5a
DK
112 (declare (debug ([&or stringp null] body))
113 (indent 1))
34afb86c
DK
114 `(let ((output-buf (get-buffer-create ,(or name "*StGit output*")))
115 (stgit-dir default-directory)
116 (inhibit-read-only t))
56d81fe5 117 (with-current-buffer output-buf
34afb86c
DK
118 (erase-buffer)
119 (setq default-directory stgit-dir)
120 (setq buffer-read-only t))
56d81fe5
DK
121 (let ((standard-output output-buf))
122 ,@body)
34afb86c
DK
123 (with-current-buffer output-buf
124 (set-buffer-modified-p nil)
125 (setq buffer-read-only t)
126 (if (< (point-min) (point-max))
127 (display-buffer output-buf t)))))
56d81fe5 128
d51722b7
GH
129(defun stgit-make-run-args (args)
130 "Return a copy of ARGS with its elements converted to strings."
131 (mapcar (lambda (x)
132 ;; don't use (format "%s" ...) to limit type errors
133 (cond ((stringp x) x)
134 ((integerp x) (number-to-string x))
135 ((symbolp x) (symbol-name x))
136 (t
137 (error "Bad element in stgit-make-run-args args: %S" x))))
138 args))
139
9aecd505 140(defun stgit-run-silent (&rest args)
d51722b7 141 (setq args (stgit-make-run-args args))
56d81fe5
DK
142 (apply 'call-process "stg" nil standard-output nil args))
143
9aecd505 144(defun stgit-run (&rest args)
d51722b7 145 (setq args (stgit-make-run-args args))
9aecd505
DK
146 (let ((msgcmd (mapconcat #'identity args " ")))
147 (message "Running stg %s..." msgcmd)
148 (apply 'call-process "stg" nil standard-output nil args)
149 (message "Running stg %s...done" msgcmd)))
150
378a003d 151(defun stgit-run-git (&rest args)
d51722b7 152 (setq args (stgit-make-run-args args))
378a003d
GH
153 (let ((msgcmd (mapconcat #'identity args " ")))
154 (message "Running git %s..." msgcmd)
155 (apply 'call-process "git" nil standard-output nil args)
156 (message "Running git %s...done" msgcmd)))
157
1f60181a 158(defun stgit-run-git-silent (&rest args)
d51722b7 159 (setq args (stgit-make-run-args args))
1f60181a
GH
160 (apply 'call-process "git" nil standard-output nil args))
161
b894e680
DK
162(defun stgit-index-empty-p ()
163 "Returns non-nil if the index contains no changes from HEAD."
164 (zerop (stgit-run-git-silent "diff-index" "--cached" "--quiet" "HEAD")))
165
2ecb05c8
GH
166(defvar stgit-index-node)
167(defvar stgit-worktree-node)
210a2a52
DK
168
169(defun stgit-refresh-index ()
170 (when stgit-index-node
171 (ewoc-invalidate (car stgit-index-node) (cdr stgit-index-node))))
172
173(defun stgit-refresh-worktree ()
174 (when stgit-worktree-node
175 (ewoc-invalidate (car stgit-worktree-node) (cdr stgit-worktree-node))))
176
8f702de4
GH
177(defun stgit-run-series-insert-index (ewoc)
178 (setq index-node (cons ewoc (ewoc-enter-last ewoc
179 (make-stgit-patch
180 :status 'index
181 :name :index
182 :desc nil
183 :empty nil)))
184 worktree-node (cons ewoc (ewoc-enter-last ewoc
185 (make-stgit-patch
186 :status 'work
187 :name :work
188 :desc nil
189 :empty nil)))))
190
98230edd 191(defun stgit-run-series (ewoc)
8f702de4
GH
192 (setq stgit-index-node nil
193 stgit-worktree-node nil)
194 (let ((inserted-index (not stgit-show-worktree))
195 index-node
03fc3b26
GH
196 worktree-node
197 all-patchsyms)
98230edd
DK
198 (with-temp-buffer
199 (let ((exit-status (stgit-run-silent "series" "--description" "--empty")))
200 (goto-char (point-min))
201 (if (not (zerop exit-status))
202 (cond ((looking-at "stg series: \\(.*\\)")
8f702de4 203 (setq inserted-index t)
98230edd 204 (ewoc-set-hf ewoc (car (ewoc-get-hf ewoc))
8f702de4
GH
205 (substitute-command-keys
206 "-- not initialized; run \\[stgit-init]")))
98230edd
DK
207 ((looking-at ".*")
208 (error "Error running stg: %s"
209 (match-string 0))))
210 (while (not (eobp))
211 (unless (looking-at
212 "\\([0 ]\\)\\([>+-]\\)\\( \\)\\([^ ]+\\) *[|#] \\(.*\\)")
213 (error "Syntax error in output from stg series"))
214 (let* ((state-str (match-string 2))
215 (state (cond ((string= state-str ">") 'top)
216 ((string= state-str "+") 'applied)
8f702de4
GH
217 ((string= state-str "-") 'unapplied)))
218 (name (intern (match-string 4)))
219 (desc (match-string 5))
220 (empty (string= (match-string 1) "0")))
221 (unless inserted-index
222 (when (or (eq stgit-show-worktree-mode 'top)
223 (and (eq stgit-show-worktree-mode 'center)
224 (eq state 'unapplied)))
225 (setq inserted-index t)
226 (stgit-run-series-insert-index ewoc)))
03fc3b26 227 (setq all-patchsyms (cons name all-patchsyms))
98230edd
DK
228 (ewoc-enter-last ewoc
229 (make-stgit-patch
230 :status state
8f702de4
GH
231 :name name
232 :desc desc
233 :empty empty)))
234 (forward-line 1))))
235 (unless inserted-index
236 (stgit-run-series-insert-index ewoc)))
237 (setq stgit-index-node index-node
03fc3b26
GH
238 stgit-worktree-node worktree-node
239 stgit-marked-patches (intersection stgit-marked-patches
240 all-patchsyms))))
98230edd 241
1f0bf00f 242(defun stgit-reload ()
a53347d9 243 "Update the contents of the StGit buffer."
56d81fe5
DK
244 (interactive)
245 (let ((inhibit-read-only t)
246 (curline (line-number-at-pos))
a9089e68
GH
247 (curpatch (stgit-patch-name-at-point))
248 (curfile (stgit-patched-file-at-point)))
98230edd
DK
249 (ewoc-filter stgit-ewoc #'(lambda (x) nil))
250 (ewoc-set-hf stgit-ewoc
251 (concat "Branch: "
252 (propertize
253 (with-temp-buffer
254 (stgit-run-silent "branch")
255 (buffer-substring (point-min) (1- (point-max))))
4f292066 256 'face 'stgit-branch-name-face)
4f7ff561 257 "\n\n")
ce3b6130
DK
258 (if stgit-show-worktree
259 "--"
260 (propertize
261 (substitute-command-keys "--\n\"\\[stgit-toggle-worktree]\"\
262 shows the working tree\n")
263 'face 'stgit-description-face)))
98230edd 264 (stgit-run-series stgit-ewoc)
56d81fe5 265 (if curpatch
a9089e68 266 (stgit-goto-patch curpatch (and curfile (stgit-file-file curfile)))
074a4fb0
GH
267 (goto-line curline)))
268 (stgit-refresh-git-status))
56d81fe5 269
8f40753a
GH
270(defgroup stgit nil
271 "A user interface for the StGit patch maintenance tool."
272 :group 'tools)
273
07f464e0
DK
274(defface stgit-description-face
275 '((((background dark)) (:foreground "tan"))
276 (((background light)) (:foreground "dark red")))
8f40753a
GH
277 "The face used for StGit descriptions"
278 :group 'stgit)
4f292066
GH
279
280(defface stgit-branch-name-face
281 '((t :inherit bold))
282 "The face used for the StGit branch name"
283 :group 'stgit)
07f464e0
DK
284
285(defface stgit-top-patch-face
286 '((((background dark)) (:weight bold :foreground "yellow"))
287 (((background light)) (:weight bold :foreground "purple"))
288 (t (:weight bold)))
8f40753a
GH
289 "The face used for the top patch names"
290 :group 'stgit)
07f464e0
DK
291
292(defface stgit-applied-patch-face
293 '((((background dark)) (:foreground "light yellow"))
294 (((background light)) (:foreground "purple"))
295 (t ()))
8f40753a
GH
296 "The face used for applied patch names"
297 :group 'stgit)
07f464e0
DK
298
299(defface stgit-unapplied-patch-face
300 '((((background dark)) (:foreground "gray80"))
301 (((background light)) (:foreground "orchid"))
302 (t ()))
8f40753a
GH
303 "The face used for unapplied patch names"
304 :group 'stgit)
07f464e0 305
1f60181a
GH
306(defface stgit-modified-file-face
307 '((((class color) (background light)) (:foreground "purple"))
308 (((class color) (background dark)) (:foreground "salmon")))
309 "StGit mode face used for modified file status"
310 :group 'stgit)
311
312(defface stgit-unmerged-file-face
313 '((((class color) (background light)) (:foreground "red" :bold t))
314 (((class color) (background dark)) (:foreground "red" :bold t)))
315 "StGit mode face used for unmerged file status"
316 :group 'stgit)
317
318(defface stgit-unknown-file-face
319 '((((class color) (background light)) (:foreground "goldenrod" :bold t))
320 (((class color) (background dark)) (:foreground "goldenrod" :bold t)))
321 "StGit mode face used for unknown file status"
322 :group 'stgit)
323
d9473917
GH
324(defface stgit-ignored-file-face
325 '((((class color) (background light)) (:foreground "grey60"))
326 (((class color) (background dark)) (:foreground "grey40")))
327 "StGit mode face used for ignored files")
328
a6d9a852
GH
329(defface stgit-file-permission-face
330 '((((class color) (background light)) (:foreground "green" :bold t))
331 (((class color) (background dark)) (:foreground "green" :bold t)))
332 "StGit mode face used for permission changes."
333 :group 'stgit)
334
46a273fd
GH
335(defface stgit-index-work-tree-title-face
336 '((((supports :slant italic)) :slant italic)
337 (t :inherit bold))
338 "StGit mode face used for the \"Index\" and \"Work tree\" titles"
339 :group 'stgit)
340
341
b6df231c 342(defcustom stgit-find-copies-harder
1f60181a
GH
343 nil
344 "Try harder to find copied files when listing patches.
345
346When not nil, runs git diff-tree with the --find-copies-harder
347flag, which reduces performance."
348 :type 'boolean
349 :group 'stgit)
350
351(defconst stgit-file-status-code-strings
352 (mapcar (lambda (arg)
353 (cons (car arg)
a6d9a852
GH
354 (propertize (cadr arg) 'face (car (cddr arg)))))
355 '((add "Added" stgit-modified-file-face)
356 (copy "Copied" stgit-modified-file-face)
357 (delete "Deleted" stgit-modified-file-face)
358 (modify "Modified" stgit-modified-file-face)
359 (rename "Renamed" stgit-modified-file-face)
360 (mode-change "Mode change" stgit-modified-file-face)
361 (unmerged "Unmerged" stgit-unmerged-file-face)
d9473917
GH
362 (unknown "Unknown" stgit-unknown-file-face)
363 (ignore "Ignored" stgit-ignored-file-face)))
1f60181a
GH
364 "Alist of code symbols to description strings")
365
000f337c
GH
366(defconst stgit-patch-status-face-alist
367 '((applied . stgit-applied-patch-face)
368 (top . stgit-top-patch-face)
369 (unapplied . stgit-unapplied-patch-face)
9153ce3a
GH
370 (index . stgit-index-work-tree-title-face)
371 (work . stgit-index-work-tree-title-face))
000f337c
GH
372 "Alist of face to use for a given patch status")
373
3164eec6
DK
374(defun stgit-file-status-code-as-string (file)
375 "Return stgit status code for FILE as a string"
376 (let* ((code (assq (stgit-file-status file)
377 stgit-file-status-code-strings))
378 (score (stgit-file-cr-score file)))
379 (when code
a6d9a852 380 (format "%-11s "
3164eec6
DK
381 (if (and score (/= score 100))
382 (format "%s %s" (cdr code)
383 (propertize (format "%d%%" score)
a6d9a852 384 'face 'stgit-description-face))
3164eec6 385 (cdr code))))))
1f60181a 386
a6d9a852 387(defun stgit-file-status-code (str &optional score)
1f60181a
GH
388 "Return stgit status code from git status string"
389 (let ((code (assoc str '(("A" . add)
390 ("C" . copy)
391 ("D" . delete)
d9473917 392 ("I" . ignore)
1f60181a
GH
393 ("M" . modify)
394 ("R" . rename)
395 ("T" . mode-change)
396 ("U" . unmerged)
397 ("X" . unknown)))))
a6d9a852
GH
398 (setq code (if code (cdr code) 'unknown))
399 (when (stringp score)
400 (if (> (length score) 0)
401 (setq score (string-to-number score))
402 (setq score nil)))
403 (if score (cons code score) code)))
404
405(defconst stgit-file-type-strings
406 '((#o100 . "file")
407 (#o120 . "symlink")
408 (#o160 . "subproject"))
409 "Alist of names of file types")
410
411(defun stgit-file-type-string (type)
47271f41
GH
412 "Return string describing file type TYPE (the high bits of file permission).
413Cf. `stgit-file-type-strings' and `stgit-file-type-change-string'."
a6d9a852
GH
414 (let ((type-str (assoc type stgit-file-type-strings)))
415 (or (and type-str (cdr type-str))
416 (format "unknown type %o" type))))
417
418(defun stgit-file-type-change-string (old-perm new-perm)
47271f41
GH
419 "Return string describing file type change from OLD-PERM to NEW-PERM.
420Cf. `stgit-file-type-string'."
a6d9a852
GH
421 (let ((old-type (lsh old-perm -9))
422 (new-type (lsh new-perm -9)))
423 (cond ((= old-type new-type) "")
424 ((zerop new-type) "")
425 ((zerop old-type)
426 (if (= new-type #o100)
427 ""
428 (format " (%s)" (stgit-file-type-string new-type))))
429 (t (format " (%s -> %s)"
430 (stgit-file-type-string old-type)
431 (stgit-file-type-string new-type))))))
432
433(defun stgit-file-mode-change-string (old-perm new-perm)
47271f41
GH
434 "Return string describing file mode change from OLD-PERM to NEW-PERM.
435Cf. `stgit-file-type-change-string'."
a6d9a852
GH
436 (setq old-perm (logand old-perm #o777)
437 new-perm (logand new-perm #o777))
438 (if (or (= old-perm new-perm)
439 (zerop old-perm)
440 (zerop new-perm))
441 ""
442 (let* ((modified (logxor old-perm new-perm))
443 (not-x-modified (logand (logxor old-perm new-perm) #o666)))
444 (cond ((zerop modified) "")
445 ((and (zerop not-x-modified)
446 (or (and (eq #o111 (logand old-perm #o111))
447 (propertize "-x" 'face 'stgit-file-permission-face))
448 (and (eq #o111 (logand new-perm #o111))
449 (propertize "+x" 'face
450 'stgit-file-permission-face)))))
451 (t (concat (propertize (format "%o" old-perm)
452 'face 'stgit-file-permission-face)
453 (propertize " -> "
454 'face 'stgit-description-face)
455 (propertize (format "%o" new-perm)
456 'face 'stgit-file-permission-face)))))))
1f60181a 457
0de6881a
DK
458(defstruct (stgit-file)
459 old-perm new-perm copy-or-rename cr-score cr-from cr-to status file)
460
3164eec6 461(defun stgit-file-pp (file)
0de6881a
DK
462 (let ((status (stgit-file-status file))
463 (name (if (stgit-file-copy-or-rename file)
464 (concat (stgit-file-cr-from file)
465 (propertize " -> "
466 'face 'stgit-description-face)
467 (stgit-file-cr-to file))
468 (stgit-file-file file)))
469 (mode-change (stgit-file-mode-change-string
470 (stgit-file-old-perm file)
471 (stgit-file-new-perm file)))
472 (start (point)))
3164eec6
DK
473 (insert (format " %-12s%1s%s%s\n"
474 (stgit-file-status-code-as-string file)
98230edd 475 mode-change
0de6881a
DK
476 name
477 (propertize (stgit-file-type-change-string
478 (stgit-file-old-perm file)
479 (stgit-file-new-perm file))
98230edd 480 'face 'stgit-description-face)))
0de6881a 481 (add-text-properties start (point)
3164eec6
DK
482 (list 'entry-type 'file
483 'file-data file))))
0de6881a 484
7567401c
GH
485(defun stgit-find-copies-harder-diff-arg ()
486 "Return the flag to use with `git-diff' depending on the
b6df231c
GH
487`stgit-find-copies-harder' flag."
488 (if stgit-find-copies-harder "--find-copies-harder" "-C"))
7567401c 489
d9473917
GH
490(defun stgit-insert-ls-files (args file-flag)
491 (let ((start (point)))
492 (apply 'stgit-run-git
493 (append '("ls-files" "--exclude-standard" "-z") args))
494 (goto-char start)
495 (while (looking-at "\\([^\0]*\\)\0")
496 (let ((name-len (- (match-end 0) (match-beginning 0))))
497 (insert ":0 0 0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 " file-flag "\0")
498 (forward-char name-len)))))
499
0de6881a 500(defun stgit-insert-patch-files (patch)
88134ff7
GH
501 "Expand (show modification of) the patch PATCH after the line
502at point."
3164eec6 503 (let* ((patchsym (stgit-patch-name patch))
0434bec1
GH
504 (end (point-marker))
505 (args (list "-z" (stgit-find-copies-harder-diff-arg)))
506 (ewoc (ewoc-create #'stgit-file-pp nil nil t)))
507 (set-marker-insertion-type end t)
3164eec6 508 (setf (stgit-patch-files-ewoc patch) ewoc)
0de6881a 509 (with-temp-buffer
b894e680
DK
510 (apply 'stgit-run-git
511 (cond ((eq patchsym :work)
030f0535 512 `("diff-files" "-0" ,@args))
b894e680
DK
513 ((eq patchsym :index)
514 `("diff-index" ,@args "--cached" "HEAD"))
515 (t
516 `("diff-tree" ,@args "-r" ,(stgit-id patchsym)))))
d9473917
GH
517
518 (when (and (eq patchsym :work))
519 (when stgit-show-ignored
520 (stgit-insert-ls-files '("--ignored" "--others") "I"))
521 (when stgit-show-unknown
522 (stgit-insert-ls-files '("--others") "X"))
523 (sort-regexp-fields nil ":[^\0]*\0\\([^\0]*\\)\0" "\\1"
524 (point-min) (point-max)))
525
0de6881a 526 (goto-char (point-min))
b894e680
DK
527 (unless (or (eobp) (memq patchsym '(:work :index)))
528 (forward-char 41))
0de6881a
DK
529 (while (looking-at ":\\([0-7]+\\) \\([0-7]+\\) [0-9A-Fa-f]\\{40\\} [0-9A-Fa-f]\\{40\\} ")
530 (let ((old-perm (string-to-number (match-string 1) 8))
531 (new-perm (string-to-number (match-string 2) 8)))
532 (goto-char (match-end 0))
533 (let ((file
534 (cond ((looking-at
535 "\\([CR]\\)\\([0-9]*\\)\0\\([^\0]*\\)\0\\([^\0]*\\)\0")
027e1370
GH
536 (let* ((patch-status (stgit-patch-status patch))
537 (file-subexp (if (eq patch-status 'unapplied)
538 3
539 4))
540 (file (match-string file-subexp)))
541 (make-stgit-file
542 :old-perm old-perm
543 :new-perm new-perm
544 :copy-or-rename t
545 :cr-score (string-to-number (match-string 2))
546 :cr-from (match-string 3)
547 :cr-to (match-string 4)
548 :status (stgit-file-status-code
549 (match-string 1))
550 :file file)))
0de6881a
DK
551 ((looking-at "\\([ABD-QS-Z]\\)\0\\([^\0]*\\)\0")
552 (make-stgit-file
553 :old-perm old-perm
554 :new-perm new-perm
555 :copy-or-rename nil
556 :cr-score nil
557 :cr-from nil
558 :cr-to nil
027e1370
GH
559 :status (stgit-file-status-code
560 (match-string 1))
0de6881a 561 :file (match-string 2))))))
3164eec6
DK
562 (ewoc-enter-last ewoc file))
563 (goto-char (match-end 0))))
564 (unless (ewoc-nth ewoc 0)
000f337c
GH
565 (ewoc-set-hf ewoc ""
566 (concat " "
567 (propertize "<no files>"
568 'face 'stgit-description-face)
569 "\n"))))
0434bec1 570 (goto-char end)))
07f464e0 571
030f0535
GH
572(defun stgit-find-file (&optional other-window)
573 (let* ((file (or (stgit-patched-file-at-point)
574 (error "No file at point")))
575 (filename (expand-file-name (stgit-file-file file))))
0de6881a
DK
576 (unless (file-exists-p filename)
577 (error "File does not exist"))
030f0535
GH
578 (funcall (if other-window 'find-file-other-window 'find-file)
579 filename)
580 (when (eq (stgit-file-status file) 'unmerged)
581 (smerge-mode 1))))
acc5652f 582
50d88c67 583(defun stgit-select-patch ()
98230edd
DK
584 (let ((patchname (stgit-patch-name-at-point)))
585 (if (memq patchname stgit-expanded-patches)
586 (setq stgit-expanded-patches (delq patchname stgit-expanded-patches))
587 (setq stgit-expanded-patches (cons patchname stgit-expanded-patches)))
588 (ewoc-invalidate stgit-ewoc (ewoc-locate stgit-ewoc)))
589 (move-to-column (stgit-goal-column)))
acc5652f 590
378a003d 591(defun stgit-select ()
da01a29b
GH
592 "With point on a patch, toggle showing files in the patch.
593
594With point on a file, open the associated file. Opens the target
595file for (applied) copies and renames."
378a003d 596 (interactive)
50d88c67
DK
597 (case (get-text-property (point) 'entry-type)
598 ('patch
599 (stgit-select-patch))
600 ('file
030f0535 601 (stgit-find-file))
50d88c67
DK
602 (t
603 (error "No patch or file on line"))))
378a003d
GH
604
605(defun stgit-find-file-other-window ()
606 "Open file at point in other window"
607 (interactive)
030f0535 608 (stgit-find-file t))
378a003d 609
d9b954c7
GH
610(defun stgit-find-file-merge ()
611 "Open file at point and merge it using `smerge-ediff'."
612 (interactive)
613 (stgit-find-file t)
614 (smerge-ediff))
615
83327d53 616(defun stgit-quit ()
a53347d9 617 "Hide the stgit buffer."
83327d53
GH
618 (interactive)
619 (bury-buffer))
620
0f076fe6 621(defun stgit-git-status ()
a53347d9 622 "Show status using `git-status'."
0f076fe6
GH
623 (interactive)
624 (unless (fboundp 'git-status)
df283a8b 625 (error "The stgit-git-status command requires git-status"))
0f076fe6
GH
626 (let ((dir default-directory))
627 (save-selected-window
628 (pop-to-buffer nil)
629 (git-status dir))))
630
58f72f16
GH
631(defun stgit-goal-column ()
632 "Return goal column for the current line"
50d88c67
DK
633 (case (get-text-property (point) 'entry-type)
634 ('patch 2)
635 ('file 4)
636 (t 0)))
58f72f16
GH
637
638(defun stgit-next-line (&optional arg)
378a003d 639 "Move cursor vertically down ARG lines"
58f72f16
GH
640 (interactive "p")
641 (next-line arg)
642 (move-to-column (stgit-goal-column)))
378a003d 643
58f72f16 644(defun stgit-previous-line (&optional arg)
378a003d 645 "Move cursor vertically up ARG lines"
58f72f16
GH
646 (interactive "p")
647 (previous-line arg)
648 (move-to-column (stgit-goal-column)))
378a003d
GH
649
650(defun stgit-next-patch (&optional arg)
98230edd 651 "Move cursor down ARG patches."
378a003d 652 (interactive "p")
98230edd
DK
653 (ewoc-goto-next stgit-ewoc (or arg 1))
654 (move-to-column goal-column))
378a003d
GH
655
656(defun stgit-previous-patch (&optional arg)
98230edd 657 "Move cursor up ARG patches."
378a003d 658 (interactive "p")
98230edd
DK
659 (ewoc-goto-prev stgit-ewoc (or arg 1))
660 (move-to-column goal-column))
378a003d 661
56d81fe5
DK
662(defvar stgit-mode-hook nil
663 "Run after `stgit-mode' is setup.")
664
665(defvar stgit-mode-map nil
666 "Keymap for StGit major mode.")
667
668(unless stgit-mode-map
d9b954c7
GH
669 (let ((diff-map (make-keymap))
670 (toggle-map (make-keymap)))
671 (suppress-keymap diff-map)
672 (mapc (lambda (arg) (define-key diff-map (car arg) (cdr arg)))
673 '(("b" . stgit-diff-base)
674 ("c" . stgit-diff-combined)
675 ("m" . stgit-find-file-merge)
676 ("o" . stgit-diff-ours)
677 ("t" . stgit-diff-theirs)))
ce3b6130
DK
678 (suppress-keymap toggle-map)
679 (mapc (lambda (arg) (define-key toggle-map (car arg) (cdr arg)))
d9473917
GH
680 '(("t" . stgit-toggle-worktree)
681 ("i" . stgit-toggle-ignored)
682 ("u" . stgit-toggle-unknown)))
ce3b6130
DK
683 (setq stgit-mode-map (make-keymap))
684 (suppress-keymap stgit-mode-map)
685 (mapc (lambda (arg) (define-key stgit-mode-map (car arg) (cdr arg)))
686 `((" " . stgit-mark)
687 ("m" . stgit-mark)
688 ("\d" . stgit-unmark-up)
689 ("u" . stgit-unmark-down)
690 ("?" . stgit-help)
691 ("h" . stgit-help)
692 ("\C-p" . stgit-previous-line)
693 ("\C-n" . stgit-next-line)
694 ([up] . stgit-previous-line)
695 ([down] . stgit-next-line)
696 ("p" . stgit-previous-patch)
697 ("n" . stgit-next-patch)
698 ("\M-{" . stgit-previous-patch)
699 ("\M-}" . stgit-next-patch)
700 ("s" . stgit-git-status)
408fa7cb 701 ("g" . stgit-reload-or-repair)
ce3b6130
DK
702 ("r" . stgit-refresh)
703 ("\C-c\C-r" . stgit-rename)
704 ("e" . stgit-edit)
705 ("M" . stgit-move-patches)
706 ("S" . stgit-squash)
707 ("N" . stgit-new)
e9fdd4ea
GH
708 ("\C-c\C-c" . stgit-commit)
709 ("\C-c\C-u" . stgit-uncommit)
3959a095 710 ("U" . stgit-revert-file)
51783171 711 ("R" . stgit-resolve-file)
ce3b6130
DK
712 ("\r" . stgit-select)
713 ("o" . stgit-find-file-other-window)
714 ("i" . stgit-file-toggle-index)
715 (">" . stgit-push-next)
716 ("<" . stgit-pop-next)
717 ("P" . stgit-push-or-pop)
718 ("G" . stgit-goto)
d9b954c7 719 ("=" . stgit-diff)
ce3b6130 720 ("D" . stgit-delete)
b8463f1d 721 ([?\C-/] . stgit-undo)
ce3b6130 722 ("\C-_" . stgit-undo)
b8463f1d
GH
723 ([?\C-c ?\C-/] . stgit-redo)
724 ("\C-c\C-_" . stgit-redo)
ce3b6130
DK
725 ("B" . stgit-branch)
726 ("t" . ,toggle-map)
d9b954c7 727 ("d" . ,diff-map)
ce3b6130 728 ("q" . stgit-quit)))))
56d81fe5
DK
729
730(defun stgit-mode ()
731 "Major mode for interacting with StGit.
732Commands:
733\\{stgit-mode-map}"
734 (kill-all-local-variables)
735 (buffer-disable-undo)
736 (setq mode-name "StGit"
737 major-mode 'stgit-mode
738 goal-column 2)
739 (use-local-map stgit-mode-map)
740 (set (make-local-variable 'list-buffers-directory) default-directory)
6df83d42 741 (set (make-local-variable 'stgit-marked-patches) nil)
6467d976 742 (set (make-local-variable 'stgit-expanded-patches) (list :work :index))
ce3b6130 743 (set (make-local-variable 'stgit-show-worktree) stgit-default-show-worktree)
2ecb05c8
GH
744 (set (make-local-variable 'stgit-index-node) nil)
745 (set (make-local-variable 'stgit-worktree-node) nil)
224ef1ec 746 (set (make-local-variable 'parse-sexp-lookup-properties) t)
2870f8b8 747 (set-variable 'truncate-lines 't)
b894e680 748 (add-hook 'after-save-hook 'stgit-update-saved-file)
56d81fe5
DK
749 (run-hooks 'stgit-mode-hook))
750
b894e680
DK
751(defun stgit-update-saved-file ()
752 (let* ((file (expand-file-name buffer-file-name))
753 (dir (file-name-directory file))
754 (gitdir (condition-case nil (git-get-top-dir dir)
755 (error nil)))
756 (buffer (and gitdir (stgit-find-buffer gitdir))))
757 (when buffer
758 (with-current-buffer buffer
210a2a52 759 (stgit-refresh-worktree)))))
b894e680 760
d51722b7
GH
761(defun stgit-add-mark (patchsym)
762 "Mark the patch PATCHSYM."
8036afdd 763 (setq stgit-marked-patches (cons patchsym stgit-marked-patches)))
6df83d42 764
d51722b7
GH
765(defun stgit-remove-mark (patchsym)
766 "Unmark the patch PATCHSYM."
8036afdd 767 (setq stgit-marked-patches (delq patchsym stgit-marked-patches)))
6df83d42 768
e6b1fdae 769(defun stgit-clear-marks ()
47271f41 770 "Unmark all patches."
e6b1fdae
DK
771 (setq stgit-marked-patches '()))
772
735cb7ec 773(defun stgit-patch-at-point (&optional cause-error)
2c862b07
DK
774 (get-text-property (point) 'patch-data))
775
64ada6f5 776(defun stgit-patch-name-at-point (&optional cause-error only-patches)
d51722b7 777 "Return the patch name on the current line as a symbol.
64ada6f5
GH
778If CAUSE-ERROR is not nil, signal an error if none found.
779If ONLY-PATCHES is not nil, only allow real patches, and not
780index or work tree."
2c862b07 781 (let ((patch (stgit-patch-at-point)))
64ada6f5
GH
782 (and patch
783 only-patches
784 (memq (stgit-patch-status patch) '(work index))
785 (setq patch nil))
2c862b07
DK
786 (cond (patch
787 (stgit-patch-name patch))
788 (cause-error
789 (error "No patch on this line")))))
378a003d 790
3164eec6
DK
791(defun stgit-patched-file-at-point ()
792 (get-text-property (point) 'file-data))
56d81fe5 793
7755d7f1 794(defun stgit-patches-marked-or-at-point ()
d51722b7 795 "Return the symbols of the marked patches, or the patch on the current line."
7755d7f1 796 (if stgit-marked-patches
d51722b7 797 stgit-marked-patches
2c862b07 798 (let ((patch (stgit-patch-name-at-point)))
7755d7f1
KH
799 (if patch
800 (list patch)
801 '()))))
802
a9089e68 803(defun stgit-goto-patch (patchsym &optional file)
d51722b7 804 "Move point to the line containing patch PATCHSYM.
a9089e68
GH
805If that patch cannot be found, do nothing.
806
807If the patch was found and FILE is not nil, instead move to that
808file's line. If FILE cannot be found, stay on the line of
809PATCHSYM."
f9b82d36
DK
810 (let ((node (ewoc-nth stgit-ewoc 0)))
811 (while (and node (not (eq (stgit-patch-name (ewoc-data node))
812 patchsym)))
813 (setq node (ewoc-next stgit-ewoc node)))
a9089e68
GH
814 (when (and node file)
815 (let* ((file-ewoc (stgit-patch-files-ewoc (ewoc-data node)))
816 (file-node (ewoc-nth file-ewoc 0)))
817 (while (and file-node (not (equal (stgit-file-file (ewoc-data file-node)) file)))
818 (setq file-node (ewoc-next file-ewoc file-node)))
819 (when file-node
820 (ewoc-goto-node file-ewoc file-node)
821 (move-to-column (stgit-goal-column))
822 (setq node nil))))
f9b82d36
DK
823 (when node
824 (ewoc-goto-node stgit-ewoc node)
d51722b7 825 (move-to-column goal-column))))
56d81fe5 826
1c2426dc 827(defun stgit-init ()
a53347d9 828 "Run stg init."
1c2426dc
DK
829 (interactive)
830 (stgit-capture-output nil
b0424080 831 (stgit-run "init"))
1f0bf00f 832 (stgit-reload))
1c2426dc 833
6df83d42 834(defun stgit-mark ()
a53347d9 835 "Mark the patch under point."
6df83d42 836 (interactive)
8036afdd 837 (let* ((node (ewoc-locate stgit-ewoc))
64ada6f5
GH
838 (patch (ewoc-data node))
839 (name (stgit-patch-name patch)))
840 (when (eq name :work)
841 (error "Cannot mark the work tree"))
842 (when (eq name :index)
843 (error "Cannot mark the index"))
8036afdd
DK
844 (stgit-add-mark (stgit-patch-name patch))
845 (ewoc-invalidate stgit-ewoc node))
378a003d 846 (stgit-next-patch))
6df83d42 847
9b151b27 848(defun stgit-unmark-up ()
a53347d9 849 "Remove mark from the patch on the previous line."
6df83d42 850 (interactive)
378a003d 851 (stgit-previous-patch)
8036afdd
DK
852 (let* ((node (ewoc-locate stgit-ewoc))
853 (patch (ewoc-data node)))
854 (stgit-remove-mark (stgit-patch-name patch))
855 (ewoc-invalidate stgit-ewoc node))
856 (move-to-column (stgit-goal-column)))
9b151b27
GH
857
858(defun stgit-unmark-down ()
a53347d9 859 "Remove mark from the patch on the current line."
9b151b27 860 (interactive)
8036afdd
DK
861 (let* ((node (ewoc-locate stgit-ewoc))
862 (patch (ewoc-data node)))
863 (stgit-remove-mark (stgit-patch-name patch))
864 (ewoc-invalidate stgit-ewoc node))
1288eda2 865 (stgit-next-patch))
6df83d42 866
56d81fe5 867(defun stgit-rename (name)
018fa1ac 868 "Rename the patch under point to NAME."
64ada6f5
GH
869 (interactive (list
870 (read-string "Patch name: "
871 (symbol-name (stgit-patch-name-at-point t t)))))
872 (let ((old-patchsym (stgit-patch-name-at-point t t)))
56d81fe5 873 (stgit-capture-output nil
d51722b7
GH
874 (stgit-run "rename" old-patchsym name))
875 (let ((name-sym (intern name)))
876 (when (memq old-patchsym stgit-expanded-patches)
378a003d 877 (setq stgit-expanded-patches
d51722b7
GH
878 (cons name-sym (delq old-patchsym stgit-expanded-patches))))
879 (when (memq old-patchsym stgit-marked-patches)
378a003d 880 (setq stgit-marked-patches
d51722b7
GH
881 (cons name-sym (delq old-patchsym stgit-marked-patches))))
882 (stgit-reload)
883 (stgit-goto-patch name-sym))))
56d81fe5 884
408fa7cb
GH
885(defun stgit-reload-or-repair (repair)
886 "Update the contents of the StGit buffer (`stgit-reload').
887
888With a prefix argument, repair the StGit metadata if the branch
889was modified with git commands (`stgit-repair')."
890 (interactive "P")
891 (if repair
892 (stgit-repair)
893 (stgit-reload)))
894
26201d96 895(defun stgit-repair ()
a53347d9 896 "Run stg repair."
26201d96
DK
897 (interactive)
898 (stgit-capture-output nil
b0424080 899 (stgit-run "repair"))
1f0bf00f 900 (stgit-reload))
26201d96 901
adeef6bc
GH
902(defun stgit-available-branches ()
903 "Returns a list of the available stg branches"
904 (let ((output (with-output-to-string
905 (stgit-run "branch" "--list")))
906 (start 0)
907 result)
908 (while (string-match "^>?\\s-+s\\s-+\\(\\S-+\\)" output start)
909 (setq result (cons (match-string 1 output) result))
910 (setq start (match-end 0)))
911 result))
912
913(defun stgit-branch (branch)
914 "Switch to branch BRANCH."
915 (interactive (list (completing-read "Switch to branch: "
916 (stgit-available-branches))))
917 (stgit-capture-output nil (stgit-run "branch" "--" branch))
918 (stgit-reload))
919
41c1c59c
GH
920(defun stgit-commit (count)
921 "Run stg commit on COUNT commits.
e552cb5f
GH
922Interactively, the prefix argument is used as COUNT.
923A negative COUNT will uncommit instead."
41c1c59c 924 (interactive "p")
e552cb5f
GH
925 (if (< count 0)
926 (stgit-uncommit (- count))
927 (stgit-capture-output nil (stgit-run "commit" "-n" count))
928 (stgit-reload)))
929
930(defun stgit-uncommit (count)
931 "Run stg uncommit on COUNT commits.
932Interactively, the prefix argument is used as COUNT.
933A negative COUNT will commit instead."
934 (interactive "p")
935 (if (< count 0)
936 (stgit-commit (- count))
937 (stgit-capture-output nil (stgit-run "uncommit" "-n" count))
938 (stgit-reload)))
c4aad9a7 939
3959a095
GH
940(defun stgit-revert-file ()
941 "Revert the file at point, which must be in the index or the
942working tree."
943 (interactive)
944 (let* ((patched-file (or (stgit-patched-file-at-point)
945 (error "No file on the current line")))
946 (patch-name (stgit-patch-name-at-point))
947 (file-status (stgit-file-status patched-file))
948 (rm-file (cond ((stgit-file-copy-or-rename patched-file)
949 (stgit-file-cr-to patched-file))
950 ((eq file-status 'add)
951 (stgit-file-file patched-file))))
952 (co-file (cond ((eq file-status 'rename)
953 (stgit-file-cr-from patched-file))
954 ((not (memq file-status '(copy add)))
955 (stgit-file-file patched-file)))))
956
957 (unless (memq patch-name '(:work :index))
958 (error "No index or working tree file on this line"))
959
d9473917
GH
960 (when (eq file-status 'ignore)
961 (error "Cannot revert ignored files"))
962
963 (when (eq file-status 'unknown)
964 (error "Cannot revert unknown files"))
965
3959a095
GH
966 (let ((nfiles (+ (if rm-file 1 0) (if co-file 1 0))))
967 (when (yes-or-no-p (format "Revert %d file%s? "
968 nfiles
969 (if (= nfiles 1) "" "s")))
970 (stgit-capture-output nil
971 (when rm-file
972 (stgit-run-git "rm" "-f" "-q" "--" rm-file))
973 (when co-file
974 (stgit-run-git "checkout" "HEAD" co-file)))
975 (stgit-reload)))))
976
51783171
GH
977(defun stgit-resolve-file ()
978 "Resolve conflict in the file at point."
979 (interactive)
980 (let* ((patched-file (stgit-patched-file-at-point))
981 (patch (stgit-patch-at-point))
982 (patch-name (and patch (stgit-patch-name patch)))
983 (status (and patched-file (stgit-file-status patched-file))))
984
985 (unless (memq patch-name '(:work :index))
986 (error "No index or working tree file on this line"))
987
988 (unless (eq status 'unmerged)
989 (error "No conflict to resolve at the current line"))
990
991 (stgit-capture-output nil
992 (stgit-move-change-to-index (stgit-file-file patched-file)))
993
994 (stgit-reload)))
995
0b661144
DK
996(defun stgit-push-next (npatches)
997 "Push the first unapplied patch.
998With numeric prefix argument, push that many patches."
999 (interactive "p")
d51722b7 1000 (stgit-capture-output nil (stgit-run "push" "-n" npatches))
074a4fb0
GH
1001 (stgit-reload)
1002 (stgit-refresh-git-status))
56d81fe5 1003
0b661144
DK
1004(defun stgit-pop-next (npatches)
1005 "Pop the topmost applied patch.
1006With numeric prefix argument, pop that many patches."
1007 (interactive "p")
d51722b7 1008 (stgit-capture-output nil (stgit-run "pop" "-n" npatches))
074a4fb0
GH
1009 (stgit-reload)
1010 (stgit-refresh-git-status))
56d81fe5 1011
f9182fca
KH
1012(defun stgit-applied-at-point ()
1013 "Is the patch on the current line applied?"
1014 (save-excursion
1015 (beginning-of-line)
1016 (looking-at "[>+]")))
1017
1018(defun stgit-push-or-pop ()
a53347d9 1019 "Push or pop the patch on the current line."
f9182fca 1020 (interactive)
2c862b07 1021 (let ((patchsym (stgit-patch-name-at-point t))
f9182fca
KH
1022 (applied (stgit-applied-at-point)))
1023 (stgit-capture-output nil
d51722b7 1024 (stgit-run (if applied "pop" "push") patchsym))
1f0bf00f 1025 (stgit-reload)))
f9182fca 1026
c7adf5ef 1027(defun stgit-goto ()
a53347d9 1028 "Go to the patch on the current line."
c7adf5ef 1029 (interactive)
2c862b07 1030 (let ((patchsym (stgit-patch-name-at-point t)))
c7adf5ef 1031 (stgit-capture-output nil
d51722b7 1032 (stgit-run "goto" patchsym))
1f0bf00f 1033 (stgit-reload)))
c7adf5ef 1034
d51722b7 1035(defun stgit-id (patchsym)
50d88c67
DK
1036 "Return the git commit id for PATCHSYM.
1037If PATCHSYM is a keyword, returns PATCHSYM unmodified."
1038 (if (keywordp patchsym)
1039 patchsym
1040 (let ((result (with-output-to-string
1041 (stgit-run-silent "id" patchsym))))
1042 (unless (string-match "^\\([0-9A-Fa-f]\\{40\\}\\)$" result)
1043 (error "Cannot find commit id for %s" patchsym))
1044 (match-string 1 result))))
378a003d 1045
1aece5c0 1046(defun stgit-show-patch (unmerged-stage ignore-whitespace)
d9b954c7
GH
1047 "Show the patch on the current line.
1048
1049UNMERGED-STAGE is the argument to `git-diff' that that selects
1050which stage to diff against in the case of unmerged files."
1aece5c0
GH
1051 (let ((space-arg (when (numberp ignore-whitespace)
1052 (cond ((> ignore-whitespace 4)
1053 "--ignore-all-space")
1054 ((> ignore-whitespace 1)
1055 "--ignore-space-change"))))
1056 (patch-name (stgit-patch-name-at-point t)))
1057 (stgit-capture-output "*StGit patch*"
1058 (case (get-text-property (point) 'entry-type)
1059 ('file
1060 (let* ((patched-file (stgit-patched-file-at-point))
1061 (patch-id (let ((id (stgit-id patch-name)))
1062 (if (and (eq id :index)
1063 (eq (stgit-file-status patched-file)
1064 'unmerged))
1065 :work
1066 id)))
1067 (args (append (and space-arg (list space-arg))
1068 (and (stgit-file-cr-from patched-file)
1069 (list (stgit-find-copies-harder-diff-arg)))
1070 (cond ((eq patch-id :index)
1071 '("--cached"))
1072 ((eq patch-id :work)
1073 (list unmerged-stage))
1074 (t
1075 (list (concat patch-id "^") patch-id)))
1076 '("--")
3164eec6
DK
1077 (if (stgit-file-copy-or-rename patched-file)
1078 (list (stgit-file-cr-from patched-file)
1079 (stgit-file-cr-to patched-file))
1080 (list (stgit-file-file patched-file))))))
1aece5c0
GH
1081 (apply 'stgit-run-git "diff" args)))
1082 ('patch
1083 (let* ((patch-id (stgit-id patch-name)))
1084 (if (or (eq patch-id :index) (eq patch-id :work))
1085 (apply 'stgit-run-git "diff"
1086 (stgit-find-copies-harder-diff-arg)
1087 (append (and space-arg (list space-arg))
1088 (if (eq patch-id :index)
1089 '("--cached")
1090 (list unmerged-stage))))
1091 (let ((args (append '("show" "-O" "--patch-with-stat" "-O" "-M")
1092 (and space-arg (list "-O" space-arg))
1093 (list (stgit-patch-name-at-point)))))
1094 (apply 'stgit-run args)))))
1095 (t
1096 (error "No patch or file at point")))
1097 (with-current-buffer standard-output
1098 (goto-char (point-min))
1099 (diff-mode)))))
1100
1101(defmacro stgit-define-diff (name diff-arg &optional unmerged-action)
1102 `(defun ,name (&optional ignore-whitespace)
1103 ,(format "Show the patch on the current line.
1104
1105%sWith a prefix argument, ignore whitespace. With a prefix argument
1106greater than four (e.g., \\[universal-argument] \
1107\\[universal-argument] \\[%s]), ignore all whitespace."
1108 (if unmerged-action
1109 (format "For unmerged files, %s.\n\n" unmerged-action)
1110 "")
1111 name)
1112 (interactive "p")
1113 (stgit-show-patch ,diff-arg ignore-whitespace)))
1114
1115(stgit-define-diff stgit-diff
1116 "--ours" nil)
1117(stgit-define-diff stgit-diff-ours
1118 "--ours"
1119 "diff against our branch")
1120(stgit-define-diff stgit-diff-theirs
1121 "--theirs"
1122 "diff against their branch")
1123(stgit-define-diff stgit-diff-base
1124 "--base"
1125 "diff against the merge base")
1126(stgit-define-diff stgit-diff-combined
1127 "--cc"
1128 "show a combined diff")
d9b954c7 1129
fd9fe574 1130(defun stgit-move-change-to-index (file)
37cb5766 1131 "Copies the workspace state of FILE to index, using git add or git rm"
306b37a6
GH
1132 (let ((op (if (or (file-exists-p file) (file-symlink-p file))
1133 '("add") '("rm" "-q"))))
37cb5766 1134 (stgit-capture-output "*git output*"
5115dea0 1135 (apply 'stgit-run-git (append op '("--") (list file))))))
37cb5766 1136
fd9fe574 1137(defun stgit-remove-change-from-index (file)
37cb5766
DK
1138 "Unstages the change in FILE from the index"
1139 (stgit-capture-output "*git output*"
1140 (stgit-run-git "reset" "-q" "--" file)))
1141
1142(defun stgit-file-toggle-index ()
a9089e68
GH
1143 "Move modified file in or out of the index.
1144
1145Leaves the point where it is, but moves the mark to where the
1146file ended up. You can then jump to the file with \
1147\\[exchange-point-and-mark]."
37cb5766
DK
1148 (interactive)
1149 (let ((patched-file (stgit-patched-file-at-point)))
1150 (unless patched-file
1151 (error "No file on the current line"))
51783171
GH
1152 (when (eq (stgit-file-status patched-file) 'unmerged)
1153 (error (substitute-command-keys "Use \\[stgit-resolve-file] to move an unmerged file to the index")))
d9473917
GH
1154 (when (eq (stgit-file-status patched-file) 'ignore)
1155 (error "You cannot add ignored files to the index"))
a9089e68
GH
1156 (let* ((patch (stgit-patch-at-point))
1157 (patch-name (stgit-patch-name patch))
1158 (old-point (point))
1159 next-file)
1160
1161 ;; find the next file in the patch, or the previous one if this
1162 ;; was the last file
1163 (and (zerop (forward-line 1))
1164 (let ((f (stgit-patched-file-at-point)))
1165 (and f (setq next-file (stgit-file-file f)))))
1166 (goto-char old-point)
1167 (unless next-file
1168 (and (zerop (forward-line -1))
1169 (let ((f (stgit-patched-file-at-point)))
1170 (and f (setq next-file (stgit-file-file f)))))
1171 (goto-char old-point))
1172
37cb5766 1173 (cond ((eq patch-name :work)
fd9fe574 1174 (stgit-move-change-to-index (stgit-file-file patched-file)))
37cb5766 1175 ((eq patch-name :index)
fd9fe574 1176 (stgit-remove-change-from-index (stgit-file-file patched-file)))
37cb5766 1177 (t
a9089e68
GH
1178 (error "Can only move files in the working tree to index")))
1179 (stgit-refresh-worktree)
1180 (stgit-refresh-index)
1181 (stgit-goto-patch (if (eq patch-name :index) :work :index)
1182 (stgit-file-file patched-file))
1183 (push-mark nil t t)
1184 (stgit-goto-patch patch-name next-file))))
37cb5766 1185
0bca35c8 1186(defun stgit-edit ()
a53347d9 1187 "Edit the patch on the current line."
0bca35c8 1188 (interactive)
64ada6f5 1189 (let ((patchsym (stgit-patch-name-at-point t t))
0780be79 1190 (edit-buf (get-buffer-create "*StGit edit*"))
0bca35c8
DK
1191 (dir default-directory))
1192 (log-edit 'stgit-confirm-edit t nil edit-buf)
d51722b7 1193 (set (make-local-variable 'stgit-edit-patchsym) patchsym)
0bca35c8
DK
1194 (setq default-directory dir)
1195 (let ((standard-output edit-buf))
d51722b7 1196 (stgit-run-silent "edit" "--save-template=-" patchsym))))
0bca35c8
DK
1197
1198(defun stgit-confirm-edit ()
1199 (interactive)
1200 (let ((file (make-temp-file "stgit-edit-")))
1201 (write-region (point-min) (point-max) file)
1202 (stgit-capture-output nil
d51722b7 1203 (stgit-run "edit" "-f" file stgit-edit-patchsym))
0bca35c8 1204 (with-current-buffer log-edit-parent-buffer
1f0bf00f 1205 (stgit-reload))))
0bca35c8 1206
aa04f831
GH
1207(defun stgit-new (add-sign)
1208 "Create a new patch.
1209With a prefix argument, include a \"Signed-off-by:\" line at the
1210end of the patch."
1211 (interactive "P")
c5d45b92
GH
1212 (let ((edit-buf (get-buffer-create "*StGit edit*"))
1213 (dir default-directory))
1214 (log-edit 'stgit-confirm-new t nil edit-buf)
aa04f831
GH
1215 (setq default-directory dir)
1216 (when add-sign
1217 (save-excursion
1218 (let ((standard-output (current-buffer)))
1219 (stgit-run-silent "new" "--sign" "--save-template=-"))))))
64c097a0
DK
1220
1221(defun stgit-confirm-new ()
1222 (interactive)
27b0f9e4 1223 (let ((file (make-temp-file "stgit-edit-")))
64c097a0
DK
1224 (write-region (point-min) (point-max) file)
1225 (stgit-capture-output nil
27b0f9e4 1226 (stgit-run "new" "-f" file))
64c097a0 1227 (with-current-buffer log-edit-parent-buffer
1f0bf00f 1228 (stgit-reload))))
64c097a0
DK
1229
1230(defun stgit-create-patch-name (description)
1231 "Create a patch name from a long description"
1232 (let ((patch ""))
1233 (while (> (length description) 0)
1234 (cond ((string-match "\\`[a-zA-Z_-]+" description)
8439f657
GH
1235 (setq patch (downcase (concat patch
1236 (match-string 0 description))))
64c097a0
DK
1237 (setq description (substring description (match-end 0))))
1238 ((string-match "\\` +" description)
1239 (setq patch (concat patch "-"))
1240 (setq description (substring description (match-end 0))))
1241 ((string-match "\\`[^a-zA-Z_-]+" description)
1242 (setq description (substring description (match-end 0))))))
1243 (cond ((= (length patch) 0)
1244 "patch")
1245 ((> (length patch) 20)
1246 (substring patch 0 20))
1247 (t patch))))
0bca35c8 1248
9008e45b 1249(defun stgit-delete (patchsyms &optional spill-p)
d51722b7 1250 "Delete the patches in PATCHSYMS.
9008e45b
GH
1251Interactively, delete the marked patches, or the patch at point.
1252
1253With a prefix argument, or SPILL-P, spill the patch contents to
1254the work tree and index."
1255 (interactive (list (stgit-patches-marked-or-at-point)
1256 current-prefix-arg))
e7231e4f
GH
1257 (unless patchsyms
1258 (error "No patches to delete"))
64ada6f5
GH
1259 (when (memq :index patchsyms)
1260 (error "Cannot delete the index"))
1261 (when (memq :work patchsyms)
1262 (error "Cannot delete the work tree"))
1263
d51722b7 1264 (let ((npatches (length patchsyms)))
9008e45b 1265 (when (yes-or-no-p (format "Really delete %d patch%s%s? "
e7231e4f 1266 npatches
9008e45b
GH
1267 (if (= 1 npatches) "" "es")
1268 (if spill-p
1269 " (spilling contents to index)"
1270 "")))
1271 (let ((args (if spill-p
1272 (cons "--spill" patchsyms)
1273 patchsyms)))
1274 (stgit-capture-output nil
1275 (apply 'stgit-run "delete" args))
1276 (stgit-reload)))))
d51722b7 1277
7cc45294
GH
1278(defun stgit-move-patches-target ()
1279 "Return the patchsym indicating a target patch for
1280`stgit-move-patches'.
1281
1282This is either the patch at point, or one of :top and :bottom, if
1283the point is after or before the applied patches."
1284
2c862b07 1285 (let ((patchsym (stgit-patch-name-at-point)))
7cc45294
GH
1286 (cond (patchsym patchsym)
1287 ((save-excursion (re-search-backward "^>" nil t)) :top)
1288 (t :bottom))))
1289
95369f6c
GH
1290(defun stgit-sort-patches (patchsyms)
1291 "Returns the list of patches in PATCHSYMS sorted according to
1292their position in the patch series, bottommost first.
1293
1294PATCHSYMS may not contain duplicate entries."
1295 (let (sorted-patchsyms
1296 (series (with-output-to-string
1297 (with-current-buffer standard-output
1298 (stgit-run-silent "series" "--noprefix"))))
1299 start)
1300 (while (string-match "^\\(.+\\)" series start)
1301 (let ((patchsym (intern (match-string 1 series))))
1302 (when (memq patchsym patchsyms)
1303 (setq sorted-patchsyms (cons patchsym sorted-patchsyms))))
1304 (setq start (match-end 0)))
1305 (setq sorted-patchsyms (nreverse sorted-patchsyms))
1306
1307 (unless (= (length patchsyms) (length sorted-patchsyms))
1308 (error "Internal error"))
1309
1310 sorted-patchsyms))
1311
7cc45294
GH
1312(defun stgit-move-patches (patchsyms target-patch)
1313 "Move the patches in PATCHSYMS to below TARGET-PATCH.
1314If TARGET-PATCH is :bottom or :top, move the patches to the
1315bottom or top of the stack, respectively.
1316
1317Interactively, move the marked patches to where the point is."
1318 (interactive (list stgit-marked-patches
1319 (stgit-move-patches-target)))
1320 (unless patchsyms
1321 (error "Need at least one patch to move"))
1322
1323 (unless target-patch
1324 (error "Point not at a patch"))
1325
1326 (if (eq target-patch :top)
1327 (stgit-capture-output nil
1328 (apply 'stgit-run "float" patchsyms))
1329
1330 ;; need to have patchsyms sorted by position in the stack
95369f6c 1331 (let ((sorted-patchsyms (stgit-sort-patches patchsyms)))
7cc45294
GH
1332 (while sorted-patchsyms
1333 (setq sorted-patchsyms
1334 (and (stgit-capture-output nil
1335 (if (eq target-patch :bottom)
1336 (stgit-run "sink" "--" (car sorted-patchsyms))
1337 (stgit-run "sink" "--to" target-patch "--"
1338 (car sorted-patchsyms))))
1339 (cdr sorted-patchsyms))))))
1340 (stgit-reload))
1341
594aa463
KH
1342(defun stgit-squash (patchsyms)
1343 "Squash the patches in PATCHSYMS.
693d179b
GH
1344Interactively, squash the marked patches.
1345
1346Unless there are any conflicts, the patches will be merged into
1347one patch, which will occupy the same spot in the series as the
1348deepest patch had before the squash."
d51722b7
GH
1349 (interactive (list stgit-marked-patches))
1350 (when (< (length patchsyms) 2)
594aa463 1351 (error "Need at least two patches to squash"))
32d7545d
GH
1352 (let ((stgit-buffer (current-buffer))
1353 (edit-buf (get-buffer-create "*StGit edit*"))
693d179b
GH
1354 (dir default-directory)
1355 (sorted-patchsyms (stgit-sort-patches patchsyms)))
594aa463 1356 (log-edit 'stgit-confirm-squash t nil edit-buf)
693d179b 1357 (set (make-local-variable 'stgit-patchsyms) sorted-patchsyms)
ea0def18 1358 (setq default-directory dir)
32d7545d
GH
1359 (let ((result (let ((standard-output edit-buf))
1360 (apply 'stgit-run-silent "squash"
1361 "--save-template=-" sorted-patchsyms))))
1362
1363 ;; stg squash may have reordered the patches or caused conflicts
1364 (with-current-buffer stgit-buffer
1365 (stgit-reload))
1366
1367 (unless (eq 0 result)
1368 (fundamental-mode)
1369 (rename-buffer "*StGit error*")
1370 (resize-temp-buffer-window)
1371 (switch-to-buffer-other-window stgit-buffer)
1372 (error "stg squash failed")))))
ea0def18 1373
594aa463 1374(defun stgit-confirm-squash ()
ea0def18
DK
1375 (interactive)
1376 (let ((file (make-temp-file "stgit-edit-")))
1377 (write-region (point-min) (point-max) file)
1378 (stgit-capture-output nil
594aa463 1379 (apply 'stgit-run "squash" "-f" file stgit-patchsyms))
ea0def18 1380 (with-current-buffer log-edit-parent-buffer
e6b1fdae
DK
1381 (stgit-clear-marks)
1382 ;; Go to first marked patch and stay there
1383 (goto-char (point-min))
1384 (re-search-forward (concat "^[>+-]\\*") nil t)
1385 (move-to-column goal-column)
1386 (let ((pos (point)))
1f0bf00f 1387 (stgit-reload)
e6b1fdae 1388 (goto-char pos)))))
ea0def18 1389
0663524d
KH
1390(defun stgit-help ()
1391 "Display help for the StGit mode."
1392 (interactive)
1393 (describe-function 'stgit-mode))
3a59f3db 1394
83e51dbf
DK
1395(defun stgit-undo (&optional arg)
1396 "Run stg undo.
b8463f1d
GH
1397With prefix argument, run it with the --hard flag.
1398
1399See also `stgit-redo'."
83e51dbf
DK
1400 (interactive "P")
1401 (stgit-capture-output nil
1402 (if arg
1403 (stgit-run "undo" "--hard")
1404 (stgit-run "undo")))
1f0bf00f 1405 (stgit-reload))
83e51dbf 1406
b8463f1d
GH
1407(defun stgit-redo (&optional arg)
1408 "Run stg redo.
1409With prefix argument, run it with the --hard flag.
1410
1411See also `stgit-undo'."
1412 (interactive "P")
1413 (stgit-capture-output nil
1414 (if arg
1415 (stgit-run "redo" "--hard")
1416 (stgit-run "redo")))
1417 (stgit-reload))
1418
4d73c4d8
DK
1419(defun stgit-refresh (&optional arg)
1420 "Run stg refresh.
36a4eacd
GH
1421If the index contains any changes, only refresh from index.
1422
a53347d9 1423With prefix argument, refresh the marked patch or the patch under point."
4d73c4d8
DK
1424 (interactive "P")
1425 (let ((patchargs (if arg
b0424080
GH
1426 (let ((patches (stgit-patches-marked-or-at-point)))
1427 (cond ((null patches)
df283a8b 1428 (error "No patch to update"))
b0424080 1429 ((> (length patches) 1)
df283a8b 1430 (error "Too many patches selected"))
b0424080
GH
1431 (t
1432 (cons "-p" patches))))
1433 nil)))
36a4eacd
GH
1434 (unless (stgit-index-empty-p)
1435 (setq patchargs (cons "--index" patchargs)))
4d73c4d8 1436 (stgit-capture-output nil
074a4fb0
GH
1437 (apply 'stgit-run "refresh" patchargs))
1438 (stgit-refresh-git-status))
4d73c4d8
DK
1439 (stgit-reload))
1440
8f702de4
GH
1441(defcustom stgit-show-worktree-mode 'center
1442 "This variable controls where the \"Index\" and \"Work tree\"
1443will be shown on in the buffer.
1444
1445It can be set to 'top (above all patches), 'center (show between
1446applied and unapplied patches), and 'bottom (below all patches).
1447
1448See also `stgit-show-worktree'."
1449 :type '(radio (const :tag "above all patches (top)" top)
1450 (const :tag "between applied and unapplied patches (center)"
1451 center)
1452 (const :tag "below all patches (bottom)" bottom))
1453 :group 'stgit)
1454
ce3b6130
DK
1455(defcustom stgit-default-show-worktree
1456 nil
1457 "Set to non-nil to by default show the working tree in a new stgit buffer.
1458
1459This value is used as the default value for `stgit-show-worktree'."
1460 :type 'boolean
1461 :group 'stgit)
1462
1463(defvar stgit-show-worktree nil
8f702de4 1464 "If nil, inhibit showing work tree and index in the stgit buffer.
ce3b6130 1465
8f702de4 1466See also `stgit-show-worktree-mode'.")
ce3b6130 1467
d9473917
GH
1468(defvar stgit-show-ignored nil
1469 "If nil, inhibit showing files ignored by git.")
1470
1471(defvar stgit-show-unknown nil
1472 "If nil, inhibit showing files not registered with git.")
1473
ce3b6130
DK
1474(defun stgit-toggle-worktree (&optional arg)
1475 "Toggle the visibility of the work tree.
1476With arg, show the work tree if arg is positive.
1477
8f702de4
GH
1478Its initial setting is controlled by `stgit-default-show-worktree'.
1479
1480`stgit-show-worktree-mode' controls where on screen the index and
1481work tree will show up."
ce3b6130
DK
1482 (interactive)
1483 (setq stgit-show-worktree
1484 (if (numberp arg)
1485 (> arg 0)
1486 (not stgit-show-worktree)))
1487 (stgit-reload))
1488
d9473917
GH
1489(defun stgit-toggle-ignored (&optional arg)
1490 "Toggle the visibility of files ignored by git in the work
1491tree. With ARG, show these files if ARG is positive.
1492
1493Use \\[stgit-toggle-worktree] to show the work tree."
1494 (interactive)
1495 (setq stgit-show-ignored
1496 (if (numberp arg)
1497 (> arg 0)
1498 (not stgit-show-ignored)))
1499 (stgit-reload))
1500
1501(defun stgit-toggle-unknown (&optional arg)
1502 "Toggle the visibility of files not registered with git in the
1503work tree. With ARG, show these files if ARG is positive.
1504
1505Use \\[stgit-toggle-worktree] to show the work tree."
1506 (interactive)
1507 (setq stgit-show-unknown
1508 (if (numberp arg)
1509 (> arg 0)
1510 (not stgit-show-unknown)))
1511 (stgit-reload))
1512
3a59f3db 1513(provide 'stgit)