stgit.el: Disable undo information in stgit output capture buffers
[stgit] / contrib / stgit.el
index ec3c1d0..829669e 100644 (file)
 (require 'cl)
 (require 'ewoc)
 (require 'easymenu)
+(require 'format-spec)
+
+(defun stgit-set-default (symbol value)
+  "Set default value of SYMBOL to VALUE using `set-default' and
+reload all StGit buffers."
+  (set-default symbol value)
+  (dolist (buf (buffer-list))
+    (with-current-buffer buf
+      (when (eq major-mode 'stgit-mode)
+        (stgit-reload)))))
+
+(defgroup stgit nil
+  "A user interface for the StGit patch maintenance tool."
+  :group 'tools
+  :link '(function-link stgit)
+  :link '(url-link "http://www.procode.org/stgit/"))
+
+(defcustom stgit-abbreviate-copies-and-renames t
+  "If non-nil, abbreviate copies and renames as \"dir/{old -> new}/file\"
+instead of \"dir/old/file -> dir/new/file\"."
+  :type 'boolean
+  :group 'stgit
+  :set 'stgit-set-default)
+
+(defcustom stgit-default-show-worktree t
+  "Set to non-nil to by default show the working tree in a new stgit buffer.
+
+Use \\<stgit-mode-map>\\[stgit-toggle-worktree] to toggle the this setting in an already-started StGit buffer."
+  :type 'boolean
+  :group 'stgit
+  :link '(variable-link stgit-show-worktree))
+
+(defcustom stgit-find-copies-harder nil
+  "Try harder to find copied files when listing patches.
+
+When not nil, runs git diff-tree with the --find-copies-harder
+flag, which reduces performance."
+  :type 'boolean
+  :group 'stgit
+  :set 'stgit-set-default)
+
+(defcustom stgit-show-worktree-mode 'center
+  "This variable controls where the \"Index\" and \"Work tree\"
+will be shown on in the buffer.
+
+It can be set to 'top (above all patches), 'center (show between
+applied and unapplied patches), and 'bottom (below all patches)."
+  :type '(radio (const :tag "above all patches (top)" top)
+                (const :tag "between applied and unapplied patches (center)"
+                       center)
+                (const :tag "below all patches (bottom)" bottom))
+  :group 'stgit
+  :link '(variable-link stgit-show-worktree)
+  :set 'stgit-set-default)
+
+(defcustom stgit-patch-line-format "%s%m%-30n %e%d"
+  "The format string used to format patch lines.
+The format string is passed to `format-spec' and the following
+format characters are recognized:
+
+  %s - A '+', '-', '>' or space, depending on whether the patch is
+       applied, unapplied, top, or something else.
+
+  %m - An asterisk if the patch is marked, and a space otherwise.
+
+  %n - The patch name.
+
+  %e - The string \"(empty) \" if the patch is empty.
+
+  %d - The short patch description.
+
+  %D - The short patch description, or the patch name.
+
+When `stgit-show-patch-names' is non-nil, the `stgit-noname-patch-line-format'
+variable is used instead."
+  :type 'string
+  :group 'stgit
+  :set 'stgit-set-default)
+
+(defcustom stgit-noname-patch-line-format "%s%m%e%D"
+  "The alternate format string used to format patch lines.
+It has the same semantics as `stgit-patch-line-format', and the
+display can be toggled between the two formats using
+\\<stgit-mode-map>>\\[stgit-toggle-patch-names].
+
+The alternate form is used when the patch name is hidden."
+  :type 'string
+  :group 'stgit
+  :set 'stgit-set-default)
+
+(defcustom stgit-default-show-patch-names t
+  "If non-nil, default to showing patch names in a new stgit buffer.
+
+Use \\<stgit-mode-map>\\[stgit-toggle-patch-names] to toggle the
+this setting in an already-started StGit buffer."
+  :type 'boolean
+  :group 'stgit
+  :link '(variable-link stgit-show-patch-names))
+
+(defcustom stgit-file-line-format "    %-11s %-2m %n   %c"
+  "The format string used to format file lines.
+The format string is passed to `format-spec' and the following
+format characters are recognized:
+
+  %s - A string describing the status of the file.
+
+  %m - Mode change information
+
+  %n - The file name.
+
+  %c - A description of file changes."
+  :type 'string
+  :group 'stgit
+  :set 'stgit-set-default)
+
+(defface stgit-branch-name-face
+  '((t :inherit bold))
+  "The face used for the StGit branch name"
+  :group 'stgit)
+
+(defface stgit-top-patch-face
+  '((((background dark)) (:weight bold :foreground "yellow"))
+    (((background light)) (:weight bold :foreground "purple"))
+    (t (:weight bold)))
+  "The face used for the top patch names"
+  :group 'stgit)
+
+(defface stgit-applied-patch-face
+  '((((background dark)) (:foreground "light yellow"))
+    (((background light)) (:foreground "purple"))
+    (t ()))
+  "The face used for applied patch names"
+  :group 'stgit)
+
+(defface stgit-unapplied-patch-face
+  '((((background dark)) (:foreground "gray80"))
+    (((background light)) (:foreground "orchid"))
+    (t ()))
+  "The face used for unapplied patch names"
+  :group 'stgit)
+
+(defface stgit-description-face
+  '((((background dark)) (:foreground "tan"))
+    (((background light)) (:foreground "dark red")))
+  "The face used for StGit descriptions"
+  :group 'stgit)
+
+(defface stgit-index-work-tree-title-face
+  '((((supports :slant italic)) :slant italic)
+    (t :inherit bold))
+  "StGit mode face used for the \"Index\" and \"Work tree\" titles"
+  :group 'stgit)
+
+(defface stgit-unmerged-file-face
+  '((((class color) (background light)) (:foreground "red" :bold t))
+    (((class color) (background dark)) (:foreground "red" :bold t)))
+  "StGit mode face used for unmerged file status"
+  :group 'stgit)
+
+(defface stgit-unknown-file-face
+  '((((class color) (background light)) (:foreground "goldenrod" :bold t))
+    (((class color) (background dark)) (:foreground "goldenrod" :bold t)))
+  "StGit mode face used for unknown file status"
+  :group 'stgit)
+
+(defface stgit-ignored-file-face
+  '((((class color) (background light)) (:foreground "grey60"))
+    (((class color) (background dark)) (:foreground "grey40")))
+  "StGit mode face used for ignored files")
+
+(defface stgit-file-permission-face
+  '((((class color) (background light)) (:foreground "green" :bold t))
+    (((class color) (background dark)) (:foreground "green" :bold t)))
+  "StGit mode face used for permission changes."
+  :group 'stgit)
+
+(defface stgit-modified-file-face
+  '((((class color) (background light)) (:foreground "purple"))
+    (((class color) (background dark)) (:foreground "salmon")))
+  "StGit mode face used for modified file status"
+  :group 'stgit)
 
 (defun stgit (dir)
   "Manage StGit patches for the tree in DIR.
@@ -73,30 +254,40 @@ directory DIR or `default-directory'"
 (defstruct (stgit-patch)
   status name desc empty files-ewoc)
 
+(defun stgit-patch-display-name (patch)
+  (let ((name (stgit-patch-name patch)))
+    (case name
+      (:index "Index")
+      (:work "Work Tree")
+      (t (symbol-name name)))))
+
 (defun stgit-patch-pp (patch)
   (let* ((status (stgit-patch-status patch))
          (start (point))
          (name (stgit-patch-name patch))
-         (face (cdr (assq status stgit-patch-status-face-alist))))
-    (insert (case status
-              ('applied "+")
-              ('top ">")
-              ('unapplied "-")
-              (t " "))
-            (if (memq name stgit-marked-patches)
-                "*" " "))
-    (if (memq status '(index work))
-        (insert (propertize (if (eq status 'index) "Index" "Work tree")
-                            'face face))
-      (insert (format "%-30s"
-                      (propertize (symbol-name name)
-                                  'face face
-                                  'syntax-table (string-to-syntax "w")))
-              "  "
-              (if (stgit-patch-empty patch) "(empty) " "")
-              (propertize (or (stgit-patch-desc patch) "")
-                          'face 'stgit-description-face)))
-    (insert "\n")
+         (face (cdr (assq status stgit-patch-status-face-alist)))
+         (fmt (if stgit-show-patch-names
+                  stgit-patch-line-format
+                stgit-noname-patch-line-format))
+         (spec (format-spec-make
+                ?s (case status
+                     ('applied "+")
+                     ('top ">")
+                     ('unapplied "-")
+                     (t " "))
+                ?m (if (memq name stgit-marked-patches)
+                       "*" " ")
+                ?n (propertize (stgit-patch-display-name patch)
+                               'face face
+                               'syntax-table (string-to-syntax "w"))
+                ?e (if (stgit-patch-empty patch) "(empty) " "")
+                ?d (propertize (or (stgit-patch-desc patch) "")
+                               'face 'stgit-description-face)
+                ?D (propertize (or (stgit-patch-desc patch)
+                                   (stgit-patch-display-name patch))
+                               'face face))))
+
+    (insert (format-spec fmt spec) "\n")
     (put-text-property start (point) 'entry-type 'patch)
     (when (memq name stgit-expanded-patches)
       (stgit-insert-patch-files patch))
@@ -125,6 +316,7 @@ Returns nil if there was no output."
          (stgit-dir default-directory)
          (inhibit-read-only t))
      (with-current-buffer output-buf
+       (buffer-disable-undo)
        (erase-buffer)
        (setq default-directory stgit-dir)
        (setq buffer-read-only t))
@@ -284,126 +476,6 @@ Returns nil if there was no output."
       (goto-line curline)))
   (stgit-refresh-git-status))
 
-(defun stgit-set-default (symbol value)
-  "Set default value of SYMBOL to VALUE using `set-default' and
-reload all StGit buffers."
-  (set-default symbol value)
-  (dolist (buf (buffer-list))
-    (with-current-buffer buf
-      (when (eq major-mode 'stgit-mode)
-        (stgit-reload)))))
-
-(defgroup stgit nil
-  "A user interface for the StGit patch maintenance tool."
-  :group 'tools
-  :link '(function-link stgit)
-  :link '(url-link "http://www.procode.org/stgit/"))
-
-(defcustom stgit-abbreviate-copies-and-renames t
-  "If non-nil, abbreviate copies and renames as \"dir/{old -> new}/file\"
-instead of \"dir/old/file -> dir/new/file\"."
-  :type 'boolean
-  :group 'stgit
-  :set 'stgit-set-default)
-
-(defcustom stgit-default-show-worktree t
-  "Set to non-nil to by default show the working tree in a new stgit buffer.
-
-Use \\<stgit-mode-map>\\[stgit-toggle-worktree] to toggle the this setting in an already-started StGit buffer."
-  :type 'boolean
-  :group 'stgit
-  :link '(variable-link stgit-show-worktree))
-
-(defcustom stgit-find-copies-harder nil
-  "Try harder to find copied files when listing patches.
-
-When not nil, runs git diff-tree with the --find-copies-harder
-flag, which reduces performance."
-  :type 'boolean
-  :group 'stgit
-  :set 'stgit-set-default)
-
-(defcustom stgit-show-worktree-mode 'center
-  "This variable controls where the \"Index\" and \"Work tree\"
-will be shown on in the buffer.
-
-It can be set to 'top (above all patches), 'center (show between
-applied and unapplied patches), and 'bottom (below all patches)."
-  :type '(radio (const :tag "above all patches (top)" top)
-                (const :tag "between applied and unapplied patches (center)"
-                       center)
-                (const :tag "below all patches (bottom)" bottom))
-  :group 'stgit
-  :link '(variable-link stgit-show-worktree)
-  :set 'stgit-set-default)
-
-(defface stgit-branch-name-face
-  '((t :inherit bold))
-  "The face used for the StGit branch name"
-  :group 'stgit)
-
-(defface stgit-top-patch-face
-  '((((background dark)) (:weight bold :foreground "yellow"))
-    (((background light)) (:weight bold :foreground "purple"))
-    (t (:weight bold)))
-  "The face used for the top patch names"
-  :group 'stgit)
-
-(defface stgit-applied-patch-face
-  '((((background dark)) (:foreground "light yellow"))
-    (((background light)) (:foreground "purple"))
-    (t ()))
-  "The face used for applied patch names"
-  :group 'stgit)
-
-(defface stgit-unapplied-patch-face
-  '((((background dark)) (:foreground "gray80"))
-    (((background light)) (:foreground "orchid"))
-    (t ()))
-  "The face used for unapplied patch names"
-  :group 'stgit)
-
-(defface stgit-description-face
-  '((((background dark)) (:foreground "tan"))
-    (((background light)) (:foreground "dark red")))
-  "The face used for StGit descriptions"
-  :group 'stgit)
-
-(defface stgit-index-work-tree-title-face
-  '((((supports :slant italic)) :slant italic)
-    (t :inherit bold))
-  "StGit mode face used for the \"Index\" and \"Work tree\" titles"
-  :group 'stgit)
-
-(defface stgit-unmerged-file-face
-  '((((class color) (background light)) (:foreground "red" :bold t))
-    (((class color) (background dark)) (:foreground "red" :bold t)))
-  "StGit mode face used for unmerged file status"
-  :group 'stgit)
-
-(defface stgit-unknown-file-face
-  '((((class color) (background light)) (:foreground "goldenrod" :bold t))
-    (((class color) (background dark)) (:foreground "goldenrod" :bold t)))
-  "StGit mode face used for unknown file status"
-  :group 'stgit)
-
-(defface stgit-ignored-file-face
-  '((((class color) (background light)) (:foreground "grey60"))
-    (((class color) (background dark)) (:foreground "grey40")))
-  "StGit mode face used for ignored files")
-
-(defface stgit-file-permission-face
-  '((((class color) (background light)) (:foreground "green" :bold t))
-    (((class color) (background dark)) (:foreground "green" :bold t)))
-  "StGit mode face used for permission changes."
-  :group 'stgit)
-
-(defface stgit-modified-file-face
-  '((((class color) (background light)) (:foreground "purple"))
-    (((class color) (background dark)) (:foreground "salmon")))
-  "StGit mode face used for modified file status"
-  :group 'stgit)
-
 (defconst stgit-file-status-code-strings
   (mapcar (lambda (arg)
             (cons (car arg)
@@ -433,12 +505,11 @@ applied and unapplied patches), and 'bottom (below all patches)."
                      stgit-file-status-code-strings))
          (score (stgit-file-cr-score file)))
     (when code
-      (format "%-11s  "
-              (if (and score (/= score 100))
-                  (format "%s %s" (cdr code)
-                          (propertize (format "%d%%" score)
-                                      'face 'stgit-description-face))
-                (cdr code))))))
+      (if (and score (/= score 100))
+          (format "%s %s" (cdr code)
+                  (propertize (format "%d%%" score)
+                              'face 'stgit-description-face))
+        (cdr code)))))
 
 (defun stgit-file-status-code (str &optional score)
   "Return stgit status code from git status string"
@@ -481,8 +552,8 @@ Cf. `stgit-file-type-string'."
           ((zerop old-type)
            (if (= new-type #o100)
                ""
-             (format "   (%s)" (stgit-file-type-string new-type))))
-          (t (format "   (%s -> %s)"
+             (format "(%s)" (stgit-file-type-string new-type))))
+          (t (format "(%s -> %s)"
                      (stgit-file-type-string old-type)
                      (stgit-file-type-string new-type))))))
 
@@ -555,23 +626,20 @@ Cf. `stgit-file-type-change-string'."
       (concat (stgit-file-cr-from file) arrow (stgit-file-cr-to file)))))
 
 (defun stgit-file-pp (file)
-  (let ((status (stgit-file-status file))
-        (name (if (stgit-file-copy-or-rename file)
-                  (stgit-describe-copy-or-rename file)
-                (stgit-file-file file)))
-        (mode-change (stgit-file-mode-change-string
-                      (stgit-file-old-perm file)
-                      (stgit-file-new-perm file)))
-        (start (point)))
-    (insert (format "    %-12s%s%s%s%s\n"
-                    (stgit-file-status-code-as-string file)
-                    mode-change
-                    (if (zerop (length mode-change)) "" " ")
-                    name
-                    (propertize (stgit-file-type-change-string
-                                 (stgit-file-old-perm file)
-                                 (stgit-file-new-perm file))
-                                'face 'stgit-description-face)))
+  (let ((start (point))
+        (spec (format-spec-make
+               ?s (stgit-file-status-code-as-string file)
+               ?m (stgit-file-mode-change-string
+                   (stgit-file-old-perm file)
+                   (stgit-file-new-perm file))
+               ?n (if (stgit-file-copy-or-rename file)
+                      (stgit-describe-copy-or-rename file)
+                    (stgit-file-file file))
+               ?c (propertize (stgit-file-type-change-string
+                               (stgit-file-old-perm file)
+                               (stgit-file-new-perm file))
+                              'face 'stgit-description-face))))
+    (insert (format-spec stgit-file-line-format spec) "\n")
     (add-text-properties start (point)
                          (list 'entry-type 'file
                                'file-data file))))
@@ -677,13 +745,13 @@ at point."
       (smerge-mode 1))))
 
 (defun stgit-expand (&optional patches collapse)
-  "Show the contents selected patches, or the patch at point.
+  "Show the contents of marked patches, or the patch at point.
 
 See also `stgit-collapse'.
 
 Non-interactively, operate on PATCHES, and collapse instead of
 expand if COLLAPSE is not nil."
-  (interactive (list (stgit-patches-marked-or-at-point)))
+  (interactive (list (stgit-patches-marked-or-at-point t)))
   (stgit-assert-mode)
   (let ((patches-diff (funcall (if collapse #'intersection #'set-difference)
                                patches stgit-expanded-patches)))
@@ -697,10 +765,10 @@ expand if COLLAPSE is not nil."
   (move-to-column (stgit-goal-column)))
 
 (defun stgit-collapse (&optional patches)
-  "Hide the contents selected patches, or the patch at point.
+  "Hide the contents of marked patches, or the patch at point.
 
 See also `stgit-expand'."
-  (interactive (list (stgit-patches-marked-or-at-point)))
+  (interactive (list (stgit-patches-marked-or-at-point t)))
   (stgit-assert-mode)
   (stgit-expand patches t))
 
@@ -807,7 +875,8 @@ file for (applied) copies and renames."
             ("t" .        stgit-diff-theirs)))
     (suppress-keymap toggle-map)
     (mapc (lambda (arg) (define-key toggle-map (car arg) (cdr arg)))
-          '(("t" .        stgit-toggle-worktree)
+          '(("n" .        stgit-toggle-patch-names)
+            ("t" .        stgit-toggle-worktree)
             ("i" .        stgit-toggle-ignored)
             ("u" .        stgit-toggle-unknown)))
     (setq stgit-mode-map (make-keymap))
@@ -904,23 +973,25 @@ file for (applied) copies and renames."
          :active (stgit-patch-name-at-point nil t)]
         ["Rename patch" stgit-rename :active (stgit-patch-name-at-point nil t)]
         ["Push/pop patch" stgit-push-or-pop
-         :label (if (stgit-applied-at-point-p) "Pop patch" "Push patch")
-         :active (stgit-patch-name-at-point nil t)]
-        ["Delete patch" stgit-delete :active (stgit-patch-name-at-point nil t)]
+         :label (if (subsetp (stgit-patches-marked-or-at-point nil t)
+                             (stgit-applied-patchsyms t))
+                    "Pop patches" "Push patches")]
+        ["Delete patches" stgit-delete
+         :active (stgit-patches-marked-or-at-point nil t)]
         "-"
         ["Move patches" stgit-move-patches
          :active stgit-marked-patches
-         :help "Move selected patch(es) to point"]
+         :help "Move marked patch(es) to point"]
         ["Squash patches" stgit-squash
          :active (> (length stgit-marked-patches) 1)
-         :help "Merge selected patches into one"]
+         :help "Merge marked patches into one"]
         "-"
         ["Refresh top patch" stgit-refresh
          :active (not (and (stgit-index-empty-p) (stgit-work-tree-empty-p)))
          :help "Refresh the top patch with changes in index or work tree"]
         ["Refresh this patch" (stgit-refresh t)
          :keys "\\[universal-argument] \\[stgit-refresh]"
-         :help "Refresh the patch at point with changes in index or work tree"
+         :help "Refresh marked patch with changes in index or work tree"
          :active (and (not (and (stgit-index-empty-p)
                                 (stgit-work-tree-empty-p)))
                       (stgit-patch-name-at-point nil t))]
@@ -966,6 +1037,8 @@ file for (applied) copies and renames."
          :selected stgit-show-unknown :active stgit-show-worktree]
         ["Show ignored files" stgit-toggle-ignored :style toggle
          :selected stgit-show-ignored :active stgit-show-worktree]
+        ["Show patch names" stgit-toggle-patch-names :style toggle
+         :selected stgit-show-patch-names]
         "-"
         ["Switch branches" stgit-branch t
          :help "Switch to another branch"]
@@ -1010,11 +1083,12 @@ Commands for patches:
 \\[stgit-refresh]      Refresh patch with changes in index or work tree
 \\[stgit-diff] Show the patch log and diff
 
-\\[stgit-expand]       Show changes in selected patches
-\\[stgit-collapse]     Hide changes in selected patches
+\\[stgit-expand]       Show changes in marked patches
+\\[stgit-collapse]     Hide changes in marked patches
 
-\\[stgit-new]  Create a new, empty patch
 \\[stgit-new-and-refresh]      Create a new patch from index or work tree
+\\[stgit-new]  Create a new, empty patch
+
 \\[stgit-rename]       Rename patch
 \\[stgit-edit] Edit patch description
 \\[stgit-delete]       Delete patch(es)
@@ -1024,11 +1098,11 @@ Commands for patches:
 
 \\[stgit-push-next]    Push next patch onto stack
 \\[stgit-pop-next]     Pop current patch from stack
-\\[stgit-push-or-pop]  Push or pop patch at point
-\\[stgit-goto] Make current patch current by popping or pushing
+\\[stgit-push-or-pop]  Push or pop marked patches
+\\[stgit-goto] Make patch at point current by popping or pushing
 
 \\[stgit-squash]       Squash (meld together) patches
-\\[stgit-move-patches] Move patch(es) to point
+\\[stgit-move-patches] Move marked patches to point
 
 \\[stgit-commit]       Commit patch(es)
 \\[stgit-uncommit]     Uncommit patch(es)
@@ -1042,6 +1116,7 @@ Commands for files:
 \\[stgit-revert]       Revert changes to file
 
 Display commands:
+\\[stgit-toggle-patch-names]   Toggle showing patch names
 \\[stgit-toggle-worktree]      Toggle showing index and work tree
 \\[stgit-toggle-unknown]       Toggle showing unknown files
 \\[stgit-toggle-ignored]       Toggle showing ignored files
@@ -1067,6 +1142,7 @@ Commands for branches:
 
 Customization variables:
 `stgit-abbreviate-copies-and-renames'
+`stgit-default-show-patch-names'
 `stgit-default-show-worktree'
 `stgit-find-copies-harder'
 `stgit-show-worktree-mode'
@@ -1081,6 +1157,8 @@ See also \\[customize-group] for the \"stgit\" group."
   (set (make-local-variable 'list-buffers-directory) default-directory)
   (set (make-local-variable 'stgit-marked-patches) nil)
   (set (make-local-variable 'stgit-expanded-patches) (list :work :index))
+  (set (make-local-variable 'stgit-show-patch-names)
+       stgit-default-show-patch-names)
   (set (make-local-variable 'stgit-show-worktree) stgit-default-show-worktree)
   (set (make-local-variable 'stgit-index-node) nil)
   (set (make-local-variable 'stgit-worktree-node) nil)
@@ -1132,14 +1210,16 @@ index or work tree."
 (defun stgit-patched-file-at-point ()
   (get-text-property (point) 'file-data))
 
-(defun stgit-patches-marked-or-at-point ()
-  "Return the symbols of the marked patches, or the patch on the current line."
+(defun stgit-patches-marked-or-at-point (&optional cause-error only-patches)
+  "Return the symbols of the marked patches, or the patch on the current line.
+If CAUSE-ERRROR is not nil, signal an error if none found.
+If ONLY-PATCHES is not nil, do not include index or work tree."
   (if stgit-marked-patches
       stgit-marked-patches
-    (let ((patch (stgit-patch-name-at-point)))
-      (if patch
-          (list patch)
-        '()))))
+    (let ((patch (stgit-patch-name-at-point nil only-patches)))
+      (cond (patch (list patch))
+            (cause-error (error "No patches marked or at this line"))
+            (t nil)))))
 
 (defun stgit-goto-patch (patchsym &optional file)
   "Move point to the line containing patch PATCHSYM.
@@ -1460,23 +1540,43 @@ With numeric prefix argument, pop that many patches."
   (stgit-reload)
   (stgit-refresh-git-status))
 
-(defun stgit-applied-at-point-p ()
-  "Return non-nil if the patch at point is applied."
-  (let ((patch (stgit-patch-at-point t)))
-    (not (eq (stgit-patch-status patch) 'unapplied))))
+(defun stgit-applied-patches (&optional only-patches)
+  "Return a list of the applied patches.
+
+If ONLY-PATCHES is not nil, exclude index and work tree."
+  (let ((states (if only-patches
+                    '(applied top)
+                  '(applied top index work)))
+        result)
+    (ewoc-map (lambda (patch) (when (memq (stgit-patch-status patch) states)
+                                (setq result (cons patch result))))
+              stgit-ewoc)
+    result))
+
+(defun stgit-applied-patchsyms (&optional only-patches)
+  "Return a list of the symbols of the applied patches.
+
+If ONLY-PATCHES is not nil, exclude index and work tree."
+  (mapcar #'stgit-patch-name (stgit-applied-patches only-patches)))
 
 (defun stgit-push-or-pop ()
-  "Push or pop the patch on the current line."
+  "Push or pop the marked patches."
   (interactive)
   (stgit-assert-mode)
-  (let ((patchsym (stgit-patch-name-at-point t t))
-        (applied (stgit-applied-at-point-p)))
+  (let* ((patchsyms (stgit-patches-marked-or-at-point t t))
+         (applied-syms (stgit-applied-patchsyms t))
+         (unapplied (set-difference patchsyms applied-syms)))
     (stgit-capture-output nil
-      (stgit-run (if applied "pop" "push") patchsym))
-    (stgit-reload)))
+      (apply 'stgit-run
+             (if unapplied "push" "pop")
+             "--"
+             (stgit-sort-patches (if unapplied unapplied patchsyms)))))
+  (stgit-reload))
 
 (defun stgit-goto ()
-  "Go to the patch on the current line."
+  "Go to the patch on the current line.
+
+Pops or pushes patches to make this patch topmost."
   (interactive)
   (stgit-assert-mode)
   (let ((patchsym (stgit-patch-name-at-point t)))
@@ -1678,7 +1778,8 @@ file ended up. You can then jump to the file with \
     (set (make-local-variable 'stgit-edit-patchsym) patchsym)
     (setq default-directory dir)
     (let ((standard-output edit-buf))
-      (stgit-run-silent "edit" "--save-template=-" patchsym))))
+      (save-excursion
+        (stgit-run-silent "edit" "--save-template=-" patchsym)))))
 
 (defun stgit-confirm-edit ()
   (interactive)
@@ -1753,7 +1854,7 @@ Interactively, delete the marked patches, or the patch at point.
 
 With a prefix argument, or SPILL-P, spill the patch contents to
 the work tree and index."
-  (interactive (list (stgit-patches-marked-or-at-point)
+  (interactive (list (stgit-patches-marked-or-at-point t t)
                      current-prefix-arg))
   (stgit-assert-mode)
   (unless patchsyms
@@ -1781,13 +1882,21 @@ the work tree and index."
   "Return the patchsym indicating a target patch for
 `stgit-move-patches'.
 
-This is either the patch at point, or one of :top and :bottom, if
-the point is after or before the applied patches."
-
-  (let ((patchsym (stgit-patch-name-at-point nil t)))
-    (cond (patchsym patchsym)
-         ((save-excursion (re-search-backward "^>" nil t)) :top)
-         (t :bottom))))
+This is either the first unmarked patch at or after point, or one
+of :top and :bottom if the point is after or before the applied
+patches."
+
+  (save-excursion
+    (let (result)
+      (while (not result)
+        (let ((patchsym (stgit-patch-name-at-point)))
+          (cond ((memq patchsym '(:work :index)) (setq result :top))
+                (patchsym (if (memq patchsym stgit-marked-patches)
+                              (stgit-next-patch)
+                            (setq result patchsym)))
+                ((re-search-backward "^>" nil t) (setq result :top))
+                (t (setq result :bottom)))))
+      result)))
 
 (defun stgit-sort-patches (patchsyms)
   "Returns the list of patches in PATCHSYMS sorted according to
@@ -1826,20 +1935,17 @@ Interactively, move the marked patches to where the point is."
   (unless target-patch
     (error "Point not at a patch"))
 
-  (if (eq target-patch :top)
-      (stgit-capture-output nil
-        (apply 'stgit-run "float" patchsyms))
-
-    ;; need to have patchsyms sorted by position in the stack
-    (let ((sorted-patchsyms (stgit-sort-patches patchsyms)))
-      (while sorted-patchsyms
-        (setq sorted-patchsyms
-              (and (stgit-capture-output nil
-                     (if (eq target-patch :bottom)
-                         (stgit-run "sink" "--" (car sorted-patchsyms))
-                       (stgit-run "sink" "--to" target-patch "--"
-                                  (car sorted-patchsyms))))
-                   (cdr sorted-patchsyms))))))
+  ;; need to have patchsyms sorted by position in the stack
+  (let ((sorted-patchsyms (stgit-sort-patches patchsyms)))
+    (stgit-capture-output nil
+      (if (eq target-patch :top)
+          (apply 'stgit-run "float" sorted-patchsyms)
+        (apply 'stgit-run
+               "sink"
+               (append (unless (eq target-patch :bottom)
+                         (list "--to" target-patch))
+                       '("--")
+                       sorted-patchsyms)))))
   (stgit-reload))
 
 (defun stgit-squash (patchsyms)
@@ -1861,8 +1967,9 @@ deepest patch had before the squash."
     (set (make-local-variable 'stgit-patchsyms) sorted-patchsyms)
     (setq default-directory dir)
     (let ((result (let ((standard-output edit-buf))
-                    (apply 'stgit-run-silent "squash"
-                           "--save-template=-" sorted-patchsyms))))
+                    (save-excursion
+                      (apply 'stgit-run-silent "squash"
+                             "--save-template=-" sorted-patchsyms)))))
 
       ;; stg squash may have reordered the patches or caused conflicts
       (with-current-buffer stgit-buffer
@@ -1930,13 +2037,10 @@ With prefix argument, refresh the marked patch or the patch under point."
   (interactive "P")
   (stgit-assert-mode)
   (let ((patchargs (if arg
-                       (let ((patches (stgit-patches-marked-or-at-point)))
-                         (cond ((null patches)
-                                (error "No patch to update"))
-                               ((> (length patches) 1)
-                                (error "Too many patches selected"))
-                               (t
-                                (cons "-p" patches))))
+                       (let ((patches (stgit-patches-marked-or-at-point nil t)))
+                         (when (> (length patches) 1)
+                           (error "Too many patches marked"))
+                         (cons "-p" patches))
                      nil)))
     (unless (stgit-index-empty-p)
       (setq patchargs (cons "--index" patchargs)))
@@ -1956,6 +2060,9 @@ See also `stgit-show-worktree-mode'.")
 (defvar stgit-show-unknown nil
   "If nil, inhibit showing files not registered with git.")
 
+(defvar stgit-show-patch-names t
+  "If nil, inhibit showing patch names.")
+
 (defun stgit-toggle-worktree (&optional arg)
   "Toggle the visibility of the work tree.
 With ARG, show the work tree if ARG is positive.
@@ -1998,4 +2105,17 @@ Use \\[stgit-toggle-worktree] to show the work tree."
           (not stgit-show-unknown)))
   (stgit-reload))
 
+(defun stgit-toggle-patch-names (&optional arg)
+  "Toggle the visibility of patch names. With ARG, show patch names
+if ARG is positive.
+
+The initial setting is controlled by `stgit-default-show-patch-names'."
+  (interactive)
+  (stgit-assert-mode)
+  (setq stgit-show-patch-names
+        (if (numberp arg)
+            (> arg 0)
+          (not stgit-show-patch-names)))
+  (stgit-reload))
+
 (provide 'stgit)