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