stgit.el: Made a few wide lines more narrow
[stgit] / contrib / stgit.el
index b7c4b7f..234dcaa 100644 (file)
 (require 'cl)
 (require 'ewoc)
 (require 'easymenu)
 (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.
 
 (defun stgit (dir)
   "Manage StGit patches for the tree in DIR.
@@ -70,33 +252,54 @@ directory DIR or `default-directory'"
     (switch-to-buffer (or buffer
                          (create-stgit-buffer dir)))))
 
     (switch-to-buffer (or buffer
                          (create-stgit-buffer dir)))))
 
-(defstruct (stgit-patch)
+(defstruct (stgit-patch
+            (:conc-name stgit-patch->))
   status name desc empty files-ewoc)
 
   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-insert-without-trailing-whitespace (text)
+  "Insert TEXT in buffer using `insert', without trailing whitespace.
+A newline is appended."
+  (unless (string-match "\\(.*?\\) *$" text)
+    (error))
+  (insert (match-string 1 text) ?\n))
+
 (defun stgit-patch-pp (patch)
 (defun stgit-patch-pp (patch)
-  (let* ((status (stgit-patch-status patch))
+  (let* ((status (stgit-patch->status patch))
          (start (point))
          (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")
+         (name (stgit-patch->name patch))
+         (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 (let ((desc (stgit-patch->desc patch)))
+                                 (if (zerop (length desc))
+                                   (stgit-patch-display-name patch)
+                                   desc))
+                               'face face)))
+         (text (format-spec fmt spec)))
+
+    (stgit-insert-without-trailing-whitespace text)
     (put-text-property start (point) 'entry-type 'patch)
     (when (memq name stgit-expanded-patches)
       (stgit-insert-patch-files patch))
     (put-text-property start (point) 'entry-type 'patch)
     (when (memq name stgit-expanded-patches)
       (stgit-insert-patch-files patch))
@@ -115,6 +318,8 @@ Argument DIR is the repository path."
       (setq buffer-read-only t))
     buf))
 
       (setq buffer-read-only t))
     buf))
 
+(def-edebug-spec stgit-capture-output
+  (form body))
 (defmacro stgit-capture-output (name &rest body)
   "Capture StGit output and, if there was any output, show it in a window
 at the end.
 (defmacro stgit-capture-output (name &rest body)
   "Capture StGit output and, if there was any output, show it in a window
 at the end.
@@ -125,6 +330,7 @@ Returns nil if there was no output."
          (stgit-dir default-directory)
          (inhibit-read-only t))
      (with-current-buffer output-buf
          (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))
        (erase-buffer)
        (setq default-directory stgit-dir)
        (setq buffer-read-only t))
@@ -180,6 +386,17 @@ Returns nil if there was no output."
 (defvar stgit-index-node)
 (defvar stgit-worktree-node)
 
 (defvar stgit-index-node)
 (defvar stgit-worktree-node)
 
+(defvar stgit-did-advise nil
+  "Set to non-nil if appropriate (non-stgit) git functions have
+been advised to update the stgit status when necessary.")
+
+(defconst stgit-allowed-branch-name-re
+  ;; Disallow control characters, space, del, and "/:@^{}~" in
+  ;; "/"-separated parts; parts may not start with a period (.)
+  "^[^\0- ./:@^{}~\177][^\0- /:@^{}~\177]*\
+\\(/[^\0- ./:@^{}~\177][^\0- /:@^{}~\177]*\\)*$"
+  "Regular expression that (new) branch names must match.")
+
 (defun stgit-refresh-index ()
   (when stgit-index-node
     (ewoc-invalidate (car stgit-index-node) (cdr stgit-index-node))))
 (defun stgit-refresh-index ()
   (when stgit-index-node
     (ewoc-invalidate (car stgit-index-node) (cdr stgit-index-node))))
@@ -255,6 +472,12 @@ Returns nil if there was no output."
           stgit-marked-patches (intersection stgit-marked-patches
                                              all-patchsyms))))
 
           stgit-marked-patches (intersection stgit-marked-patches
                                              all-patchsyms))))
 
+(defun stgit-current-branch ()
+  "Return the name of the current branch."
+  (substring (with-output-to-string
+               (stgit-run-silent "branch"))
+             0 -1))
+
 (defun stgit-reload ()
   "Update the contents of the StGit buffer."
   (interactive)
 (defun stgit-reload ()
   "Update the contents of the StGit buffer."
   (interactive)
@@ -266,11 +489,8 @@ Returns nil if there was no output."
     (ewoc-filter stgit-ewoc #'(lambda (x) nil))
     (ewoc-set-hf stgit-ewoc
                  (concat "Branch: "
     (ewoc-filter stgit-ewoc #'(lambda (x) nil))
     (ewoc-set-hf stgit-ewoc
                  (concat "Branch: "
-                         (propertize
-                          (substring (with-output-to-string
-                                       (stgit-run-silent "branch"))
-                                     0 -1)
-                          'face 'stgit-branch-name-face)
+                         (propertize (stgit-current-branch)
+                                     'face 'stgit-branch-name-face)
                          "\n\n")
                  (if stgit-show-worktree
                      "--"
                          "\n\n")
                  (if stgit-show-worktree
                      "--"
@@ -280,130 +500,10 @@ Returns nil if there was no output."
                     'face 'stgit-description-face)))
     (stgit-run-series stgit-ewoc)
     (if curpatch
                     'face 'stgit-description-face)))
     (stgit-run-series stgit-ewoc)
     (if curpatch
-        (stgit-goto-patch curpatch (and curfile (stgit-file-file curfile)))
+        (stgit-goto-patch curpatch (and curfile (stgit-file->file curfile)))
       (goto-line curline)))
   (stgit-refresh-git-status))
 
       (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)
 (defconst stgit-file-status-code-strings
   (mapcar (lambda (arg)
             (cons (car arg)
@@ -429,16 +529,15 @@ applied and unapplied patches), and 'bottom (below all patches)."
 
 (defun stgit-file-status-code-as-string (file)
   "Return stgit status code for FILE as a string"
 
 (defun stgit-file-status-code-as-string (file)
   "Return stgit status code for FILE as a string"
-  (let* ((code (assq (stgit-file-status file)
+  (let* ((code (assq (stgit-file->status file)
                      stgit-file-status-code-strings))
                      stgit-file-status-code-strings))
-         (score (stgit-file-cr-score file)))
+         (score (stgit-file->cr-score file)))
     (when code
     (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"
 
 (defun stgit-file-status-code (str &optional score)
   "Return stgit status code from git status string"
@@ -481,8 +580,8 @@ Cf. `stgit-file-type-string'."
           ((zerop old-type)
            (if (= new-type #o100)
                ""
           ((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))))))
 
                      (stgit-file-type-string old-type)
                      (stgit-file-type-string new-type))))))
 
@@ -511,7 +610,8 @@ Cf. `stgit-file-type-change-string'."
                        (propertize (format "%o" new-perm)
                                    'face 'stgit-file-permission-face)))))))
 
                        (propertize (format "%o" new-perm)
                                    'face 'stgit-file-permission-face)))))))
 
