chiark / gitweb /
stgit.el: Fix problem where standard-output isn't bound correctly
[stgit] / contrib / stgit.el
index e19be3374a1562c545651032bcbca1813252ac0a..bbc7eb1e6bfc56f3b78fcadf4fe89e251c35aa99 100644 (file)
@@ -9,12 +9,17 @@
 ;;
 ;; To start: `M-x stgit'
 
+(when (< emacs-major-version 22)
+  (error "Emacs older than 22 is not supported by stgit.el"))
+
 (require 'git nil t)
 (require 'cl)
 (require 'ewoc)
 
 (defun stgit (dir)
-  "Manage StGit patches for the tree in DIR."
+  "Manage StGit patches for the tree in DIR.
+
+See `stgit-mode' for commands available."
   (interactive "DDirectory: \n")
   (switch-to-stgit-buffer (git-get-top-dir dir))
   (stgit-reload))
@@ -79,7 +84,9 @@ (defun stgit-patch-pp (patch)
         (insert (propertize (if (eq status 'index) "Index" "Work tree")
                             'face face))
       (insert (format "%-30s"
-                      (propertize (symbol-name name) 'face face))
+                      (propertize (symbol-name name)
+                                  'face face
+                                  'syntax-table (string-to-syntax "w")))
               "  "
               (if (stgit-patch-empty patch) "(empty) " "")
               (propertize (or (stgit-patch-desc patch) "")
@@ -194,7 +201,9 @@ (defun stgit-run-series (ewoc)
         worktree-node
         all-patchsyms)
     (with-temp-buffer
-      (let ((exit-status (stgit-run-silent "series" "--description" "--empty")))
+      (let* ((standard-output (current-buffer))
+             (exit-status (stgit-run-silent "series"
+                                            "--description" "--empty")))
         (goto-char (point-min))
         (if (not (zerop exit-status))
             (cond ((looking-at "stg series: \\(.*\\)")
@@ -248,9 +257,9 @@ (defun stgit-reload ()
     (ewoc-set-hf stgit-ewoc
                  (concat "Branch: "
                          (propertize
-                          (with-temp-buffer
-                            (stgit-run-silent "branch")
-                            (buffer-substring (point-min) (1- (point-max))))
+                          (substring (with-output-to-string
+                                       (stgit-run-silent "branch"))
+                                     0 -1)
                           'face 'stgit-branch-name-face)
                          "\n\n")
                  (if stgit-show-worktree
@@ -265,15 +274,58 @@ (defun stgit-reload ()
       (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)
+  :group 'tools
+  :link '(function-link stgit)
+  :link '(url-link "http://www.procode.org/stgit/"))
 
-(defface stgit-description-face
-  '((((background dark)) (:foreground "tan"))
-    (((background light)) (:foreground "dark red")))
-  "The face used for StGit descriptions"
-  :group '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))
@@ -301,10 +353,16 @@ (defface stgit-unapplied-patch-face
   "The face used for unapplied patch names"
   :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"
+(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
@@ -330,20 +388,10 @@ (defface stgit-file-permission-face
   "StGit mode face used for permission changes."
   :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)
-
-
-(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
+(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
@@ -456,21 +504,61 @@ (defun stgit-file-mode-change-string (old-perm new-perm)
 (defstruct (stgit-file)
   old-perm new-perm copy-or-rename cr-score cr-from cr-to status file)
 
+(defun stgit-describe-copy-or-rename (file)
+  (let (from to common-head common-tail
+        (arrow (concat " "
+                       (propertize "->" 'face 'stgit-description-face)
+                       " ")))
+
+    (when stgit-abbreviate-copies-and-renames
+      (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)))
+        (setq common-head (cons (car from) common-head)
+              from        (cdr from)
+              to          (cdr to)))
+      (setq common-head (nreverse common-head)
+            from        (nreverse from)
+            to          (nreverse to))
+      (while (and from to (cdr from) (cdr to)
+                  (string-equal (car from) (car to)))
+        (setq common-tail (cons (car from) common-tail)
+              from        (cdr from)
+              to          (cdr to)))
+      (setq from (nreverse from)
+            to   (nreverse to)))
+
+    (if (or common-head common-tail)
+        (concat (if common-head
+                    (mapconcat #'identity common-head "/")
+                  "")
+                (if common-head "/" "")
+                (propertize "{" 'face 'stgit-description-face)
+                (mapconcat #'identity from "/")
+                arrow
+                (mapconcat #'identity to "/")
+                (propertize "}" 'face 'stgit-description-face)
+                (if common-tail "/" "")
+                (if common-tail
+                    (mapconcat #'identity common-tail "/")
+                  ""))
+      (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)
-                  (concat (stgit-file-cr-from file)
-                          (propertize " -> "
-                                      'face 'stgit-description-face)
-                          (stgit-file-cr-to 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%1s%s%s\n"
+    (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)
@@ -505,66 +593,68 @@ (defun stgit-insert-patch-files (patch)
     (set-marker-insertion-type end t)
     (setf (stgit-patch-files-ewoc patch) ewoc)
     (with-temp-buffer
-      (apply 'stgit-run-git
-             (cond ((eq patchsym :work)
-                    `("diff-files" "-0" ,@args))
-                   ((eq patchsym :index)
-                    `("diff-index" ,@args "--cached" "HEAD"))
-                   (t
-                    `("diff-tree" ,@args "-r" ,(stgit-id patchsym)))))
-
-      (when (and (eq patchsym :work))
-        (when stgit-show-ignored
-          (stgit-insert-ls-files '("--ignored" "--others") "I"))
-        (when stgit-show-unknown
-          (stgit-insert-ls-files '("--others") "X"))
-        (sort-regexp-fields nil ":[^\0]*\0\\([^\0]*\\)\0" "\\1"
-                            (point-min) (point-max)))
+      (let ((standard-output (current-buffer)))
+        (apply 'stgit-run-git
+               (cond ((eq patchsym :work)
+                      `("diff-files" "-0" ,@args))
+                     ((eq patchsym :index)
+                      `("diff-index" ,@args "--cached" "HEAD"))
+                     (t
+                      `("diff-tree" ,@args "-r" ,(stgit-id patchsym)))))
+
+        (when (and (eq patchsym :work))
+          (when stgit-show-ignored
+            (stgit-insert-ls-files '("--ignored" "--others") "I"))
+          (when stgit-show-unknown
+            (stgit-insert-ls-files '("--others") "X"))
+          (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)))
+        (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 t
-                           :cr-score       (string-to-number (match-string 2))
-                           :cr-from        (match-string 3)
-                           :cr-to          (match-string 4)
+                           :copy-or-rename nil
+                           :cr-score       nil
+                           :cr-from        nil
+                           :cr-to          nil
                            :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))))))
-            (ewoc-enter-last ewoc file))
-          (goto-char (match-end 0))))
-      (unless (ewoc-nth ewoc 0)
-        (ewoc-set-hf ewoc ""
-                     (concat "    "
-                             (propertize "<no files>"
-                                         'face 'stgit-description-face)
-                             "\n"))))
+                           :file           (match-string 2))))))
+              (goto-char (match-end 0))
+              (ewoc-enter-last ewoc file))))
+
+        (unless (ewoc-nth ewoc 0)
+          (ewoc-set-hf ewoc ""
+                       (concat "    "
+                               (propertize "<no files>"
+                                           'face 'stgit-description-face)
+                               "\n")))))
     (goto-char end)))
 
 (defun stgit-find-file (&optional other-window)
@@ -716,17 +806,105 @@ (unless stgit-mode-map
             ("G" .        stgit-goto)
             ("=" .        stgit-diff)
             ("D" .        stgit-delete)
-            ([(control ?/)] . stgit-undo)
+            ([?\C-/] .    stgit-undo)
             ("\C-_" .     stgit-undo)
+            ([?\C-c ?\C-/] . stgit-redo)
+            ("\C-c\C-_" . stgit-redo)
             ("B" .        stgit-branch)
+            ("\C-c\C-b" . stgit-rebase)
             ("t" .        ,toggle-map)
             ("d" .        ,diff-map)
             ("q" .        stgit-quit)))))
 
 (defun stgit-mode ()
   "Major mode for interacting with StGit.
-Commands:
-\\{stgit-mode-map}"
+
+Start StGit using \\[stgit].
+
+Basic commands:
+\\<stgit-mode-map>\
+\\[stgit-help] Show this help text
+\\[stgit-quit] Hide the StGit buffer
+\\[describe-bindings]  Show all key bindings
+
+\\[stgit-reload-or-repair]     Reload the StGit buffer
+\\[universal-argument] \\[stgit-reload-or-repair]      Repair StGit metadata
+
+\\[stgit-undo] Undo most recent StGit operation
+\\[stgit-redo] Undo recent undo
+
+\\[stgit-git-status]   Run `git-status' (if available)
+
+Movement commands:
+\\[stgit-previous-line]        Move to previous line
+\\[stgit-next-line]    Move to next line
+\\[stgit-previous-patch]       Move to previous patch
+\\[stgit-next-patch]   Move to next patch
+
+\\[stgit-mark] Mark patch
+\\[stgit-unmark-up]    Unmark patch and move up
+\\[stgit-unmark-down]  Unmark patch and move down
+
+Commands for patches:
+\\[stgit-select]       Toggle showing changed files in patch
+\\[stgit-refresh]      Refresh patch with changes in index or work tree
+\\[stgit-diff] Show the patch log and diff
+
+\\[stgit-new]  Create a new, empty patch
+\\[stgit-rename]       Rename patch
+\\[stgit-edit] Edit patch description
+\\[stgit-delete]       Delete patch(es)
+
+\\[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-squash]       Squash (meld together) patches
+\\[stgit-move-patches] Move patch(es) to point
+
+\\[stgit-commit]       Commit patch(es)
+\\[stgit-uncommit]     Uncommit patch(es)
+
+Commands for files:
+\\[stgit-select]       Open the file in this window
+\\[stgit-find-file-other-window]       Open the file in another window
+\\[stgit-diff] Show the file's diff
+
+\\[stgit-file-toggle-index]    Toggle change between index and work tree
+\\[stgit-revert-file]  Revert changes to file
+
+Display commands:
+\\[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-base]    Show diff against the merge base
+\\[stgit-diff-ours]    Show diff against our branch
+\\[stgit-diff-theirs]  Show diff against their branch
+
+  With one prefix argument (e.g., \\[universal-argument] \\[stgit-diff]), \
+ignore space changes.
+  With two prefix arguments (e.g., \\[universal-argument] \
+\\[universal-argument] \\[stgit-diff]), ignore all space changes.
+
+Commands for merge conflicts:
+\\[stgit-find-file-merge]      Resolve conflicts using `smerge-ediff'
+\\[stgit-resolve-file] Mark unmerged file as resolved
+
+Commands for branches:
+\\[stgit-branch]       Switch to another branch
+\\[stgit-rebase]       Rebase the current branch
+
+Customization variables:
+`stgit-abbreviate-copies-and-renames'
+`stgit-default-show-worktree'
+`stgit-find-copies-harder'
+`stgit-show-worktree-mode'
+
+See also \\[customize-group] for the \"stgit\" group."
   (kill-all-local-variables)
   (buffer-disable-undo)
   (setq mode-name "StGit"
@@ -739,6 +917,7 @@ (defun stgit-mode ()
   (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)
   (run-hooks 'stgit-mode-hook))
@@ -912,6 +1091,31 @@ (defun stgit-branch (branch)
   (stgit-capture-output nil (stgit-run "branch" "--" branch))
   (stgit-reload))
 
+(defun stgit-available-refs (&optional omit-stgit)
+  "Returns a list of the available git refs.
+If OMIT-STGIT is not nil, filter out \"resf/heads/*.stgit\"."
+  (let* ((output (with-output-to-string
+                   (stgit-run-git-silent "for-each-ref" "--format=%(refname)"
+                                         "refs/tags" "refs/heads"
+                                         "refs/remotes")))
+         (result (split-string output "\n" t)))
+    (mapcar (lambda (s)
+              (if (string-match "^refs/\\(heads\\|tags\\|remotes\\)/" s)
+                  (substring s (match-end 0))
+                s))
+            (if omit-stgit
+                (delete-if (lambda (s)
+                             (string-match "^refs/heads/.*\\.stgit$" s))
+                           result)
+              result))))
+
+(defun stgit-rebase (new-base)
+  "Rebase to NEW-BASE."
+  (interactive (list (completing-read "Rebase to: "
+                                      (stgit-available-refs t))))
+  (stgit-capture-output nil (stgit-run "rebase" new-base))
+  (stgit-reload))
+
 (defun stgit-commit (count)
   "Run stg commit on COUNT commits.
 Interactively, the prefix argument is used as COUNT.
@@ -932,6 +1136,22 @@ (defun stgit-uncommit (count)
     (stgit-capture-output nil (stgit-run "uncommit" "-n" count))
     (stgit-reload)))
 
+(defun stgit-neighbour-file ()
+  "Return the file name of the next file after point, or the
+previous file if point is at the last file within a patch."
+  (let ((old-point (point))
+        neighbour-file)
+    (and (zerop (forward-line 1))
+         (let ((f (stgit-patched-file-at-point)))
+           (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)))
+             (and f (setq neighbour-file (stgit-file-file f)))))
+      (goto-char old-point))
+    neighbour-file))
+
 (defun stgit-revert-file ()
   "Revert the file at point, which must be in the index or the
 working tree."
@@ -947,7 +1167,8 @@ (defun stgit-revert-file ()
          (co-file      (cond ((eq file-status 'rename)
                               (stgit-file-cr-from patched-file))
                              ((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))
       (error "No index or working tree file on this line"))
@@ -967,7 +1188,8 @@ (defun stgit-revert-file ()
             (stgit-run-git "rm" "-f" "-q" "--" rm-file))
           (when co-file
             (stgit-run-git "checkout" "HEAD" co-file)))
-        (stgit-reload)))))
+        (stgit-reload)
+        (stgit-goto-patch patch-name next-file)))))
 
 (defun stgit-resolve-file ()
   "Resolve conflict in the file at point."
@@ -1038,92 +1260,99 @@ (defun stgit-id (patchsym)
        (error "Cannot find commit id for %s" patchsym))
       (match-string 1 result))))
 
-(defun stgit-show-patch (unmerged-stage)
+(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."
-  (stgit-capture-output "*StGit patch*"
-    (case (get-text-property (point) 'entry-type)
-      ('file
-       (let* ((patched-file (stgit-patched-file-at-point))
-              (patch-name (stgit-patch-name-at-point))
-              (patch-id (let ((id (stgit-id patch-name)))
-                          (if (and (eq id :index)
-                                   (eq (stgit-file-status patched-file)
-                                       'unmerged))
-                              :work
-                            id)))
-              (args (append (and (stgit-file-cr-from patched-file)
-                                 (list (stgit-find-copies-harder-diff-arg)))
-                            (cond ((eq patch-id :index)
-                                   '("--cached"))
-                                  ((eq patch-id :work)
-                                   (list unmerged-stage))
-                                  (t
-                                   (list (concat patch-id "^") patch-id)))
-                            '("--")
+  (let ((space-arg (when (numberp ignore-whitespace)
+                     (cond ((> ignore-whitespace 4)
+                            "--ignore-all-space")
+                           ((> ignore-whitespace 1)
+                            "--ignore-space-change"))))
+        (patch-name (stgit-patch-name-at-point t)))
+    (stgit-capture-output "*StGit patch*"
+      (case (get-text-property (point) 'entry-type)
+        ('file
+         (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)
+                                         'unmerged))
+                                :work
+                              id)))
+                (args (append (and space-arg (list space-arg))
+                              (and (stgit-file-cr-from patched-file)
+                                   (list (stgit-find-copies-harder-diff-arg)))
+                              (cond ((eq patch-id :index)
+                                     '("--cached"))
+                                    ((eq patch-id :work)
+                                     (list unmerged-stage))
+                                    (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))))))
-         (apply 'stgit-run-git "diff" args)))
-      ('patch
-       (let* ((patch-name (stgit-patch-name-at-point))
-              (patch-id (stgit-id patch-name)))
-         (if (or (eq patch-id :index) (eq patch-id :work))
-             (apply 'stgit-run-git "diff"
-                    (stgit-find-copies-harder-diff-arg)
-                    (if (eq patch-id :index)
-                        '("--cached")
-                      (list unmerged-stage)))
-           (stgit-run "show" "-O" "--patch-with-stat" "-O" "-M"
-                      (stgit-patch-name-at-point)))))
-      (t
-       (error "No patch or file at point")))
-    (with-current-buffer standard-output
-      (goto-char (point-min))
-      (diff-mode))))
-
-(defun stgit-diff ()
-  "Show the patch on the current line."
-  (interactive)
-  (stgit-show-patch "--ours"))
-
-(defun stgit-diff-ours ()
-  "Show the patch on the current line.
-
-For unmerged files, diff against our branch."
-  (interactive)
-  (stgit-show-patch "--ours"))
-
-(defun stgit-diff-theirs ()
-  "Show the patch on the current line.
-
-For unmerged files, diff against their branch."
-  (interactive)
-  (stgit-show-patch "--theirs"))
-
-(defun stgit-diff-base ()
-  "Show the patch on the current line.
-
-For unmerged files, diff against the base version."
-  (interactive)
-  (stgit-show-patch "--base"))
-
-(defun stgit-diff-combined ()
-  "Show the patch on the current line.
-
-For unmerged files, show a combined diff."
-  (interactive)
-  (stgit-show-patch "--cc"))
-
-(defun stgit-move-change-to-index (file)
-  "Copies the workspace state of FILE to index, using git add or git rm"
+           (apply 'stgit-run-git "diff" args)))
+        ('patch
+         (let* ((patch-id (stgit-id patch-name)))
+           (if (or (eq patch-id :index) (eq patch-id :work))
+               (apply 'stgit-run-git "diff"
+                      (stgit-find-copies-harder-diff-arg)
+                      (append (and space-arg (list space-arg))
+                              (if (eq patch-id :index)
+                                  '("--cached")
+                                (list unmerged-stage))))
+             (let ((args (append '("show" "-O" "--patch-with-stat" "-O" "-M")
+                                 (and space-arg (list "-O" space-arg))
+                                 (list (stgit-patch-name-at-point)))))
+               (apply 'stgit-run args)))))
+         (t
+          (error "No patch or file at point")))
+      (with-current-buffer standard-output
+        (goto-char (point-min))
+        (diff-mode)))))
+
+(defmacro stgit-define-diff (name diff-arg &optional unmerged-action)
+  `(defun ,name (&optional ignore-whitespace)
+     ,(format "Show the patch on the current line.
+
+%sWith a prefix argument, ignore whitespace. With a prefix argument
+greater than four (e.g., \\[universal-argument] \
+\\[universal-argument] \\[%s]), ignore all whitespace."
+              (if unmerged-action
+                  (format "For unmerged files, %s.\n\n" unmerged-action)
+                "")
+              name)
+     (interactive "p")
+     (stgit-show-patch ,diff-arg ignore-whitespace)))
+
+(stgit-define-diff stgit-diff
+                   "--ours" nil)
+(stgit-define-diff stgit-diff-ours
+                   "--ours"
+                   "diff against our branch")
+(stgit-define-diff stgit-diff-theirs
+                   "--theirs"
+                   "diff against their branch")
+(stgit-define-diff stgit-diff-base
+                   "--base"
+                   "diff against the merge base")
+(stgit-define-diff stgit-diff-combined
+                   "--cc"
+                   "show a combined diff")
+
+(defun stgit-move-change-to-index (file &optional force)
+  "Copies the work tree state of FILE to index, using git add or git rm.
+
+If FORCE is not nil, use --force."
   (let ((op (if (or (file-exists-p file) (file-symlink-p file))
                 '("add") '("rm" "-q"))))
     (stgit-capture-output "*git output*"
-      (apply 'stgit-run-git (append op '("--") (list file))))))
+      (apply 'stgit-run-git (append op (and force '("--force"))
+                                    '("--") (list file))))))
 
 (defun stgit-remove-change-from-index (file)
   "Unstages the change in FILE from the index"
@@ -1137,42 +1366,32 @@ (defun stgit-file-toggle-index ()
 file ended up. You can then jump to the file with \
 \\[exchange-point-and-mark]."
   (interactive)
-  (let ((patched-file (stgit-patched-file-at-point)))
-    (unless patched-file
-      (error "No file on the current line"))
-    (when (eq (stgit-file-status patched-file) 'unmerged)
+  (let* ((patched-file   (or (stgit-patched-file-at-point)
+                            (error "No file on the current line")))
+        (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")))
-    (when (eq (stgit-file-status patched-file) 'ignore)
-      (error "You cannot add ignored files to the index"))
     (let* ((patch      (stgit-patch-at-point))
            (patch-name (stgit-patch-name patch))
-           (old-point  (point))
-           next-file)
-
-      ;; find the next file in the patch, or the previous one if this
-      ;; was the last file
-      (and (zerop (forward-line 1))
-           (let ((f (stgit-patched-file-at-point)))
-             (and f (setq next-file (stgit-file-file f)))))
-      (goto-char old-point)
-      (unless next-file
-        (and (zerop (forward-line -1))
-             (let ((f (stgit-patched-file-at-point)))
-               (and f (setq next-file (stgit-file-file f)))))
-        (goto-char old-point))
+           (mark-file  (if (eq patched-status 'rename)
+                          (stgit-file-cr-to patched-file)
+                        (stgit-file-file patched-file)))
+           (point-file  (if (eq patched-status 'rename)
+                          (stgit-file-cr-from patched-file)
+                        (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)
              (stgit-remove-change-from-index (stgit-file-file patched-file)))
             (t
-             (error "Can only move files in the working tree to index")))
+             (error "Can only move files between working tree and index")))
       (stgit-refresh-worktree)
       (stgit-refresh-index)
-      (stgit-goto-patch (if (eq patch-name :index) :work :index)
-                        (stgit-file-file patched-file))
+      (stgit-goto-patch (if (eq patch-name :index) :work :index) mark-file)
       (push-mark nil t t)
-      (stgit-goto-patch patch-name next-file))))
+      (stgit-goto-patch patch-name point-file))))
 
 (defun stgit-edit ()
   "Edit the patch on the current line."
@@ -1273,7 +1492,7 @@ (defun stgit-move-patches-target ()
 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)))
+  (let ((patchsym (stgit-patch-name-at-point nil t)))
     (cond (patchsym patchsym)
          ((save-excursion (re-search-backward "^>" nil t)) :top)
          (t :bottom))))
@@ -1282,7 +1501,7 @@ (defun stgit-sort-patches (patchsyms)
   "Returns the list of patches in PATCHSYMS sorted according to
 their position in the patch series, bottommost first.
 
-PATCHSYMS may not contain duplicate entries."
+PATCHSYMS must not contain duplicate entries."
   (let (sorted-patchsyms
         (series (with-output-to-string
                   (with-current-buffer standard-output
@@ -1385,7 +1604,9 @@ (defun stgit-help ()
 
 (defun stgit-undo (&optional arg)
   "Run stg undo.
-With prefix argument, run it with the --hard flag."
+With prefix argument, run it with the --hard flag.
+
+See also `stgit-redo'."
   (interactive "P")
   (stgit-capture-output nil
     (if arg
@@ -1393,6 +1614,18 @@ (defun stgit-undo (&optional arg)
       (stgit-run "undo")))
   (stgit-reload))
 
+(defun stgit-redo (&optional arg)
+  "Run stg redo.
+With prefix argument, run it with the --hard flag.
+
+See also `stgit-undo'."
+  (interactive "P")
+  (stgit-capture-output nil
+    (if arg
+        (stgit-run "redo" "--hard")
+      (stgit-run "redo")))
+  (stgit-reload))
+
 (defun stgit-refresh (&optional arg)
   "Run stg refresh.
 If the index contains any changes, only refresh from index.
@@ -1415,28 +1648,6 @@ (defun stgit-refresh (&optional arg)
     (stgit-refresh-git-status))
   (stgit-reload))
 
-(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).
-
-See also `stgit-show-worktree'."
-  :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)
-
-(defcustom stgit-default-show-worktree
-  nil
-  "Set to non-nil to by default show the working tree in a new stgit buffer.
-
-This value is used as the default value for `stgit-show-worktree'."
-  :type 'boolean
-  :group 'stgit)
-
 (defvar stgit-show-worktree nil
   "If nil, inhibit showing work tree and index in the stgit buffer.
 
@@ -1450,7 +1661,7 @@ (defvar stgit-show-unknown nil
 
 (defun stgit-toggle-worktree (&optional arg)
   "Toggle the visibility of the work tree.
-With arg, show the work tree if arg is positive.
+With ARG, show the work tree if ARG is positive.
 
 Its initial setting is controlled by `stgit-default-show-worktree'.