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