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