-(defstruct (stgit-file)
+(defstruct (stgit-file
+            (:conc-name stgit-file->))
   old-perm new-perm copy-or-rename cr-score cr-from cr-to status file)
 
 (defun stgit-describe-copy-or-rename (file)
   old-perm new-perm copy-or-rename cr-score cr-from cr-to status file)
 
 (defun stgit-describe-copy-or-rename (file)
@@ -519,8 +619,8 @@ Cf. `stgit-file-type-change-string'."
         from to common-head common-tail)
 
     (when stgit-abbreviate-copies-and-renames
         from to common-head common-tail)
 
     (when stgit-abbreviate-copies-and-renames
-      (setq from (split-string (stgit-file-cr-from file) "/")
-            to   (split-string (stgit-file-cr-to   file) "/"))
+      (setq from (split-string (stgit-file->cr-from file) "/")
+            to   (split-string (stgit-file->cr-to   file) "/"))
 
       (while (and from to (cdr from) (cdr to)
                   (string-equal (car from) (car to)))
 
       (while (and from to (cdr from) (cdr to)
                   (string-equal (car from) (car to)))
@@ -552,26 +652,24 @@ Cf. `stgit-file-type-change-string'."
                 (if common-tail
                     (mapconcat #'identity common-tail "/")
                   ""))
                 (if common-tail
                     (mapconcat #'identity common-tail "/")
                   ""))
-      (concat (stgit-file-cr-from file) arrow (stgit-file-cr-to file)))))
+      (concat (stgit-file->cr-from file) arrow (stgit-file->cr-to file)))))
 
 (defun stgit-file-pp (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))))
+    (stgit-insert-without-trailing-whitespace
+     (format-spec stgit-file-line-format spec))
     (add-text-properties start (point)
                          (list 'entry-type 'file
                                'file-data file))))
     (add-text-properties start (point)
                          (list 'entry-type 'file
                                'file-data file))))
@@ -591,19 +689,62 @@ Cf. `stgit-file-type-change-string'."
         (insert ":0 0 0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 " file-flag "\0")
         (forward-char name-len)))))
 
         (insert ":0 0 0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 " file-flag "\0")
         (forward-char name-len)))))
 
+(defun stgit-process-files (callback)
+  (goto-char (point-min))
+  (when (looking-at "[0-9A-Fa-f]\\{40\\}\0")
+    (goto-char (match-end 0)))
+  (while (looking-at ":\\([0-7]+\\) \\([0-7]+\\) [0-9A-Fa-f]\\{40\\} [0-9A-Fa-f]\\{40\\} ")
+    (let ((old-perm (string-to-number (match-string 1) 8))
+          (new-perm (string-to-number (match-string 2) 8)))
+      (goto-char (match-end 0))
+      (let ((file
+             (cond ((looking-at
+                     "\\([CR]\\)\\([0-9]*\\)\0\\([^\0]*\\)\0\\([^\0]*\\)\0")
+                    (let* ((patch-status (stgit-patch->status patch))
+                           (file-subexp  (if (eq patch-status 'unapplied)
+                                             3
+                                           4))
+                           (file         (match-string file-subexp)))
+                      (make-stgit-file
+                       :old-perm       old-perm
+                       :new-perm       new-perm
+                       :copy-or-rename t
+                       :cr-score       (string-to-number (match-string 2))
+                       :cr-from        (match-string 3)
+                       :cr-to          (match-string 4)
+                       :status         (stgit-file-status-code
+                                        (match-string 1))
+                       :file           file)))
+                   ((looking-at "\\([ABD-QS-Z]\\)\0\\([^\0]*\\)\0")
+                    (make-stgit-file
+                     :old-perm       old-perm
+                     :new-perm       new-perm
+                     :copy-or-rename nil
+                     :cr-score       nil
+                     :cr-from        nil
+                     :cr-to          nil
+                     :status         (stgit-file-status-code
+                                      (match-string 1))
+                     :file           (match-string 2))))))
+        (goto-char (match-end 0))
+        (funcall callback file)))))
+
+
 (defun stgit-insert-patch-files (patch)
   "Expand (show modification of) the patch PATCH after the line
 at point."
 (defun stgit-insert-patch-files (patch)
   "Expand (show modification of) the patch PATCH after the line
 at point."
-  (let* ((patchsym (stgit-patch-name patch))
+  (let* ((patchsym (stgit-patch->name patch))
          (end      (point-marker))
          (args     (list "-z" (stgit-find-copies-harder-diff-arg)))
          (ewoc     (ewoc-create #'stgit-file-pp nil nil t)))
     (set-marker-insertion-type end t)
          (end      (point-marker))
          (args     (list "-z" (stgit-find-copies-harder-diff-arg)))
          (ewoc     (ewoc-create #'stgit-file-pp nil nil t)))
     (set-marker-insertion-type end t)
-    (setf (stgit-patch-files-ewoc patch) ewoc)
+    (setf (stgit-patch->files-ewoc patch) ewoc)
     (with-temp-buffer
       (let ((standard-output (current-buffer)))
         (apply 'stgit-run-git
                (cond ((eq patchsym :work)
     (with-temp-buffer
       (let ((standard-output (current-buffer)))
         (apply 'stgit-run-git
                (cond ((eq patchsym :work)
+                      (let (standard-output)
+                        (stgit-run-git "update-index" "--refresh"))
                       `("diff-files" "-0" ,@args))
                      ((eq patchsym :index)
                       `("diff-index" ,@args "--cached" "HEAD"))
                       `("diff-files" "-0" ,@args))
                      ((eq patchsym :index)
                       `("diff-index" ,@args "--cached" "HEAD"))
@@ -614,48 +755,13 @@ at point."
           (when stgit-show-ignored
             (stgit-insert-ls-files '("--ignored" "--others") "I"))
           (when stgit-show-unknown
           (when stgit-show-ignored
             (stgit-insert-ls-files '("--ignored" "--others") "I"))
           (when stgit-show-unknown
-            (stgit-insert-ls-files '("--others") "X"))
+            (stgit-insert-ls-files '("--directory" "--no-empty-directory"
+                                     "--others")
+                                   "X"))
           (sort-regexp-fields nil ":[^\0]*\0\\([^\0]*\\)\0" "\\1"
                               (point-min) (point-max)))
 
           (sort-regexp-fields nil ":[^\0]*\0\\([^\0]*\\)\0" "\\1"
                               (point-min) (point-max)))
 
-        (goto-char (point-min))
-        (unless (or (eobp) (memq patchsym '(:work :index)))
-          (forward-char 41))
-        (while (looking-at ":\\([0-7]+\\) \\([0-7]+\\) [0-9A-Fa-f]\\{40\\} [0-9A-Fa-f]\\{40\\} ")
-          (let ((old-perm (string-to-number (match-string 1) 8))
-                (new-perm (string-to-number (match-string 2) 8)))
-            (goto-char (match-end 0))
-            (let ((file
-                   (cond ((looking-at
-                           "\\([CR]\\)\\([0-9]*\\)\0\\([^\0]*\\)\0\\([^\0]*\\)\0")
-                          (let* ((patch-status (stgit-patch-status patch))
-                                 (file-subexp  (if (eq patch-status 'unapplied)
-                                                   3
-                                                 4))
-                                 (file         (match-string file-subexp)))
-                            (make-stgit-file
-                             :old-perm       old-perm
-                             :new-perm       new-perm
-                             :copy-or-rename t
-                             :cr-score       (string-to-number (match-string 2))
-                             :cr-from        (match-string 3)
-                             :cr-to          (match-string 4)
-                             :status         (stgit-file-status-code
-                                              (match-string 1))
-                             :file           file)))
-                         ((looking-at "\\([ABD-QS-Z]\\)\0\\([^\0]*\\)\0")
-                          (make-stgit-file
-                           :old-perm       old-perm
-                           :new-perm       new-perm
-                           :copy-or-rename nil
-                           :cr-score       nil
-                           :cr-from        nil
-                           :cr-to          nil
-                           :status         (stgit-file-status-code
-                                            (match-string 1))
-                           :file           (match-string 2))))))
-              (goto-char (match-end 0))
-              (ewoc-enter-last ewoc file))))
+        (stgit-process-files (lambda (file) (ewoc-enter-last ewoc file)))
 
         (unless (ewoc-nth ewoc 0)
           (ewoc-set-hf ewoc ""
 
         (unless (ewoc-nth ewoc 0)
           (ewoc-set-hf ewoc ""
@@ -668,12 +774,12 @@ at point."
 (defun stgit-find-file (&optional other-window)
   (let* ((file (or (stgit-patched-file-at-point)
                    (error "No file at point")))
 (defun stgit-find-file (&optional other-window)
   (let* ((file (or (stgit-patched-file-at-point)
                    (error "No file at point")))
-         (filename (expand-file-name (stgit-file-file file))))
+         (filename (expand-file-name (stgit-file->file file))))
     (unless (file-exists-p filename)
       (error "File does not exist"))
     (funcall (if other-window 'find-file-other-window 'find-file)
              filename)
     (unless (file-exists-p filename)
       (error "File does not exist"))
     (funcall (if other-window 'find-file-other-window 'find-file)
              filename)
-    (when (eq (stgit-file-status file) 'unmerged)
+    (when (eq (stgit-file->status file) 'unmerged)
       (smerge-mode 1))))
 
 (defun stgit-expand (&optional patches collapse)
       (smerge-mode 1))))
 
 (defun stgit-expand (&optional patches collapse)
@@ -692,7 +798,7 @@ expand if COLLAPSE is not nil."
               (set-difference stgit-expanded-patches patches-diff)
             (append stgit-expanded-patches patches-diff)))
     (ewoc-map #'(lambda (patch)
               (set-difference stgit-expanded-patches patches-diff)
             (append stgit-expanded-patches patches-diff)))
     (ewoc-map #'(lambda (patch)
-                  (memq (stgit-patch-name patch) patches-diff))
+                  (memq (stgit-patch->name patch) patches-diff))
               stgit-ewoc))
   (move-to-column (stgit-goal-column)))
 
               stgit-ewoc))
   (move-to-column (stgit-goal-column)))
 
@@ -709,6 +815,45 @@ See also `stgit-expand'."
     (stgit-expand (list patchname)
                   (memq patchname stgit-expanded-patches))))
 
     (stgit-expand (list patchname)
                   (memq patchname stgit-expanded-patches))))
 
