stgit.el: Document stgit-select properly
[stgit] / contrib / stgit.el
index 7e0c10d..3d6be1f 100644 (file)
@@ -68,8 +68,12 @@ directory DIR or `default-directory'"
         (start (point))
         (name (stgit-patch-name patch)))
     (case name
-       (:index (insert (propertize "  Index" 'face 'italic)))
-       (:work (insert (propertize "  Work tree" 'face 'italic)))
+       (:index (insert "  "
+                       (propertize "Index"
+                                   'face 'stgit-index-work-tree-title-face)))
+       (:work  (insert "  "
+                       (propertize "Work tree"
+                                   'face 'stgit-index-work-tree-title-face)))
        (t (insert (case status
                     ('applied "+")
                     ('top ">")
@@ -164,8 +168,8 @@ Returns nil if there was no output."
   "Returns non-nil if the index contains no changes from HEAD."
   (zerop (stgit-run-git-silent "diff-index" "--cached" "--quiet" "HEAD")))
 
-(defvar stgit-index-node nil)
-(defvar stgit-worktree-node nil)
+(defvar stgit-index-node)
+(defvar stgit-worktree-node)
 
 (defun stgit-refresh-index ()
   (when stgit-index-node
@@ -175,15 +179,36 @@ Returns nil if there was no output."
   (when stgit-worktree-node
     (ewoc-invalidate (car stgit-worktree-node) (cdr stgit-worktree-node))))
 
+(defun stgit-run-series-insert-index (ewoc)
+  (setq index-node    (cons ewoc (ewoc-enter-last ewoc
+                                                  (make-stgit-patch
+                                                   :status 'index
+                                                   :name :index
+                                                   :desc nil
+                                                   :empty nil)))
+        worktree-node (cons ewoc (ewoc-enter-last ewoc
+                                                  (make-stgit-patch
+                                                   :status 'work
+                                                   :name :work
+                                                   :desc nil
+                                                   :empty nil)))))
+
 (defun stgit-run-series (ewoc)
-  (let ((first-line t))
+  (setq stgit-index-node nil
+        stgit-worktree-node nil)
+  (let ((inserted-index (not stgit-show-worktree))
+        index-node
+        worktree-node
+        all-patchsyms)
     (with-temp-buffer
       (let ((exit-status (stgit-run-silent "series" "--description" "--empty")))
         (goto-char (point-min))
         (if (not (zerop exit-status))
             (cond ((looking-at "stg series: \\(.*\\)")
+                   (setq inserted-index t)
                    (ewoc-set-hf ewoc (car (ewoc-get-hf ewoc))
-                                "-- not initialized (run M-x stgit-init)"))
+                                (substitute-command-keys
+                                 "-- not initialized; run \\[stgit-init]")))
                   ((looking-at ".*")
                    (error "Error running stg: %s"
                           (match-string 0))))
@@ -194,30 +219,30 @@ Returns nil if there was no output."
             (let* ((state-str (match-string 2))
                    (state (cond ((string= state-str ">") 'top)
                                 ((string= state-str "+") 'applied)
-                                ((string= state-str "-") 'unapplied))))
+                                ((string= state-str "-") 'unapplied)))
+                   (name (intern (match-string 4)))
+                   (desc (match-string 5))
+                   (empty (string= (match-string 1) "0")))
+              (unless inserted-index
+                (when (or (eq stgit-show-worktree-mode 'top)
+                          (and (eq stgit-show-worktree-mode 'center)
+                               (eq state 'unapplied)))
+                  (setq inserted-index t)
+                  (stgit-run-series-insert-index ewoc)))
+              (setq all-patchsyms (cons name all-patchsyms))
               (ewoc-enter-last ewoc
                                (make-stgit-patch
                                 :status state
-                                :name (intern (match-string 4))
-                                :desc (match-string 5)
-                                :empty (string= (match-string 1) "0"))))
-            (setq first-line nil)
-            (forward-line 1)))))
-    (if stgit-show-worktree
-        (setq stgit-index-node (cons ewoc (ewoc-enter-last ewoc
-                                                           (make-stgit-patch
-                                                            :status 'index
-                                                            :name :index
-                                                            :desc nil
-                                                            :empty nil)))
-              stgit-worktree-node (cons ewoc (ewoc-enter-last ewoc
-                                                              (make-stgit-patch
-                                                               :status 'work
-                                                               :name :work
-                                                               :desc nil
-                                                               :empty nil))))
-      (setq stgit-worktree-node nil))))
-
+                                :name   name
+                                :desc   desc
+                                :empty  empty)))
+            (forward-line 1))))
+      (unless inserted-index
+        (stgit-run-series-insert-index ewoc)))
+    (setq stgit-index-node    index-node
+          stgit-worktree-node worktree-node
+          stgit-marked-patches (intersection stgit-marked-patches
+                                             all-patchsyms))))
 
 (defun stgit-reload ()
   "Update the contents of the StGit buffer."
@@ -232,7 +257,7 @@ Returns nil if there was no output."
                           (with-temp-buffer
                             (stgit-run-silent "branch")
                             (buffer-substring (point-min) (1- (point-max))))
-                          'face 'bold)
+                          'face 'stgit-branch-name-face)
                          "\n")
                  (if stgit-show-worktree
                      "--"
@@ -256,6 +281,11 @@ Returns nil if there was no output."
   "The face used for StGit descriptions"
   :group 'stgit)
 
+(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"))
@@ -301,6 +331,13 @@ Returns nil if there was no output."
   "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-expand-find-copies-harder
   nil
   "Try harder to find copied files when listing patches.
@@ -434,18 +471,21 @@ Cf. `stgit-file-type-change-string'."
                          (list 'entry-type 'file
                                'file-data file))))
 
+(defun stgit-find-copies-harder-diff-arg ()
+  "Return the flag to use with `git-diff' depending on the
+`stgit-expand-find-copies-harder' flag."
+  (if stgit-expand-find-copies-harder
+      "--find-copies-harder"
+    "-C"))
+
 (defun stgit-insert-patch-files (patch)
   "Expand (show modification of) the patch PATCH after the line
 at point."
   (let* ((patchsym (stgit-patch-name patch))
          (end (progn (insert "#") (prog1 (point-marker) (forward-char -1))))
-         (args (list "-z" (if stgit-expand-find-copies-harder
-                              "--find-copies-harder"
-                            "-C")))
+         (args (list "-z" (stgit-find-copies-harder-diff-arg)))
          (ewoc (ewoc-create #'stgit-file-pp nil nil t)))
     (setf (stgit-patch-files-ewoc patch) ewoc)
-    (when (eq patchsym :work)
-      (setq stgit-work-ewoc ewoc))
     (with-temp-buffer
       (apply 'stgit-run-git
              (cond ((eq patchsym :work)
@@ -464,15 +504,21 @@ at point."
           (let ((file
                  (cond ((looking-at
                          "\\([CR]\\)\\([0-9]*\\)\0\\([^\0]*\\)\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)
-                         :status         (stgit-file-status-code (match-string 1))
-                         :file           (match-string 3)))
+                        (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
@@ -481,7 +527,8 @@ at point."
                          :cr-score       nil
                          :cr-from        nil
                          :cr-to          nil
-                         :status         (stgit-file-status-code (match-string 1))
+                         :status         (stgit-file-status-code
+                                          (match-string 1))
                          :file           (match-string 2))))))
             (ewoc-enter-last ewoc file))
           (goto-char (match-end 0))))
@@ -507,7 +554,10 @@ at point."
   (move-to-column (stgit-goal-column)))
 
 (defun stgit-select ()
-  "Expand or collapse the current entry"
+  "With point on a patch, toggle showing files in the patch.
+
+With point on a file, open the associated file. Opens the target
+file for (applied) copies and renames."
   (interactive)
   (case (get-text-property (point) 'entry-type)
     ('patch
@@ -603,16 +653,17 @@ at point."
             ("\M-{" .     stgit-previous-patch)
             ("\M-}" .     stgit-next-patch)
             ("s" .        stgit-git-status)
-            ("g" .        stgit-reload)
+            ("g" .        stgit-reload-or-repair)
             ("r" .        stgit-refresh)
             ("\C-c\C-r" . stgit-rename)
             ("e" .        stgit-edit)
             ("M" .        stgit-move-patches)
             ("S" .        stgit-squash)
             ("N" .        stgit-new)
-            ("R" .        stgit-repair)
-            ("C" .        stgit-commit)
-            ("U" .        stgit-uncommit)
+            ("\C-c\C-c" . stgit-commit)
+            ("\C-c\C-u" . stgit-uncommit)
+            ("U" .        stgit-revert-file)
+            ("R" .        stgit-resolve-file)
             ("\r" .       stgit-select)
             ("o" .        stgit-find-file-other-window)
             ("i" .        stgit-file-toggle-index)
@@ -640,8 +691,10 @@ Commands:
   (use-local-map stgit-mode-map)
   (set (make-local-variable 'list-buffers-directory) default-directory)
   (set (make-local-variable 'stgit-marked-patches) nil)
-  (set (make-local-variable 'stgit-expanded-patches) nil)
+  (set (make-local-variable 'stgit-expanded-patches) (list :work :index))
   (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-variable 'truncate-lines 't)
   (add-hook 'after-save-hook 'stgit-update-saved-file)
   (run-hooks 'stgit-mode-hook))
@@ -671,10 +724,16 @@ Commands:
 (defun stgit-patch-at-point (&optional cause-error)
   (get-text-property (point) 'patch-data))
 
-(defun stgit-patch-name-at-point (&optional cause-error)
+(defun stgit-patch-name-at-point (&optional cause-error only-patches)
   "Return the patch name on the current line as a symbol.
-If CAUSE-ERROR is not nil, signal an error if none found."
+If CAUSE-ERROR is not nil, signal an error if none found.
+If ONLY-PATCHES is not nil, only allow real patches, and not
+index or work tree."
   (let ((patch (stgit-patch-at-point)))
+    (and patch
+         only-patches
+         (memq (stgit-patch-status patch) '(work index))
+         (setq patch nil))
     (cond (patch
            (stgit-patch-name patch))
           (cause-error
@@ -714,7 +773,12 @@ If that patch cannot be found, do nothing."
   "Mark the patch under point."
   (interactive)
   (let* ((node (ewoc-locate stgit-ewoc))
-         (patch (ewoc-data node)))
+         (patch (ewoc-data node))
+         (name (stgit-patch-name patch)))
+    (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))
     (ewoc-invalidate stgit-ewoc node))
   (stgit-next-patch))
@@ -740,9 +804,10 @@ If that patch cannot be found, do nothing."
 
 (defun stgit-rename (name)
   "Rename the patch under point to NAME."
-  (interactive (list (read-string "Patch name: "
-                                  (symbol-name (stgit-patch-name-at-point t)))))
-  (let ((old-patchsym (stgit-patch-name-at-point t)))
+  (interactive (list
+                (read-string "Patch name: "
+                             (symbol-name (stgit-patch-name-at-point t t)))))
+  (let ((old-patchsym (stgit-patch-name-at-point t t)))
     (stgit-capture-output nil
       (stgit-run "rename" old-patchsym name))
     (let ((name-sym (intern name)))
@@ -755,6 +820,16 @@ If that patch cannot be found, do nothing."
       (stgit-reload)
       (stgit-goto-patch name-sym))))
 
+(defun stgit-reload-or-repair (repair)
+  "Update the contents of the StGit buffer (`stgit-reload').
+
+With a prefix argument, repair the StGit metadata if the branch
+was modified with git commands (`stgit-repair')."
+  (interactive "P")
+  (if repair
+      (stgit-repair)
+    (stgit-reload)))
+
 (defun stgit-repair ()
   "Run stg repair."
   (interactive)
@@ -787,6 +862,56 @@ Interactively, the prefix argument is used as COUNT."
   (stgit-capture-output nil (stgit-run "commit" "-n" count))
   (stgit-reload))
 
+(defun stgit-revert-file ()
+  "Revert the file at point, which must be in the index or the
+working tree."
+  (interactive)
+  (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))
+                             ((eq file-status 'add)
+                              (stgit-file-file patched-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)))))
+
+    (unless (memq patch-name '(:work :index))
+      (error "No index or working tree file on this line"))
+
+    (let ((nfiles (+ (if rm-file 1 0) (if co-file 1 0))))
+      (when (yes-or-no-p (format "Revert %d file%s? "
+                                 nfiles
+                                 (if (= nfiles 1) "" "s")))
+        (stgit-capture-output nil
+          (when rm-file
+            (stgit-run-git "rm" "-f" "-q" "--" rm-file))
+          (when co-file
+            (stgit-run-git "checkout" "HEAD" co-file)))
+        (stgit-reload)))))
+
+(defun stgit-resolve-file ()
+  "Resolve conflict in the file at point."
+  (interactive)
+  (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))))
+
+    (unless (memq patch-name '(:work :index))
+      (error "No index or working tree file on this line"))
+
+    (unless (eq status 'unmerged)
+      (error "No conflict to resolve at the current line"))
+
+    (stgit-capture-output nil
+      (stgit-move-change-to-index (stgit-file-file patched-file)))
+
+    (stgit-reload)))
+
 (defun stgit-uncommit (count)
   "Run stg uncommit on COUNT commits.
 Interactively, the prefix argument is used as COUNT."
@@ -854,9 +979,7 @@ If PATCHSYM is a keyword, returns PATCHSYM unmodified."
               (patch-name (stgit-patch-name-at-point))
               (patch-id (stgit-id patch-name))
               (args (append (and (stgit-file-cr-from patched-file)
-                                 (if stgit-expand-find-copies-harder
-                                     '("--find-copies-harder")
-                                   '("-C")))
+                                 (list (stgit-find-copies-harder-diff-arg)))
                             (cond ((eq patch-id :index)
                                    '("--cached"))
                                   ((eq patch-id :work)
@@ -870,21 +993,29 @@ If PATCHSYM is a keyword, returns PATCHSYM unmodified."
                                 (list (stgit-file-file patched-file))))))
          (apply 'stgit-run-git "diff" args)))
       ('patch
-       (stgit-run "show" "-O" "--patch-with-stat" "-O" "-M"
-                  (stgit-patch-name-at-point)))
+       (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)
+                    (and (eq patch-id :index)
+                         '("--cached")))
+           (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-move-change-to-index (file status)
+(defun stgit-move-change-to-index (file)
   "Copies the workspace state of FILE to index, using git add or git rm"
-  (let ((op (if (file-exists-p file) "add" "rm")))
+  (let ((op (if (or (file-exists-p file) (file-symlink-p file))
+                '("add") '("rm" "-q"))))
     (stgit-capture-output "*git output*"
-      (stgit-run-git op "--" file))))
+      (apply 'stgit-run-git (append op '("--") (list file))))))
 
-(defun stgit-remove-change-from-index (file status)
+(defun stgit-remove-change-from-index (file)
   "Unstages the change in FILE from the index"
   (stgit-capture-output "*git output*"
     (stgit-run-git "reset" "-q" "--" file)))
@@ -895,13 +1026,13 @@ If PATCHSYM is a keyword, returns PATCHSYM unmodified."
   (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)
+      (error (substitute-command-keys "Use \\[stgit-resolve-file] to move an unmerged file to the index")))
     (let ((patch-name (stgit-patch-name-at-point)))
       (cond ((eq patch-name :work)
-             (stgit-move-change-to-index (stgit-file-file patched-file)
-                                         (stgit-file-status patched-file)))
+             (stgit-move-change-to-index (stgit-file-file patched-file)))
             ((eq patch-name :index)
-             (stgit-remove-change-from-index (stgit-file-file patched-file)
-                                             (stgit-file-status patched-file)))
+             (stgit-remove-change-from-index (stgit-file-file patched-file)))
             (t
              (error "Can only move files in the working tree to index")))))
   (stgit-refresh-worktree)
@@ -910,7 +1041,7 @@ If PATCHSYM is a keyword, returns PATCHSYM unmodified."
 (defun stgit-edit ()
   "Edit the patch on the current line."
   (interactive)
-  (let ((patchsym (stgit-patch-name-at-point t))
+  (let ((patchsym (stgit-patch-name-at-point t t))
         (edit-buf (get-buffer-create "*StGit edit*"))
         (dir default-directory))
     (log-edit 'stgit-confirm-edit t nil edit-buf)
@@ -980,6 +1111,11 @@ the work tree and index."
                      current-prefix-arg))
   (unless patchsyms
     (error "No patches to delete"))
+  (when (memq :index patchsyms)
+    (error "Cannot delete the index"))
+  (when (memq :work  patchsyms)
+    (error "Cannot delete the work tree"))
+
   (let ((npatches (length patchsyms)))
     (when (yes-or-no-p (format "Really delete %d patch%s%s? "
                               npatches
@@ -1123,6 +1259,8 @@ With prefix argument, run it with the --hard flag."
 
 (defun stgit-refresh (&optional arg)
   "Run stg refresh.
+If the index contains any changes, only refresh from index.
+
 With prefix argument, refresh the marked patch or the patch under point."
   (interactive "P")
   (let ((patchargs (if arg
@@ -1134,11 +1272,27 @@ With prefix argument, refresh the marked patch or the patch under point."
                                (t
                                 (cons "-p" patches))))
                      nil)))
+    (unless (stgit-index-empty-p)
+      (setq patchargs (cons "--index" patchargs)))
     (stgit-capture-output nil
       (apply 'stgit-run "refresh" patchargs))
     (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.
@@ -1148,15 +1302,18 @@ This value is used as the default value for `stgit-show-worktree'."
   :group 'stgit)
 
 (defvar stgit-show-worktree nil
-  "Show work tree and index in the stgit buffer.
+  "If nil, inhibit showing work tree and index in the stgit buffer.
 
-See `stgit-default-show-worktree' for its default value.")
+See also `stgit-show-worktree-mode'.")
 
 (defun stgit-toggle-worktree (&optional arg)
   "Toggle the visibility of the work tree.
 With arg, show the work tree if arg is positive.
 
-Its initial setting is controlled by `stgit-default-show-worktree'."
+Its initial setting is controlled by `stgit-default-show-worktree'.
+
+`stgit-show-worktree-mode' controls where on screen the index and
+work tree will show up."
   (interactive)
   (setq stgit-show-worktree
         (if (numberp arg)