stgit.el: Set patch names to be word syntax throughout
[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
DK
720 ("D" . stgit-delete)
721 ([(control ?/)] . stgit-undo)
722 ("\C-_" . stgit-undo)
723 ("B" . stgit-branch)
724 ("t" . ,toggle-map)
d9b954c7 725 ("d" . ,diff-map)
ce3b6130 726 ("q" . stgit-quit)))))
56d81fe5
DK
727
728(defun stgit-mode ()
729 "Major mode for interacting with StGit.
730Commands:
731\\{stgit-mode-map}"
732 (kill-all-local-variables)
733 (buffer-disable-undo)
734 (setq mode-name "StGit"
735 major-mode 'stgit-mode
736 goal-column 2)
737 (use-local-map stgit-mode-map)
738 (set (make-local-variable 'list-buffers-directory) default-directory)
6df83d42 739 (set (make-local-variable 'stgit-marked-patches) nil)
6467d976 740 (set (make-local-variable 'stgit-expanded-patches) (list :work :index))
ce3b6130 741 (set (make-local-variable 'stgit-show-worktree) stgit-default-show-worktree)
2ecb05c8
GH
742 (set (make-local-variable 'stgit-index-node) nil)
743 (set (make-local-variable 'stgit-worktree-node) nil)
224ef1ec 744 (set (make-local-variable 'parse-sexp-lookup-properties) t)
2870f8b8 745 (set-variable 'truncate-lines 't)
b894e680 746 (add-hook 'after-save-hook 'stgit-update-saved-file)
56d81fe5
DK
747 (run-hooks 'stgit-mode-hook))
748
b894e680
DK
749(defun stgit-update-saved-file ()
750 (let* ((file (expand-file-name buffer-file-name))
751 (dir (file-name-directory file))
752 (gitdir (condition-case nil (git-get-top-dir dir)
753 (error nil)))
754 (buffer (and gitdir (stgit-find-buffer gitdir))))
755 (when buffer
756 (with-current-buffer buffer
210a2a52 757 (stgit-refresh-worktree)))))
b894e680 758
d51722b7
GH
759(defun stgit-add-mark (patchsym)
760 "Mark the patch PATCHSYM."
8036afdd 761 (setq stgit-marked-patches (cons patchsym stgit-marked-patches)))
6df83d42 762
d51722b7
GH
763(defun stgit-remove-mark (patchsym)
764 "Unmark the patch PATCHSYM."
8036afdd 765 (setq stgit-marked-patches (delq patchsym stgit-marked-patches)))
6df83d42 766
e6b1fdae 767(defun stgit-clear-marks ()
47271f41 768 "Unmark all patches."
e6b1fdae
DK
769 (setq stgit-marked-patches '()))
770
735cb7ec 771(defun stgit-patch-at-point (&optional cause-error)
2c862b07
DK
772 (get-text-property (point) 'patch-data))
773
64ada6f5 774(defun stgit-patch-name-at-point (&optional cause-error only-patches)
d51722b7 775 "Return the patch name on the current line as a symbol.
64ada6f5
GH
776If CAUSE-ERROR is not nil, signal an error if none found.
777If ONLY-PATCHES is not nil, only allow real patches, and not
778index or work tree."
2c862b07 779 (let ((patch (stgit-patch-at-point)))
64ada6f5
GH
780 (and patch
781 only-patches
782 (memq (stgit-patch-status patch) '(work index))
783 (setq patch nil))
2c862b07
DK
784 (cond (patch
785 (stgit-patch-name patch))
786 (cause-error
787 (error "No patch on this line")))))
378a003d 788
3164eec6
DK
789(defun stgit-patched-file-at-point ()
790 (get-text-property (point) 'file-data))
56d81fe5 791
7755d7f1 792(defun stgit-patches-marked-or-at-point ()
d51722b7 793 "Return the symbols of the marked patches, or the patch on the current line."
7755d7f1 794 (if stgit-marked-patches
d51722b7 795 stgit-marked-patches
2c862b07 796 (let ((patch (stgit-patch-name-at-point)))
7755d7f1
KH
797 (if patch
798 (list patch)
799 '()))))
800
a9089e68 801(defun stgit-goto-patch (patchsym &optional file)
d51722b7 802 "Move point to the line containing patch PATCHSYM.
a9089e68
GH
803If that patch cannot be found, do nothing.
804
805If the patch was found and FILE is not nil, instead move to that
806file's line. If FILE cannot be found, stay on the line of
807PATCHSYM."
f9b82d36
DK
808 (let ((node (ewoc-nth stgit-ewoc 0)))
809 (while (and node (not (eq (stgit-patch-name (ewoc-data node))
810 patchsym)))
811 (setq node (ewoc-next stgit-ewoc node)))
a9089e68
GH
812 (when (and node file)
813 (let* ((file-ewoc (stgit-patch-files-ewoc (ewoc-data node)))
814 (file-node (ewoc-nth file-ewoc 0)))
815 (while (and file-node (not (equal (stgit-file-file (ewoc-data file-node)) file)))
816 (setq file-node (ewoc-next file-ewoc file-node)))
817 (when file-node
818 (ewoc-goto-node file-ewoc file-node)
819 (move-to-column (stgit-goal-column))
820 (setq node nil))))
f9b82d36
DK
821 (when node
822 (ewoc-goto-node stgit-ewoc node)
d51722b7 823 (move-to-column goal-column))))
56d81fe5 824
1c2426dc 825(defun stgit-init ()
a53347d9 826 "Run stg init."
1c2426dc
DK
827 (interactive)
828 (stgit-capture-output nil
b0424080 829 (stgit-run "init"))
1f0bf00f 830 (stgit-reload))
1c2426dc 831
6df83d42 832(defun stgit-mark ()
a53347d9 833 "Mark the patch under point."
6df83d42 834 (interactive)
8036afdd 835 (let* ((node (ewoc-locate stgit-ewoc))
64ada6f5
GH
836 (patch (ewoc-data node))
837 (name (stgit-patch-name patch)))
838 (when (eq name :work)
839 (error "Cannot mark the work tree"))
840 (when (eq name :index)
841 (error "Cannot mark the index"))
8036afdd
DK
842 (stgit-add-mark (stgit-patch-name patch))
843 (ewoc-invalidate stgit-ewoc node))
378a003d 844 (stgit-next-patch))
6df83d42 845
9b151b27 846(defun stgit-unmark-up ()
a53347d9 847 "Remove mark from the patch on the previous line."
6df83d42 848 (interactive)
378a003d 849 (stgit-previous-patch)
8036afdd
DK
850 (let* ((node (ewoc-locate stgit-ewoc))
851 (patch (ewoc-data node)))
852 (stgit-remove-mark (stgit-patch-name patch))
853 (ewoc-invalidate stgit-ewoc node))
854 (move-to-column (stgit-goal-column)))
9b151b27
GH
855
856(defun stgit-unmark-down ()
a53347d9 857 "Remove mark from the patch on the current line."
9b151b27 858 (interactive)
8036afdd
DK
859 (let* ((node (ewoc-locate stgit-ewoc))
860 (patch (ewoc-data node)))
861 (stgit-remove-mark (stgit-patch-name patch))
862 (ewoc-invalidate stgit-ewoc node))
1288eda2 863 (stgit-next-patch))
6df83d42 864
56d81fe5 865(defun stgit-rename (name)
018fa1ac 866 "Rename the patch under point to NAME."
64ada6f5
GH
867 (interactive (list
868 (read-string "Patch name: "
869 (symbol-name (stgit-patch-name-at-point t t)))))
870 (let ((old-patchsym (stgit-patch-name-at-point t t)))
56d81fe5 871 (stgit-capture-output nil
d51722b7
GH
872 (stgit-run "rename" old-patchsym name))
873 (let ((name-sym (intern name)))
874 (when (memq old-patchsym stgit-expanded-patches)
378a003d 875 (setq stgit-expanded-patches
d51722b7
GH
876 (cons name-sym (delq old-patchsym stgit-expanded-patches))))
877 (when (memq old-patchsym stgit-marked-patches)
378a003d 878 (setq stgit-marked-patches
d51722b7
GH
879 (cons name-sym (delq old-patchsym stgit-marked-patches))))
880 (stgit-reload)
881 (stgit-goto-patch name-sym))))
56d81fe5 882
408fa7cb
GH
883(defun stgit-reload-or-repair (repair)
884 "Update the contents of the StGit buffer (`stgit-reload').
885
886With a prefix argument, repair the StGit metadata if the branch
887was modified with git commands (`stgit-repair')."
888 (interactive "P")
889 (if repair
890 (stgit-repair)
891 (stgit-reload)))
892
26201d96 893(defun stgit-repair ()
a53347d9 894 "Run stg repair."
26201d96
DK
895 (interactive)
896 (stgit-capture-output nil
b0424080 897 (stgit-run "repair"))
1f0bf00f 898 (stgit-reload))
26201d96 899
adeef6bc
GH
900(defun stgit-available-branches ()
901 "Returns a list of the available stg branches"
902 (let ((output (with-output-to-string
903 (stgit-run "branch" "--list")))
904 (start 0)
905 result)
906 (while (string-match "^>?\\s-+s\\s-+\\(\\S-+\\)" output start)
907 (setq result (cons (match-string 1 output) result))
908 (setq start (match-end 0)))
909 result))
910
911(defun stgit-branch (branch)
912 "Switch to branch BRANCH."
913 (interactive (list (completing-read "Switch to branch: "
914 (stgit-available-branches))))
915 (stgit-capture-output nil (stgit-run "branch" "--" branch))
916 (stgit-reload))
917
41c1c59c
GH
918(defun stgit-commit (count)
919 "Run stg commit on COUNT commits.
e552cb5f
GH
920Interactively, the prefix argument is used as COUNT.
921A negative COUNT will uncommit instead."
41c1c59c 922 (interactive "p")
e552cb5f
GH
923 (if (< count 0)
924 (stgit-uncommit (- count))
925 (stgit-capture-output nil (stgit-run "commit" "-n" count))
926 (stgit-reload)))
927
928(defun stgit-uncommit (count)
929 "Run stg uncommit on COUNT commits.
930Interactively, the prefix argument is used as COUNT.
931A negative COUNT will commit instead."
932 (interactive "p")
933 (if (< count 0)
934 (stgit-commit (- count))
935 (stgit-capture-output nil (stgit-run "uncommit" "-n" count))
936 (stgit-reload)))
c4aad9a7 937
3959a095
GH
938(defun stgit-revert-file ()
939 "Revert the file at point, which must be in the index or the
940working tree."
941 (interactive)
942 (let* ((patched-file (or (stgit-patched-file-at-point)
943 (error "No file on the current line")))
944 (patch-name (stgit-patch-name-at-point))
945 (file-status (stgit-file-status patched-file))
946 (rm-file (cond ((stgit-file-copy-or-rename patched-file)
947 (stgit-file-cr-to patched-file))
948 ((eq file-status 'add)
949 (stgit-file-file patched-file))))
950 (co-file (cond ((eq file-status 'rename)
951 (stgit-file-cr-from patched-file))
952 ((not (memq file-status '(copy add)))
953 (stgit-file-file patched-file)))))
954
955 (unless (memq patch-name '(:work :index))
956 (error "No index or working tree file on this line"))
957
d9473917
GH
958 (when (eq file-status 'ignore)
959 (error "Cannot revert ignored files"))
960
961 (when (eq file-status 'unknown)
962 (error "Cannot revert unknown files"))
963
3959a095
GH
964 (let ((nfiles (+ (if rm-file 1 0) (if co-file 1 0))))
965 (when (yes-or-no-p (format "Revert %d file%s? "
966 nfiles
967 (if (= nfiles 1) "" "s")))
968 (stgit-capture-output nil
969 (when rm-file
970 (stgit-run-git "rm" "-f" "-q" "--" rm-file))
971 (when co-file
972 (stgit-run-git "checkout" "HEAD" co-file)))
973 (stgit-reload)))))
974
51783171
GH
975(defun stgit-resolve-file ()
976 "Resolve conflict in the file at point."
977 (interactive)
978 (let* ((patched-file (stgit-patched-file-at-point))
979 (patch (stgit-patch-at-point))
980 (patch-name (and patch (stgit-patch-name patch)))
981 (status (and patched-file (stgit-file-status patched-file))))
982
983 (unless (memq patch-name '(:work :index))
984 (error "No index or working tree file on this line"))
985
986 (unless (eq status 'unmerged)
987 (error "No conflict to resolve at the current line"))
988
989 (stgit-capture-output nil
990 (stgit-move-change-to-index (stgit-file-file patched-file)))
991
992 (stgit-reload)))
993
0b661144
DK
994(defun stgit-push-next (npatches)
995 "Push the first unapplied patch.
996With numeric prefix argument, push that many patches."
997 (interactive "p")
d51722b7 998 (stgit-capture-output nil (stgit-run "push" "-n" npatches))
074a4fb0
GH
999 (stgit-reload)
1000 (stgit-refresh-git-status))
56d81fe5 1001
0b661144
DK
1002(defun stgit-pop-next (npatches)
1003 "Pop the topmost applied patch.
1004With numeric prefix argument, pop that many patches."
1005 (interactive "p")
d51722b7 1006 (stgit-capture-output nil (stgit-run "pop" "-n" npatches))
074a4fb0
GH
1007 (stgit-reload)
1008 (stgit-refresh-git-status))
56d81fe5 1009
f9182fca
KH
1010(defun stgit-applied-at-point ()
1011 "Is the patch on the current line applied?"
1012 (save-excursion
1013 (beginning-of-line)
1014 (looking-at "[>+]")))
1015
1016(defun stgit-push-or-pop ()
a53347d9 1017 "Push or pop the patch on the current line."
f9182fca 1018 (interactive)
2c862b07 1019 (let ((patchsym (stgit-patch-name-at-point t))
f9182fca
KH
1020 (applied (stgit-applied-at-point)))
1021 (stgit-capture-output nil
d51722b7 1022 (stgit-run (if applied "pop" "push") patchsym))
1f0bf00f 1023 (stgit-reload)))
f9182fca 1024
c7adf5ef 1025(defun stgit-goto ()
a53347d9 1026 "Go to the patch on the current line."
c7adf5ef 1027 (interactive)
2c862b07 1028 (let ((patchsym (stgit-patch-name-at-point t)))
c7adf5ef 1029 (stgit-capture-output nil
d51722b7 1030 (stgit-run "goto" patchsym))
1f0bf00f 1031 (stgit-reload)))
c7adf5ef 1032
d51722b7 1033(defun stgit-id (patchsym)
50d88c67
DK
1034 "Return the git commit id for PATCHSYM.
1035If PATCHSYM is a keyword, returns PATCHSYM unmodified."
1036 (if (keywordp patchsym)
1037 patchsym
1038 (let ((result (with-output-to-string
1039 (stgit-run-silent "id" patchsym))))
1040 (unless (string-match "^\\([0-9A-Fa-f]\\{40\\}\\)$" result)
1041 (error "Cannot find commit id for %s" patchsym))
1042 (match-string 1 result))))
378a003d 1043
1aece5c0 1044(defun stgit-show-patch (unmerged-stage ignore-whitespace)
d9b954c7
GH
1045 "Show the patch on the current line.
1046
1047UNMERGED-STAGE is the argument to `git-diff' that that selects
1048which stage to diff against in the case of unmerged files."
1aece5c0
GH
1049 (let ((space-arg (when (numberp ignore-whitespace)
1050 (cond ((> ignore-whitespace 4)
1051 "--ignore-all-space")
1052 ((> ignore-whitespace 1)
1053 "--ignore-space-change"))))
1054 (patch-name (stgit-patch-name-at-point t)))
1055 (stgit-capture-output "*StGit patch*"
1056 (case (get-text-property (point) 'entry-type)
1057 ('file
1058 (let* ((patched-file (stgit-patched-file-at-point))
1059 (patch-id (let ((id (stgit-id patch-name)))
1060 (if (and (eq id :index)
1061 (eq (stgit-file-status patched-file)
1062 'unmerged))
1063 :work
1064 id)))
1065 (args (append (and space-arg (list space-arg))
1066 (and (stgit-file-cr-from patched-file)
1067 (list (stgit-find-copies-harder-diff-arg)))
1068 (cond ((eq patch-id :index)
1069 '("--cached"))
1070 ((eq patch-id :work)
1071 (list unmerged-stage))
1072 (t
1073 (list (concat patch-id "^") patch-id)))
1074 '("--")
3164eec6
DK
1075 (if (stgit-file-copy-or-rename patched-file)
1076 (list (stgit-file-cr-from patched-file)
1077 (stgit-file-cr-to patched-file))
1078 (list (stgit-file-file patched-file))))))
1aece5c0
GH
1079 (apply 'stgit-run-git "diff" args)))
1080 ('patch
1081 (let* ((patch-id (stgit-id patch-name)))
1082 (if (or (eq patch-id :index) (eq patch-id :work))
1083 (apply 'stgit-run-git "diff"
1084 (stgit-find-copies-harder-diff-arg)
1085 (append (and space-arg (list space-arg))
1086 (if (eq patch-id :index)
1087 '("--cached")
1088 (list unmerged-stage))))
1089 (let ((args (append '("show" "-O" "--patch-with-stat" "-O" "-M")
1090 (and space-arg (list "-O" space-arg))
1091 (list (stgit-patch-name-at-point)))))
1092 (apply 'stgit-run args)))))
1093 (t
1094 (error "No patch or file at point")))
1095 (with-current-buffer standard-output
1096 (goto-char (point-min))
1097 (diff-mode)))))
1098
1099(defmacro stgit-define-diff (name diff-arg &optional unmerged-action)
1100 `(defun ,name (&optional ignore-whitespace)
1101 ,(format "Show the patch on the current line.
1102
1103%sWith a prefix argument, ignore whitespace. With a prefix argument
1104greater than four (e.g., \\[universal-argument] \
1105\\[universal-argument] \\[%s]), ignore all whitespace."
1106 (if unmerged-action
1107 (format "For unmerged files, %s.\n\n" unmerged-action)
1108 "")
1109 name)
1110 (interactive "p")
1111 (stgit-show-patch ,diff-arg ignore-whitespace)))
1112
1113(stgit-define-diff stgit-diff
1114 "--ours" nil)
1115(stgit-define-diff stgit-diff-ours
1116 "--ours"
1117 "diff against our branch")
1118(stgit-define-diff stgit-diff-theirs
1119 "--theirs"
1120 "diff against their branch")
1121(stgit-define-diff stgit-diff-base
1122 "--base"
1123 "diff against the merge base")
1124(stgit-define-diff stgit-diff-combined
1125 "--cc"
1126 "show a combined diff")
d9b954c7 1127
fd9fe574 1128(defun stgit-move-change-to-index (file)
37cb5766 1129 "Copies the workspace state of FILE to index, using git add or git rm"
306b37a6
GH
1130 (let ((op (if (or (file-exists-p file) (file-symlink-p file))
1131 '("add") '("rm" "-q"))))
37cb5766 1132 (stgit-capture-output "*git output*"
5115dea0 1133 (apply 'stgit-run-git (append op '("--") (list file))))))
37cb5766 1134
fd9fe574 1135(defun stgit-remove-change-from-index (file)
37cb5766
DK
1136 "Unstages the change in FILE from the index"
1137 (stgit-capture-output "*git output*"
1138 (stgit-run-git "reset" "-q" "--" file)))
1139
1140(defun stgit-file-toggle-index ()
a9089e68
GH
1141 "Move modified file in or out of the index.
1142
1143Leaves the point where it is, but moves the mark to where the
1144file ended up. You can then jump to the file with \
1145\\[exchange-point-and-mark]."
37cb5766
DK
1146 (interactive)
1147 (let ((patched-file (stgit-patched-file-at-point)))
1148 (unless patched-file
1149 (error "No file on the current line"))
51783171
GH
1150 (when (eq (stgit-file-status patched-file) 'unmerged)
1151 (error (substitute-command-keys "Use \\[stgit-resolve-file] to move an unmerged file to the index")))
d9473917
GH
1152 (when (eq (stgit-file-status patched-file) 'ignore)
1153 (error "You cannot add ignored files to the index"))
a9089e68
GH
1154 (let* ((patch (stgit-patch-at-point))
1155 (patch-name (stgit-patch-name patch))
1156 (old-point (point))
1157 next-file)
1158
1159 ;; find the next file in the patch, or the previous one if this
1160 ;; was the last file
1161 (and (zerop (forward-line 1))
1162 (let ((f (stgit-patched-file-at-point)))
1163 (and f (setq next-file (stgit-file-file f)))))
1164 (goto-char old-point)
1165 (unless next-file
1166 (and (zerop (forward-line -1))
1167 (let ((f (stgit-patched-file-at-point)))
1168 (and f (setq next-file (stgit-file-file f)))))
1169 (goto-char old-point))
1170
37cb5766 1171 (cond ((eq patch-name :work)
fd9fe574 1172 (stgit-move-change-to-index (stgit-file-file patched-file)))
37cb5766 1173 ((eq patch-name :index)
fd9fe574 1174 (stgit-remove-change-from-index (stgit-file-file patched-file)))
37cb5766 1175 (t
a9089e68
GH
1176 (error "Can only move files in the working tree to index")))
1177 (stgit-refresh-worktree)
1178 (stgit-refresh-index)
1179 (stgit-goto-patch (if (eq patch-name :index) :work :index)
1180 (stgit-file-file patched-file))
1181 (push-mark nil t t)
1182 (stgit-goto-patch patch-name next-file))))
37cb5766 1183
0bca35c8 1184(defun stgit-edit ()
a53347d9 1185 "Edit the patch on the current line."
0bca35c8 1186 (interactive)
64ada6f5 1187 (let ((patchsym (stgit-patch-name-at-point t t))
0780be79 1188 (edit-buf (get-buffer-create "*StGit edit*"))
0bca35c8
DK
1189 (dir default-directory))
1190 (log-edit 'stgit-confirm-edit t nil edit-buf)
d51722b7 1191 (set (make-local-variable 'stgit-edit-patchsym) patchsym)
0bca35c8
DK
1192 (setq default-directory dir)
1193 (let ((standard-output edit-buf))
d51722b7 1194 (stgit-run-silent "edit" "--save-template=-" patchsym))))
0bca35c8
DK
1195
1196(defun stgit-confirm-edit ()
1197 (interactive)
1198 (let ((file (make-temp-file "stgit-edit-")))
1199 (write-region (point-min) (point-max) file)
1200 (stgit-capture-output nil
d51722b7 1201 (stgit-run "edit" "-f" file stgit-edit-patchsym))
0bca35c8 1202 (with-current-buffer log-edit-parent-buffer
1f0bf00f 1203 (stgit-reload))))
0bca35c8 1204
aa04f831
GH
1205(defun stgit-new (add-sign)
1206 "Create a new patch.
1207With a prefix argument, include a \"Signed-off-by:\" line at the
1208end of the patch."
1209 (interactive "P")
c5d45b92
GH
1210 (let ((edit-buf (get-buffer-create "*StGit edit*"))
1211 (dir default-directory))
1212 (log-edit 'stgit-confirm-new t nil edit-buf)
aa04f831
GH
1213 (setq default-directory dir)
1214 (when add-sign
1215 (save-excursion
1216 (let ((standard-output (current-buffer)))
1217 (stgit-run-silent "new" "--sign" "--save-template=-"))))))
64c097a0
DK
1218
1219(defun stgit-confirm-new ()
1220 (interactive)
27b0f9e4 1221 (let ((file (make-temp-file "stgit-edit-")))
64c097a0
DK
1222 (write-region (point-min) (point-max) file)
1223 (stgit-capture-output nil
27b0f9e4 1224 (stgit-run "new" "-f" file))
64c097a0 1225 (with-current-buffer log-edit-parent-buffer
1f0bf00f 1226 (stgit-reload))))
64c097a0
DK
1227
1228(defun stgit-create-patch-name (description)
1229 "Create a patch name from a long description"
1230 (let ((patch ""))
1231 (while (> (length description) 0)
1232 (cond ((string-match "\\`[a-zA-Z_-]+" description)
8439f657
GH
1233 (setq patch (downcase (concat patch
1234 (match-string 0 description))))
64c097a0
DK
1235 (setq description (substring description (match-end 0))))
1236 ((string-match "\\` +" description)
1237 (setq patch (concat patch "-"))
1238 (setq description (substring description (match-end 0))))
1239 ((string-match "\\`[^a-zA-Z_-]+" description)
1240 (setq description (substring description (match-end 0))))))
1241 (cond ((= (length patch) 0)
1242 "patch")
1243 ((> (length patch) 20)
1244 (substring patch 0 20))
1245 (t patch))))
0bca35c8 1246
9008e45b 1247(defun stgit-delete (patchsyms &optional spill-p)
d51722b7 1248 "Delete the patches in PATCHSYMS.
9008e45b
GH
1249Interactively, delete the marked patches, or the patch at point.
1250
1251With a prefix argument, or SPILL-P, spill the patch contents to
1252the work tree and index."
1253 (interactive (list (stgit-patches-marked-or-at-point)
1254 current-prefix-arg))
e7231e4f
GH
1255 (unless patchsyms
1256 (error "No patches to delete"))
64ada6f5
GH
1257 (when (memq :index patchsyms)
1258 (error "Cannot delete the index"))
1259 (when (memq :work patchsyms)
1260 (error "Cannot delete the work tree"))
1261
d51722b7 1262 (let ((npatches (length patchsyms)))
9008e45b 1263 (when (yes-or-no-p (format "Really delete %d patch%s%s? "
e7231e4f 1264 npatches
9008e45b
GH
1265 (if (= 1 npatches) "" "es")
1266 (if spill-p
1267 " (spilling contents to index)"
1268 "")))
1269 (let ((args (if spill-p
1270 (cons "--spill" patchsyms)
1271 patchsyms)))
1272 (stgit-capture-output nil
1273 (apply 'stgit-run "delete" args))
1274 (stgit-reload)))))
d51722b7 1275
7cc45294
GH
1276(defun stgit-move-patches-target ()
1277 "Return the patchsym indicating a target patch for
1278`stgit-move-patches'.
1279
1280This is either the patch at point, or one of :top and :bottom, if
1281the point is after or before the applied patches."
1282
2c862b07 1283 (let ((patchsym (stgit-patch-name-at-point)))
7cc45294
GH
1284 (cond (patchsym patchsym)
1285 ((save-excursion (re-search-backward "^>" nil t)) :top)
1286 (t :bottom))))
1287
95369f6c
GH
1288(defun stgit-sort-patches (patchsyms)
1289 "Returns the list of patches in PATCHSYMS sorted according to
1290their position in the patch series, bottommost first.
1291
1292PATCHSYMS may not contain duplicate entries."
1293 (let (sorted-patchsyms
1294 (series (with-output-to-string
1295 (with-current-buffer standard-output
1296 (stgit-run-silent "series" "--noprefix"))))
1297 start)
1298 (while (string-match "^\\(.+\\)" series start)
1299 (let ((patchsym (intern (match-string 1 series))))
1300 (when (memq patchsym patchsyms)
1301 (setq sorted-patchsyms (cons patchsym sorted-patchsyms))))
1302 (setq start (match-end 0)))
1303 (setq sorted-patchsyms (nreverse sorted-patchsyms))
1304
1305 (unless (= (length patchsyms) (length sorted-patchsyms))
1306 (error "Internal error"))
1307
1308 sorted-patchsyms))
1309
7cc45294
GH
1310(defun stgit-move-patches (patchsyms target-patch)
1311 "Move the patches in PATCHSYMS to below TARGET-PATCH.
1312If TARGET-PATCH is :bottom or :top, move the patches to the
1313bottom or top of the stack, respectively.
1314
1315Interactively, move the marked patches to where the point is."
1316 (interactive (list stgit-marked-patches
1317 (stgit-move-patches-target)))
1318 (unless patchsyms
1319 (error "Need at least one patch to move"))
1320
1321 (unless target-patch
1322 (error "Point not at a patch"))
1323
1324 (if (eq target-patch :top)
1325 (stgit-capture-output nil
1326 (apply 'stgit-run "float" patchsyms))
1327
1328 ;; need to have patchsyms sorted by position in the stack
95369f6c 1329 (let ((sorted-patchsyms (stgit-sort-patches patchsyms)))
7cc45294
GH
1330 (while sorted-patchsyms
1331 (setq sorted-patchsyms
1332 (and (stgit-capture-output nil
1333 (if (eq target-patch :bottom)
1334 (stgit-run "sink" "--" (car sorted-patchsyms))
1335 (stgit-run "sink" "--to" target-patch "--"
1336 (car sorted-patchsyms))))
1337 (cdr sorted-patchsyms))))))
1338 (stgit-reload))
1339
594aa463
KH
1340(defun stgit-squash (patchsyms)
1341 "Squash the patches in PATCHSYMS.
693d179b
GH
1342Interactively, squash the marked patches.
1343
1344Unless there are any conflicts, the patches will be merged into
1345one patch, which will occupy the same spot in the series as the
1346deepest patch had before the squash."
d51722b7
GH
1347 (interactive (list stgit-marked-patches))
1348 (when (< (length patchsyms) 2)
594aa463 1349 (error "Need at least two patches to squash"))
32d7545d
GH
1350 (let ((stgit-buffer (current-buffer))
1351 (edit-buf (get-buffer-create "*StGit edit*"))
693d179b
GH
1352 (dir default-directory)
1353 (sorted-patchsyms (stgit-sort-patches patchsyms)))
594aa463 1354 (log-edit 'stgit-confirm-squash t nil edit-buf)
693d179b 1355 (set (make-local-variable 'stgit-patchsyms) sorted-patchsyms)
ea0def18 1356 (setq default-directory dir)
32d7545d
GH
1357 (let ((result (let ((standard-output edit-buf))
1358 (apply 'stgit-run-silent "squash"
1359 "--save-template=-" sorted-patchsyms))))
1360
1361 ;; stg squash may have reordered the patches or caused conflicts
1362 (with-current-buffer stgit-buffer
1363 (stgit-reload))
1364
1365 (unless (eq 0 result)
1366 (fundamental-mode)
1367 (rename-buffer "*StGit error*")
1368 (resize-temp-buffer-window)
1369 (switch-to-buffer-other-window stgit-buffer)
1370 (error "stg squash failed")))))
ea0def18 1371
594aa463 1372(defun stgit-confirm-squash ()
ea0def18
DK
1373 (interactive)
1374 (let ((file (make-temp-file "stgit-edit-")))
1375 (write-region (point-min) (point-max) file)
1376 (stgit-capture-output nil
594aa463 1377 (apply 'stgit-run "squash" "-f" file stgit-patchsyms))
ea0def18 1378 (with-current-buffer log-edit-parent-buffer
e6b1fdae
DK
1379 (stgit-clear-marks)
1380 ;; Go to first marked patch and stay there
1381 (goto-char (point-min))
1382 (re-search-forward (concat "^[>+-]\\*") nil t)
1383 (move-to-column goal-column)
1384 (let ((pos (point)))
1f0bf00f 1385 (stgit-reload)
e6b1fdae 1386 (goto-char pos)))))
ea0def18 1387
0663524d
KH
1388(defun stgit-help ()
1389 "Display help for the StGit mode."
1390 (interactive)
1391 (describe-function 'stgit-mode))
3a59f3db 1392
83e51dbf
DK
1393(defun stgit-undo (&optional arg)
1394 "Run stg undo.
1395With prefix argument, run it with the --hard flag."
1396 (interactive "P")
1397 (stgit-capture-output nil
1398 (if arg
1399 (stgit-run "undo" "--hard")
1400 (stgit-run "undo")))
1f0bf00f 1401 (stgit-reload))
83e51dbf 1402
4d73c4d8
DK
1403(defun stgit-refresh (&optional arg)
1404 "Run stg refresh.
36a4eacd
GH
1405If the index contains any changes, only refresh from index.
1406
a53347d9 1407With prefix argument, refresh the marked patch or the patch under point."
4d73c4d8
DK
1408 (interactive "P")
1409 (let ((patchargs (if arg
b0424080
GH
1410 (let ((patches (stgit-patches-marked-or-at-point)))
1411 (cond ((null patches)
df283a8b 1412 (error "No patch to update"))
b0424080 1413 ((> (length patches) 1)
df283a8b 1414 (error "Too many patches selected"))
b0424080
GH
1415 (t
1416 (cons "-p" patches))))
1417 nil)))
36a4eacd
GH
1418 (unless (stgit-index-empty-p)
1419 (setq patchargs (cons "--index" patchargs)))
4d73c4d8 1420 (stgit-capture-output nil
074a4fb0
GH
1421 (apply 'stgit-run "refresh" patchargs))
1422 (stgit-refresh-git-status))
4d73c4d8
DK
1423 (stgit-reload))
1424
8f702de4
GH
1425(defcustom stgit-show-worktree-mode 'center
1426 "This variable controls where the \"Index\" and \"Work tree\"
1427will be shown on in the buffer.
1428
1429It can be set to 'top (above all patches), 'center (show between
1430applied and unapplied patches), and 'bottom (below all patches).
1431
1432See also `stgit-show-worktree'."
1433 :type '(radio (const :tag "above all patches (top)" top)
1434 (const :tag "between applied and unapplied patches (center)"
1435 center)
1436 (const :tag "below all patches (bottom)" bottom))
1437 :group 'stgit)
1438
ce3b6130
DK
1439(defcustom stgit-default-show-worktree
1440 nil
1441 "Set to non-nil to by default show the working tree in a new stgit buffer.
1442
1443This value is used as the default value for `stgit-show-worktree'."
1444 :type 'boolean
1445 :group 'stgit)
1446
1447(defvar stgit-show-worktree nil
8f702de4 1448 "If nil, inhibit showing work tree and index in the stgit buffer.
ce3b6130 1449
8f702de4 1450See also `stgit-show-worktree-mode'.")
ce3b6130 1451
d9473917
GH
1452(defvar stgit-show-ignored nil
1453 "If nil, inhibit showing files ignored by git.")
1454
1455(defvar stgit-show-unknown nil
1456 "If nil, inhibit showing files not registered with git.")
1457
ce3b6130
DK
1458(defun stgit-toggle-worktree (&optional arg)
1459 "Toggle the visibility of the work tree.
1460With arg, show the work tree if arg is positive.
1461
8f702de4
GH
1462Its initial setting is controlled by `stgit-default-show-worktree'.
1463
1464`stgit-show-worktree-mode' controls where on screen the index and
1465work tree will show up."
ce3b6130
DK
1466 (interactive)
1467 (setq stgit-show-worktree
1468 (if (numberp arg)
1469 (> arg 0)
1470 (not stgit-show-worktree)))
1471 (stgit-reload))
1472
d9473917
GH
1473(defun stgit-toggle-ignored (&optional arg)
1474 "Toggle the visibility of files ignored by git in the work
1475tree. With ARG, show these files if ARG is positive.
1476
1477Use \\[stgit-toggle-worktree] to show the work tree."
1478 (interactive)
1479 (setq stgit-show-ignored
1480 (if (numberp arg)
1481 (> arg 0)
1482 (not stgit-show-ignored)))
1483 (stgit-reload))
1484
1485(defun stgit-toggle-unknown (&optional arg)
1486 "Toggle the visibility of files not registered with git in the
1487work tree. With ARG, show these files if ARG is positive.
1488
1489Use \\[stgit-toggle-worktree] to show the work tree."
1490 (interactive)
1491 (setq stgit-show-unknown
1492 (if (numberp arg)
1493 (> arg 0)
1494 (not stgit-show-unknown)))
1495 (stgit-reload))
1496
3a59f3db 1497(provide 'stgit)