+(defun stgit-expand-directory (file)
+  (let* ((patch (stgit-patch-at-point))
+         (ewoc (stgit-patch->files-ewoc patch))
+         (node (ewoc-locate ewoc))
+         (filename (stgit-file->file file))
+         (start (make-marker))
+         (end (make-marker)))
+
+    (save-excursion
+      (forward-line 1)
+      (set-marker start (point))
+      (set-marker end (point))
+      (set-marker-insertion-type end t))
+
+    (assert (string-match "/$" filename))
+    ;; remove trailing "/"
+    (setf (stgit-file->file file) (substring filename 0 -1))
+    (ewoc-invalidate ewoc node)
+
+    (with-temp-buffer
+      (let ((standard-output (current-buffer)))
+        (stgit-insert-ls-files (list "--directory" "--others"
+                                     "--no-empty-directory" "--"
+                                     filename)
+                               "X")
+        (stgit-process-files (lambda (f)
+                               (setq node (ewoc-enter-after ewoc node f))))))
+
+    (let ((inhibit-read-only t))
+      (put-text-property start end 'patch-data patch))))
+
+(defun stgit-select-file ()
+  (let* ((file (or (stgit-patched-file-at-point)
+                   (error "No file at point")))
+         (filename (stgit-file->file file)))
+    (if (string-match "/$" filename)
+        (stgit-expand-directory file)
+      (stgit-find-file))))
+
 (defun stgit-select ()
   "With point on a patch, toggle showing files in the patch.
 
 (defun stgit-select ()
   "With point on a patch, toggle showing files in the patch.
 
@@ -720,7 +865,7 @@ file for (applied) copies and renames."
     ('patch
      (stgit-select-patch))
     ('file
     ('patch
      (stgit-select-patch))
     ('file
-     (stgit-find-file))
+     (stgit-select-file))
     (t
      (error "No patch or file on line"))))
 
     (t
      (error "No patch or file on line"))))
 
@@ -804,10 +949,12 @@ file for (applied) copies and renames."
             ("c" .        stgit-diff-combined)
             ("m" .        stgit-find-file-merge)
             ("o" .        stgit-diff-ours)
             ("c" .        stgit-diff-combined)
             ("m" .        stgit-find-file-merge)
             ("o" .        stgit-diff-ours)
+            ("r" .        stgit-diff-range)
             ("t" .        stgit-diff-theirs)))
     (suppress-keymap toggle-map)
     (mapc (lambda (arg) (define-key toggle-map (car arg) (cdr arg)))
             ("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))
             ("i" .        stgit-toggle-ignored)
             ("u" .        stgit-toggle-unknown)))
     (setq stgit-mode-map (make-keymap))
@@ -862,7 +1009,7 @@ file for (applied) copies and renames."
             ("q" .        stgit-quit))))
 
   (let ((at-unmerged-file '(let ((file (stgit-patched-file-at-point)))
             ("q" .        stgit-quit))))
 
   (let ((at-unmerged-file '(let ((file (stgit-patched-file-at-point)))
-                             (and file (eq (stgit-file-status file)
+                             (and file (eq (stgit-file->status file)
                                            'unmerged))))
         (patch-collapsed-p '(lambda (p) (not (memq p stgit-expanded-patches)))))
     (easy-menu-define stgit-menu stgit-mode-map
                                            'unmerged))))
         (patch-collapsed-p '(lambda (p) (not (memq p stgit-expanded-patches)))))
     (easy-menu-define stgit-menu stgit-mode-map
@@ -904,8 +1051,9 @@ 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
          :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)]
+         :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)]
         "-"
         ["Delete patches" stgit-delete
          :active (stgit-patches-marked-or-at-point nil t)]
         "-"
@@ -939,6 +1087,8 @@ file for (applied) copies and renames."
         "-"
         ["Show diff" stgit-diff
          :active (get-text-property (point) 'entry-type)]
         "-"
         ["Show diff" stgit-diff
          :active (get-text-property (point) 'entry-type)]
+        ["Show diff for range of applied patches" stgit-diff-range
+         :active (= (length stgit-marked-patches) 1)]
         ("Merge"
          :active (stgit-git-index-unmerged-p)
          ["Combined diff" stgit-diff-combined
         ("Merge"
          :active (stgit-git-index-unmerged-p)
          ["Combined diff" stgit-diff-combined
@@ -967,9 +1117,11 @@ 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]
          :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
         "-"
         ["Switch branches" stgit-branch t
-         :help "Switch to another branch"]
+         :help "Switch to or create another branch"]
         ["Rebase branch" stgit-rebase t
          :help "Rebase the current branch"]
         ))))
         ["Rebase branch" stgit-rebase t
          :help "Rebase the current branch"]
         ))))
@@ -1014,8 +1166,9 @@ Commands for patches:
 \\[stgit-expand]       Show changes in marked patches
 \\[stgit-collapse]     Hide changes in marked 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-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)
 \\[stgit-rename]       Rename patch
 \\[stgit-edit] Edit patch description
 \\[stgit-delete]       Delete patch(es)
@@ -1025,11 +1178,11 @@ Commands for patches:
 
 \\[stgit-push-next]    Push next patch onto stack
 \\[stgit-pop-next]     Pop current patch from stack
 
 \\[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-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)
 
 \\[stgit-commit]       Commit patch(es)
 \\[stgit-uncommit]     Uncommit patch(es)
@@ -1043,12 +1196,14 @@ Commands for files:
 \\[stgit-revert]       Revert changes to file
 
 Display commands:
 \\[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
 
 Commands for diffs:
 \\[stgit-diff] Show diff of patch or file
 \\[stgit-toggle-worktree]      Toggle showing index and work tree
 \\[stgit-toggle-unknown]       Toggle showing unknown files
 \\[stgit-toggle-ignored]       Toggle showing ignored files
 
 Commands for diffs:
 \\[stgit-diff] Show diff of patch or file
+\\[stgit-diff-range]   Show diff for range of patches
 \\[stgit-diff-base]    Show diff against the merge base
 \\[stgit-diff-ours]    Show diff against our branch
 \\[stgit-diff-theirs]  Show diff against their branch
 \\[stgit-diff-base]    Show diff against the merge base
 \\[stgit-diff-ours]    Show diff against our branch
 \\[stgit-diff-theirs]  Show diff against their branch
@@ -1063,11 +1218,12 @@ Commands for merge conflicts:
 \\[stgit-resolve-file] Mark unmerged file as resolved
 
 Commands for branches:
 \\[stgit-resolve-file] Mark unmerged file as resolved
 
 Commands for branches:
-\\[stgit-branch]       Switch to another branch
+\\[stgit-branch]       Switch to or create another branch
 \\[stgit-rebase]       Rebase the current branch
 
 Customization variables:
 `stgit-abbreviate-copies-and-renames'
 \\[stgit-rebase]       Rebase the current branch
 
 Customization variables:
 `stgit-abbreviate-copies-and-renames'
+`stgit-default-show-patch-names'
 `stgit-default-show-worktree'
 `stgit-find-copies-harder'
 `stgit-show-worktree-mode'
 `stgit-default-show-worktree'
 `stgit-find-copies-harder'
 `stgit-show-worktree-mode'
@@ -1082,23 +1238,60 @@ 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 '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)
   (set (make-local-variable 'parse-sexp-lookup-properties) t)
   (set-variable 'truncate-lines 't)
   (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)
   (set (make-local-variable 'parse-sexp-lookup-properties) t)
   (set-variable 'truncate-lines 't)
-  (add-hook 'after-save-hook 'stgit-update-saved-file)
+  (add-hook 'after-save-hook 'stgit-update-stgit-for-buffer)
+  (unless stgit-did-advise
+    (stgit-advise)
+    (setq stgit-did-advise t))
   (run-hooks 'stgit-mode-hook))
 
   (run-hooks 'stgit-mode-hook))
 
-(defun stgit-update-saved-file ()
-  (let* ((file (expand-file-name buffer-file-name))
-         (dir (file-name-directory file))
-         (gitdir (condition-case nil (git-get-top-dir dir)
-                   (error nil)))
+(defun stgit-advise-funlist (funlist)
+  "Add advice to the functions in FUNLIST so we can refresh the
+stgit buffers as the git status of files change."
+  (mapc (lambda (sym)
+          (when (fboundp sym)
+            (eval `(defadvice ,sym (after stgit-update-stgit-for-buffer)
+                     (stgit-update-stgit-for-buffer t)))
+            (ad-activate sym)))
+        funlist))
+
+(defun stgit-advise ()
+  "Add advice to appropriate (non-stgit) git functions so we can
+refresh the stgit buffers as the git status of files change."
+  (mapc (lambda (arg)
+          (let ((feature (car arg))
+                (funlist (cdr arg)))
+            (if (featurep feature)
+                (stgit-advise-funlist funlist)
+              (add-to-list 'after-load-alist
+                           `(,feature (stgit-advise-funlist
+                                       (quote ,funlist)))))))
+        '((vc-git vc-git-rename-file vc-git-revert vc-git-register)
+          (git    git-add-file git-checkout git-revert-file git-remove-file))))
+
+(defun stgit-update-stgit-for-buffer (&optional refresh-index)
+  "Refresh worktree status in any `stgit-mode' buffer that shows
+the status of the current buffer.
+
+If REFRESH-INDEX is not-nil, also update the index."
+  (let* ((dir (cond ((eq major-mode 'git-status-mode)
+                     default-directory)
+                    (buffer-file-name
+                     (file-name-directory
+                      (expand-file-name buffer-file-name)))))
+         (gitdir (and dir (condition-case nil (git-get-top-dir dir)
+                            (error nil))))
         (buffer (and gitdir (stgit-find-buffer gitdir))))
     (when buffer
       (with-current-buffer buffer
         (buffer (and gitdir (stgit-find-buffer gitdir))))
     (when buffer
       (with-current-buffer buffer
-        (stgit-refresh-worktree)))))
+        (stgit-refresh-worktree)
+        (when refresh-index (stgit-refresh-index))))))
 
 (defun stgit-add-mark (patchsym)
   "Mark the patch PATCHSYM."
 
 (defun stgit-add-mark (patchsym)
   "Mark the patch PATCHSYM."
@@ -1123,10 +1316,10 @@ index or work tree."
   (let ((patch (stgit-patch-at-point)))
     (and patch
          only-patches
   (let ((patch (stgit-patch-at-point)))
     (and patch
          only-patches
-         (memq (stgit-patch-status patch) '(work index))
+         (memq (stgit-patch->status patch) '(work index))
          (setq patch nil))
     (cond (patch
          (setq patch nil))
     (cond (patch
-           (stgit-patch-name patch))
+           (stgit-patch->name patch))
           (cause-error
            (error "No patch on this line")))))
 
           (cause-error
            (error "No patch on this line")))))
 
@@ -1152,13 +1345,15 @@ If the patch was found and FILE is not nil, instead move to that
 file's line. If FILE cannot be found, stay on the line of
 PATCHSYM."
   (let ((node (ewoc-nth stgit-ewoc 0)))
 file's line. If FILE cannot be found, stay on the line of
 PATCHSYM."
   (let ((node (ewoc-nth stgit-ewoc 0)))
-    (while (and node (not (eq (stgit-patch-name (ewoc-data node))
+    (while (and node (not (eq (stgit-patch->name (ewoc-data node))
                               patchsym)))
       (setq node (ewoc-next stgit-ewoc node)))
     (when (and node file)
                               patchsym)))
       (setq node (ewoc-next stgit-ewoc node)))
     (when (and node file)
-      (let* ((file-ewoc (stgit-patch-files-ewoc (ewoc-data node)))
+      (let* ((file-ewoc (stgit-patch->files-ewoc (ewoc-data node)))
              (file-node (ewoc-nth file-ewoc 0)))
              (file-node (ewoc-nth file-ewoc 0)))
-        (while (and file-node (not (equal (stgit-file-file (ewoc-data file-node)) file)))
+        (while (and file-node
+                    (not (equal (stgit-file->file (ewoc-data file-node))
+                                file)))
           (setq file-node (ewoc-next file-ewoc file-node)))
         (when file-node
           (ewoc-goto-node file-ewoc file-node)
           (setq file-node (ewoc-next file-ewoc file-node)))
         (when file-node
           (ewoc-goto-node file-ewoc file-node)
@@ -1190,12 +1385,12 @@ PATCHSYM."
   (stgit-assert-mode)
   (let* ((node (ewoc-locate stgit-ewoc))
          (patch (ewoc-data node))
   (stgit-assert-mode)
   (let* ((node (ewoc-locate stgit-ewoc))
          (patch (ewoc-data node))
-         (name (stgit-patch-name patch)))
+         (name (stgit-patch->name patch)))
     (when (eq name :work)
       (error "Cannot mark the work tree"))
     (when (eq name :index)
       (error "Cannot mark the index"))
     (when (eq name :work)
       (error "Cannot mark the work tree"))
     (when (eq name :index)
       (error "Cannot mark the index"))
-    (stgit-add-mark (stgit-patch-name patch))
+    (stgit-add-mark (stgit-patch->name patch))
     (let ((column (current-column)))
       (ewoc-invalidate stgit-ewoc node)
       (move-to-column column))))
     (let ((column (current-column)))
       (ewoc-invalidate stgit-ewoc node)
       (move-to-column column))))
@@ -1212,7 +1407,7 @@ PATCHSYM."
   (stgit-assert-mode)
   (let* ((node (ewoc-locate stgit-ewoc))
          (patch (ewoc-data node)))
   (stgit-assert-mode)
   (let* ((node (ewoc-locate stgit-ewoc))
          (patch (ewoc-data node)))
-    (stgit-remove-mark (stgit-patch-name patch))
+    (stgit-remove-mark (stgit-patch->name patch))
     (let ((column (current-column)))
       (ewoc-invalidate stgit-ewoc node)
       (move-to-column column))))
     (let ((column (current-column)))
       (ewoc-invalidate stgit-ewoc node)
       (move-to-column column))))
@@ -1269,24 +1464,38 @@ was modified with git commands (`stgit-repair')."
     (stgit-run "repair"))
   (stgit-reload))
 
     (stgit-run "repair"))
   (stgit-reload))
 
-(defun stgit-available-branches ()
-  "Returns a list of the available stg branches"
+(defun stgit-available-branches (&optional all)
+  "Returns a list of the names of the available stg branches as strings.
+
+If ALL is not nil, also return non-stgit branches."
   (let ((output (with-output-to-string
                   (stgit-run "branch" "--list")))
   (let ((output (with-output-to-string
                   (stgit-run "branch" "--list")))
+        (pattern (format "^>?\\s-+%c\\s-+\\(\\S-+\\)"
+                         (if all ?. ?s)))
         (start 0)
         result)
         (start 0)
         result)
-    (while (string-match "^>?\\s-+s\\s-+\\(\\S-+\\)" output start)
+    (while (string-match pattern output start)
       (setq result (cons (match-string 1 output) result))
       (setq start (match-end 0)))
     result))
 
 (defun stgit-branch (branch)
       (setq result (cons (match-string 1 output) result))
       (setq start (match-end 0)))
     result))
 
 (defun stgit-branch (branch)
-  "Switch to branch BRANCH."
+  "Switch to or create branch BRANCH."
   (interactive (list (completing-read "Switch to branch: "
                                       (stgit-available-branches))))
   (stgit-assert-mode)
   (interactive (list (completing-read "Switch to branch: "
                                       (stgit-available-branches))))
   (stgit-assert-mode)
-  (stgit-capture-output nil (stgit-run "branch" "--" branch))
-  (stgit-reload))
+  (when (cond ((equal branch (stgit-current-branch))
+               (error "Branch is already current"))
+              ((member branch (stgit-available-branches t))
+               (stgit-capture-output nil (stgit-run "branch" "--" branch))
+               t)
+              ((not (string-match stgit-allowed-branch-name-re branch))
+               (error "Invalid branch name"))
+              ((yes-or-no-p (format "Create branch \"%s\"? " branch))
+               (stgit-capture-output nil (stgit-run "branch" "--create" "--"
+                                                    branch))
+               t))
+    (stgit-reload)))
 
 (defun stgit-available-refs (&optional omit-stgit)
   "Returns a list of the available git refs.
 
 (defun stgit-available-refs (&optional omit-stgit)
   "Returns a list of the available git refs.
@@ -1306,10 +1515,25 @@ If OMIT-STGIT is not nil, filter out \"resf/heads/*.stgit\"."
                            result)
               result))))
 
                            result)
               result))))
 
+(defun stgit-parent-branch ()
+  "Return the parent branch of the current stg branch as per
+git-config setting branch.<branch>.stgit.parentbranch."
+  (let ((output (with-output-to-string
+                  (stgit-run-git-silent "config"
+                                        (format "branch.%s.stgit.parentbranch"
+                                                (stgit-current-branch))))))
+    (when (string-match ".*" output)
+      (match-string 0 output))))
+
 (defun stgit-rebase (new-base)
 (defun stgit-rebase (new-base)
-  "Rebase to NEW-BASE."
+  "Rebase the current branch to NEW-BASE.
+
+Interactively, first ask which branch to rebase to. Defaults to
+what git-config branch.<branch>.stgit.parentbranch is set to."
   (interactive (list (completing-read "Rebase to: "
   (interactive (list (completing-read "Rebase to: "
-                                      (stgit-available-refs t))))
+                                      (stgit-available-refs t)
+                                      nil nil
+                                      (stgit-parent-branch))))
   (stgit-assert-mode)
   (stgit-capture-output nil (stgit-run "rebase" new-base))
   (stgit-reload))
   (stgit-assert-mode)
   (stgit-capture-output nil (stgit-run "rebase" new-base))
   (stgit-reload))
@@ -1343,12 +1567,12 @@ previous file if point is at the last file within a patch."
         neighbour-file)
     (and (zerop (forward-line 1))
          (let ((f (stgit-patched-file-at-point)))
         neighbour-file)
     (and (zerop (forward-line 1))
          (let ((f (stgit-patched-file-at-point)))
-           (and f (setq neighbour-file (stgit-file-file f)))))
+           (and f (setq neighbour-file (stgit-file->file f)))))
     (goto-char old-point)
     (unless neighbour-file
       (and (zerop (forward-line -1))
            (let ((f (stgit-patched-file-at-point)))
     (goto-char old-point)
     (unless neighbour-file
       (and (zerop (forward-line -1))
            (let ((f (stgit-patched-file-at-point)))
-             (and f (setq neighbour-file (stgit-file-file f)))))
+             (and f (setq neighbour-file (stgit-file->file f)))))
       (goto-char old-point))
     neighbour-file))
 
       (goto-char old-point))
     neighbour-file))
 
@@ -1360,15 +1584,15 @@ working tree."
   (let* ((patched-file (or (stgit-patched-file-at-point)
                            (error "No file on the current line")))
          (patch-name   (stgit-patch-name-at-point))
   (let* ((patched-file (or (stgit-patched-file-at-point)
                            (error "No file on the current line")))
          (patch-name   (stgit-patch-name-at-point))
-         (file-status  (stgit-file-status patched-file))
-         (rm-file      (cond ((stgit-file-copy-or-rename patched-file)
-                              (stgit-file-cr-to patched-file))
+         (file-status  (stgit-file->status patched-file))
+         (rm-file      (cond ((stgit-file->copy-or-rename patched-file)
+                              (stgit-file->cr-to patched-file))
                              ((eq file-status 'add)
                              ((eq file-status 'add)
-                              (stgit-file-file patched-file))))
+                              (stgit-file->file patched-file))))
          (co-file      (cond ((eq file-status 'rename)
          (co-file      (cond ((eq file-status 'rename)
-                              (stgit-file-cr-from patched-file))
+                              (stgit-file->cr-from patched-file))
                              ((not (memq file-status '(copy add)))
                              ((not (memq file-status '(copy add)))
-                              (stgit-file-file patched-file))))
+                              (stgit-file->file patched-file))))
          (next-file    (stgit-neighbour-file)))
 
     (unless (memq patch-name '(:work :index))
          (next-file    (stgit-neighbour-file)))
 
     (unless (memq patch-name '(:work :index))
@@ -1431,8 +1655,8 @@ tree, or a single change in either."
   (stgit-assert-mode)
   (let* ((patched-file (stgit-patched-file-at-point))
          (patch        (stgit-patch-at-point))
   (stgit-assert-mode)
   (let* ((patched-file (stgit-patched-file-at-point))
          (patch        (stgit-patch-at-point))
-         (patch-name   (and patch (stgit-patch-name patch)))
-         (status       (and patched-file (stgit-file-status patched-file))))
+         (patch-name   (and patch (stgit-patch->name patch)))
+         (status       (and patched-file (stgit-file->status patched-file))))
 
     (unless (memq patch-name '(:work :index))
       (error "No index or working tree file on this line"))
 
     (unless (memq patch-name '(:work :index))
       (error "No index or working tree file on this line"))
@@ -1441,51 +1665,99 @@ tree, or a single change in either."
       (error "No conflict to resolve at the current line"))
 
     (stgit-capture-output nil
       (error "No conflict to resolve at the current line"))
 
     (stgit-capture-output nil
-      (stgit-move-change-to-index (stgit-file-file patched-file)))
+      (stgit-move-change-to-index (stgit-file->file patched-file)))
 
     (stgit-reload)))
 
 
     (stgit-reload)))
 
+(defun stgit-push-or-pop-patches (do-push npatches)
+  "Push (if DO-PUSH is not nil) or pop (if DO-PUSH is nil)
+NPATCHES patches, or all patches if NPATCHES is t."
+  (stgit-assert-mode)
+  (stgit-capture-output nil
+    (apply 'stgit-run
+           (if do-push "push" "pop")
+           (if (eq npatches t)
+               '("--all")
+             (list "-n" npatches))))
+  (stgit-reload)
+  (stgit-refresh-git-status))
+
 (defun stgit-push-next (npatches)
   "Push the first unapplied patch.
 With numeric prefix argument, push that many patches."
   (interactive "p")
 (defun stgit-push-next (npatches)
   "Push the first unapplied patch.
 With numeric prefix argument, push that many patches."
   (interactive "p")
-  (stgit-assert-mode)
-  (stgit-capture-output nil (stgit-run "push" "-n" npatches))
-  (stgit-reload)
-  (stgit-refresh-git-status))
+  (stgit-push-or-pop-patches t npatches))
 
 (defun stgit-pop-next (npatches)
   "Pop the topmost applied patch.
 
 (defun stgit-pop-next (npatches)
   "Pop the topmost applied patch.
-With numeric prefix argument, pop that many patches."
+With numeric prefix argument, pop that many patches.
+
+If NPATCHES is t, pop all patches."
   (interactive "p")
   (interactive "p")
-  (stgit-assert-mode)
-  (stgit-capture-output nil (stgit-run "pop" "-n" npatches))
-  (stgit-reload)
-  (stgit-refresh-git-status))
+  (stgit-push-or-pop-patches nil npatches))
+
+(defun stgit-applied-patches (&optional only-patches)
+  "Return a list of the applied patches.
 
 
-(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))))
+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)))
+                nil)
+              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 ()
 
 (defun stgit-push-or-pop ()
-  "Push or pop the patch on the current line."
+  "Push or pop the marked patches."
   (interactive)
   (stgit-assert-mode)
   (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-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-target ()
+  "Return the goto target a point; either a patchsym, :top,
+or :bottom."
+  (let ((patchsym (stgit-patch-name-at-point)))
+    (cond ((memq patchsym '(:work :index)) nil)
+          (patchsym)
+          ((not (next-single-property-change (point) 'patch-data))
+           :top)
+          ((not (previous-single-property-change (point) 'patch-data))
+           :bottom))))
 
 (defun stgit-goto ()
 
 (defun stgit-goto ()
-  "Go to the patch on the current line."
+  "Go to the patch on the current line.
+
+Push or pop patches to make this patch topmost. Push or pop all
+patches if used on a line after or before all patches."
   (interactive)
   (stgit-assert-mode)
   (interactive)
   (stgit-assert-mode)
-  (let ((patchsym (stgit-patch-name-at-point t)))
-    (stgit-capture-output nil
-      (stgit-run "goto" patchsym))
-    (stgit-reload)))
+  (let ((patchsym (stgit-goto-target)))
+    (unless patchsym
+      (error "No patch to go to on this line"))
+    (case patchsym
+      (:top    (stgit-push-or-pop-patches t t))
+      (:bottom (stgit-push-or-pop-patches nil t))
+      (t (stgit-capture-output nil
+           (stgit-run "goto" patchsym))
+         (stgit-reload)))))
 
 (defun stgit-id (patchsym)
   "Return the git commit id for PATCHSYM.
 
 (defun stgit-id (patchsym)
   "Return the git commit id for PATCHSYM.
@@ -1498,16 +1770,17 @@ If PATCHSYM is a keyword, returns PATCHSYM unmodified."
        (error "Cannot find commit id for %s" patchsym))
       (match-string 1 result))))
 
        (error "Cannot find commit id for %s" patchsym))
       (match-string 1 result))))
 
+(defun stgit-whitespace-diff-arg (arg)
+  (when (numberp arg)
+    (cond ((> arg 4) "--ignore-all-space")
+          ((> arg 1) "--ignore-space-change"))))
+
 (defun stgit-show-patch (unmerged-stage ignore-whitespace)
   "Show the patch on the current line.
 
 UNMERGED-STAGE is the argument to `git-diff' that that selects
 which stage to diff against in the case of unmerged files."
 (defun stgit-show-patch (unmerged-stage ignore-whitespace)
   "Show the patch on the current line.
 
 UNMERGED-STAGE is the argument to `git-diff' that that selects
 which stage to diff against in the case of unmerged files."
-  (let ((space-arg (when (numberp ignore-whitespace)
-                     (cond ((> ignore-whitespace 4)
-                            "--ignore-all-space")
-                           ((> ignore-whitespace 1)
-                            "--ignore-space-change"))))
+  (let ((space-arg (stgit-whitespace-diff-arg ignore-whitespace))
         (patch-name (stgit-patch-name-at-point t)))
     (stgit-capture-output "*StGit patch*"
       (case (get-text-property (point) 'entry-type)
         (patch-name (stgit-patch-name-at-point t)))
     (stgit-capture-output "*StGit patch*"
       (case (get-text-property (point) 'entry-type)
@@ -1515,12 +1788,12 @@ which stage to diff against in the case of unmerged files."
          (let* ((patched-file (stgit-patched-file-at-point))
                 (patch-id (let ((id (stgit-id patch-name)))
                             (if (and (eq id :index)
          (let* ((patched-file (stgit-patched-file-at-point))
                 (patch-id (let ((id (stgit-id patch-name)))
                             (if (and (eq id :index)
-                                     (eq (stgit-file-status patched-file)
+                                     (eq (stgit-file->status patched-file)
                                          'unmerged))
                                 :work
                               id)))
                 (args (append (and space-arg (list space-arg))
                                          'unmerged))
                                 :work
                               id)))
                 (args (append (and space-arg (list space-arg))
-                              (and (stgit-file-cr-from patched-file)
+                              (and (stgit-file->cr-from patched-file)
                                    (list (stgit-find-copies-harder-diff-arg)))
                               (cond ((eq patch-id :index)
                                      '("--cached"))
                                    (list (stgit-find-copies-harder-diff-arg)))
                               (cond ((eq patch-id :index)
                                      '("--cached"))
@@ -1529,10 +1802,10 @@ which stage to diff against in the case of unmerged files."
                                     (t
                                      (list (concat patch-id "^") patch-id)))
                               '("--")
                                     (t
                                      (list (concat patch-id "^") patch-id)))
                               '("--")
-                              (if (stgit-file-copy-or-rename patched-file)
-                                  (list (stgit-file-cr-from patched-file)
-                                        (stgit-file-cr-to patched-file))
-                                (list (stgit-file-file patched-file))))))
+                              (if (stgit-file->copy-or-rename patched-file)
+                                  (list (stgit-file->cr-from patched-file)
+                                        (stgit-file->cr-to patched-file))
+                                (list (stgit-file->file patched-file))))))
            (apply 'stgit-run-git "diff" args)))
         ('patch
          (let* ((patch-id (stgit-id patch-name)))
            (apply 'stgit-run-git "diff" args)))
         ('patch
          (let* ((patch-id (stgit-id patch-name)))
@@ -1583,6 +1856,35 @@ greater than four (e.g., \\[universal-argument] \
                    "--cc"
                    "show a combined diff")
 
                    "--cc"
                    "show a combined diff")
 
+(defun stgit-diff-range (&optional ignore-whitespace)
+  "Show diff for the range of patches between point and the marked patch.
+
+With a prefix argument, ignore whitespace. With a prefix argument
+greater than four (e.g., \\[universal-argument] \
+\\[universal-argument] \\[stgit-diff-range]), ignore all whitespace."
+  (interactive "p")
+  (stgit-assert-mode)
+  (unless (= (length stgit-marked-patches) 1)
+    (error "Need exactly one patch marked"))
+  (let* ((patches (stgit-sort-patches (cons (stgit-patch-name-at-point t t)
+                                            stgit-marked-patches)
+                                      t))
+         (first-patch (car patches))
+         (second-patch (if (cdr patches) (cadr patches) first-patch))
+         (whitespace-arg (stgit-whitespace-diff-arg ignore-whitespace))
+         (applied (stgit-applied-patchsyms t)))
+    (unless (and (memq first-patch applied) (memq second-patch applied))
+      (error "Can only show diff range for applied patches"))
+    (stgit-capture-output (format "*StGit diff %s..%s*"
+                                  first-patch second-patch)
+      (apply 'stgit-run-git (append '("diff" "--patch-with-stat")
+                                    (and whitespace-arg (list whitespace-arg))
+                                    (list (format "%s^" (stgit-id first-patch))
+                                          (stgit-id second-patch))))
+      (with-current-buffer standard-output
+        (goto-char (point-min))
+        (diff-mode)))))
+
 (defun stgit-move-change-to-index (file &optional force)
   "Copies the work tree state of FILE to index, using git add or git rm.
 
 (defun stgit-move-change-to-index (file &optional force)
   "Copies the work tree state of FILE to index, using git add or git rm.
 
@@ -1616,23 +1918,23 @@ file ended up. You can then jump to the file with \
   (stgit-assert-mode)
   (let* ((patched-file   (or (stgit-patched-file-at-point)
                             (error "No file on the current line")))
   (stgit-assert-mode)
   (let* ((patched-file   (or (stgit-patched-file-at-point)
                             (error "No file on the current line")))
-        (patched-status (stgit-file-status patched-file)))
+        (patched-status (stgit-file->status patched-file)))
     (when (eq patched-status 'unmerged)
       (error (substitute-command-keys "Use \\[stgit-resolve-file] to move an unmerged file to the index")))
     (let* ((patch      (stgit-patch-at-point))
     (when (eq patched-status 'unmerged)
       (error (substitute-command-keys "Use \\[stgit-resolve-file] to move an unmerged file to the index")))
     (let* ((patch      (stgit-patch-at-point))
-           (patch-name (stgit-patch-name patch))
+           (patch-name (stgit-patch->name patch))
            (mark-file  (if (eq patched-status 'rename)
            (mark-file  (if (eq patched-status 'rename)
-                          (stgit-file-cr-to patched-file)
-                        (stgit-file-file patched-file)))
+                          (stgit-file->cr-to patched-file)
+                        (stgit-file->file patched-file)))
            (point-file  (if (eq patched-status 'rename)
            (point-file  (if (eq patched-status 'rename)
-                            (stgit-file-cr-from patched-file)
+                            (stgit-file->cr-from patched-file)
                           (stgit-neighbour-file))))
 
       (cond ((eq patch-name :work)
                           (stgit-neighbour-file))))
 
       (cond ((eq patch-name :work)
-             (stgit-move-change-to-index (stgit-file-file patched-file)
+             (stgit-move-change-to-index (stgit-file->file patched-file)
                                          (eq patched-status 'ignore)))
             ((eq patch-name :index)
                                          (eq patched-status 'ignore)))
             ((eq patch-name :index)
-             (stgit-remove-change-from-index (stgit-file-file patched-file)))
+             (stgit-remove-change-from-index (stgit-file->file patched-file)))
             (t
              (error "Can only move files between working tree and index")))
       (stgit-refresh-worktree)
             (t
              (error "Can only move files between working tree and index")))
       (stgit-refresh-worktree)
@@ -1681,7 +1983,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))
     (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)
 
 (defun stgit-confirm-edit ()
   (interactive)
@@ -1784,19 +2087,28 @@ the work tree and index."
   "Return the patchsym indicating a target patch for
 `stgit-move-patches'.
 
   "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))))
-
-(defun stgit-sort-patches (patchsyms)
+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 &optional allow-duplicates)
   "Returns the list of patches in PATCHSYMS sorted according to
 their position in the patch series, bottommost first.
 
   "Returns the list of patches in PATCHSYMS sorted according to
 their position in the patch series, bottommost first.
 
-PATCHSYMS must not contain duplicate entries."
+PATCHSYMS must not contain duplicate entries, unless
+ALLOW-DUPLICATES is not nil."
   (let (sorted-patchsyms
         (series (with-output-to-string
                   (with-current-buffer standard-output
   (let (sorted-patchsyms
         (series (with-output-to-string
                   (with-current-buffer standard-output
@@ -1809,8 +2121,9 @@ PATCHSYMS must not contain duplicate entries."
       (setq start (match-end 0)))
     (setq sorted-patchsyms (nreverse sorted-patchsyms))
 
       (setq start (match-end 0)))
     (setq sorted-patchsyms (nreverse sorted-patchsyms))
 
-    (unless (= (length patchsyms) (length sorted-patchsyms))
-      (error "Internal error"))
+    (unless allow-duplicates
+      (unless (= (length patchsyms) (length sorted-patchsyms))
+        (error "Internal error")))
 
     sorted-patchsyms))
 
 
     sorted-patchsyms))
 
@@ -1829,20 +2142,17 @@ Interactively, move the marked patches to where the point is."
   (unless target-patch
     (error "Point not at a patch"))
 
   (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)
   (stgit-reload))
 
 (defun stgit-squash (patchsyms)
@@ -1864,8 +2174,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))
     (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
 
       ;; stg squash may have reordered the patches or caused conflicts
       (with-current-buffer stgit-buffer
@@ -1899,18 +2210,29 @@ deepest patch had before the squash."
   (interactive)
   (describe-function 'stgit-mode))
 
   (interactive)
   (describe-function 'stgit-mode))
 
+(defun stgit-undo-or-redo (redo hard)
+  "Run stg undo or, if REDO is non-nil, stg redo.
+
+If HARD is non-nil, use the --hard flag."
+  (stgit-assert-mode)
+  (let ((cmd (if redo "redo" "undo")))
+    (stgit-capture-output nil
+      (if arg
+          (when (or (and (stgit-index-empty-p)
+                         (stgit-work-tree-empty-p))
+                    (y-or-n-p (format "Hard %s may overwrite index/work tree changes. Continue? "
+                                      cmd)))
+            (stgit-run cmd "--hard"))
+        (stgit-run cmd))))
+  (stgit-reload))
+
 (defun stgit-undo (&optional arg)
   "Run stg undo.
 With prefix argument, run it with the --hard flag.
 
 See also `stgit-redo'."
   (interactive "P")
 (defun stgit-undo (&optional arg)
   "Run stg undo.
 With prefix argument, run it with the --hard flag.
 
 See also `stgit-redo'."
   (interactive "P")
-  (stgit-assert-mode)
-  (stgit-capture-output nil
-    (if arg
-        (stgit-run "undo" "--hard")
-      (stgit-run "undo")))
-  (stgit-reload))
+  (stgit-undo-or-redo nil arg))
 
 (defun stgit-redo (&optional arg)
   "Run stg redo.
 
 (defun stgit-redo (&optional arg)
   "Run stg redo.
@@ -1918,12 +2240,7 @@ With prefix argument, run it with the --hard flag.
 
 See also `stgit-undo'."
   (interactive "P")
 
 See also `stgit-undo'."
   (interactive "P")
-  (stgit-assert-mode)
-  (stgit-capture-output nil
-    (if arg
-        (stgit-run "redo" "--hard")
-      (stgit-run "redo")))
-  (stgit-reload))
+  (stgit-undo-or-redo t arg))
 
 (defun stgit-refresh (&optional arg)
   "Run stg refresh.
 
 (defun stgit-refresh (&optional arg)
   "Run stg refresh.
@@ -1956,6 +2273,9 @@ See also `stgit-show-worktree-mode'.")
 (defvar stgit-show-unknown nil
   "If nil, inhibit showing files not registered with git.")
 
 (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.
 (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 +2318,17 @@ Use \\[stgit-toggle-worktree] to show the work tree."
           (not stgit-show-unknown)))
   (stgit-reload))
 
           (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)
 (provide 'stgit)