stgit.el: Bugfix moving symlinks to/from index
[stgit] / contrib / stgit.el
index 0c39286..c4ed958 100644 (file)
@@ -42,8 +42,8 @@ directory DIR or `default-directory'"
         (with-current-buffer git-status-buffer
           (git-refresh-status))))))
 
-(defun switch-to-stgit-buffer (dir)
-  "Switch to a (possibly new) buffer displaying StGit patches for DIR."
+(defun stgit-find-buffer (dir)
+  "Return the buffer displaying StGit patches for DIR, or nil if none."
   (setq dir (file-name-as-directory dir))
   (let ((buffers (buffer-list)))
     (while (and buffers
@@ -51,9 +51,15 @@ directory DIR or `default-directory'"
                        (and (eq major-mode 'stgit-mode)
                             (string= default-directory dir)))))
       (setq buffers (cdr buffers)))
-    (switch-to-buffer (if buffers
-                          (car buffers)
-                        (create-stgit-buffer dir)))))
+    (and buffers (car buffers))))
+
+(defun switch-to-stgit-buffer (dir)
+  "Switch to a (possibly new) buffer displaying StGit patches for DIR."
+  (setq dir (file-name-as-directory dir))
+  (let ((buffer (stgit-find-buffer dir)))
+    (switch-to-buffer (or buffer
+                         (create-stgit-buffer dir)))))
+
 (defstruct (stgit-patch)
   status name desc empty files-ewoc)
 
@@ -61,22 +67,27 @@ directory DIR or `default-directory'"
   (let ((status (stgit-patch-status patch))
         (start (point))
         (name (stgit-patch-name patch)))
-    (insert (case status
-              ('applied "+")
-              ('top ">")
-              ('unapplied "-")
-              (t "ยท"))
-            (if (memq name stgit-marked-patches)
-                "*" " ")
-            (propertize (format "%-30s" (symbol-name name))
-                        'face (case status
-                                ('applied 'stgit-applied-patch-face)
-                                ('top 'stgit-top-patch-face)
-                                ('unapplied 'stgit-unapplied-patch-face)))
-            "  "
-            (if (stgit-patch-empty patch) "(empty) " "")
-            (propertize (or (stgit-patch-desc patch) "")
-                        'face 'stgit-description-face))
+    (case name
+       (:index (insert (propertize "  Index" 'face 'italic)))
+       (:work (insert (propertize "  Work tree" 'face 'italic)))
+       (t (insert (case status
+                    ('applied "+")
+                    ('top ">")
+                    ('unapplied "-"))
+                  (if (memq name stgit-marked-patches)
+                      "*" " ")
+                  (propertize (format "%-30s"
+                                      (symbol-name name))
+                              'face (case status
+                                      ('applied 'stgit-applied-patch-face)
+                                      ('top 'stgit-top-patch-face)
+                                      ('unapplied 'stgit-unapplied-patch-face)
+                                      ('index nil)
+                                      ('work nil)))
+                  "  "
+                  (if (stgit-patch-empty patch) "(empty) " "")
+                  (propertize (or (stgit-patch-desc patch) "")
+                              'face 'stgit-description-face))))
     (put-text-property start (point) 'entry-type 'patch)
     (when (memq name stgit-expanded-patches)
       (stgit-insert-patch-files patch))
@@ -149,6 +160,21 @@ Returns nil if there was no output."
   (setq args (stgit-make-run-args args))
   (apply 'call-process "git" nil standard-output nil args))
 
+(defun stgit-index-empty-p ()
+  "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)
+
+(defun stgit-refresh-index ()
+  (when stgit-index-node
+    (ewoc-invalidate (car stgit-index-node) (cdr stgit-index-node))))
+
+(defun stgit-refresh-worktree ()
+  (when stgit-worktree-node
+    (ewoc-invalidate (car stgit-worktree-node) (cdr stgit-worktree-node))))
+
 (defun stgit-run-series (ewoc)
   (let ((first-line t))
     (with-temp-buffer
@@ -176,7 +202,21 @@ Returns nil if there was no output."
                                 :desc (match-string 5)
                                 :empty (string= (match-string 1) "0"))))
             (setq first-line nil)
-            (forward-line 1)))))))
+            (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))))
 
 
 (defun stgit-reload ()
@@ -194,7 +234,12 @@ Returns nil if there was no output."
                             (buffer-substring (point-min) (1- (point-max))))
                           'face 'bold)
                          "\n")
-                 "--")
+                 (if stgit-show-worktree
+                     "--"
+                   (propertize
+                    (substitute-command-keys "--\n\"\\[stgit-toggle-worktree]\"\
+ shows the working tree\n")
+                   'face 'stgit-description-face)))
     (stgit-run-series stgit-ewoc)
     (if curpatch
         (stgit-goto-patch curpatch)
@@ -390,23 +435,26 @@ Cf. `stgit-file-type-change-string'."
                                'file-data file))))
 
 (defun stgit-insert-patch-files (patch)
-  "Expand (show modification of) the patch with name PATCHSYM (a
-symbol) after the line at point.
-`stgit-expand-find-copies-harder' controls how hard to try to
-find copied files."
-  (insert "\n")
+  "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 "-r" "-z" (if stgit-expand-find-copies-harder
-                                   "--find-copies-harder"
-                                 "-C")))
+         (args (list "-z" (if stgit-expand-find-copies-harder
+                              "--find-copies-harder"
+                            "-C")))
          (ewoc (ewoc-create #'stgit-file-pp nil nil t)))
     (setf (stgit-patch-files-ewoc patch) ewoc)
     (with-temp-buffer
-      (apply 'stgit-run-git "diff-tree"
-             (append args (list (stgit-id patchsym))))
+      (apply 'stgit-run-git
+             (cond ((eq patchsym :work)
+                    `("diff-files" ,@args))
+                   ((eq patchsym :index)
+                    `("diff-index" ,@args "--cached" "HEAD"))
+                   (t
+                    `("diff-tree" ,@args "-r" ,(stgit-id patchsym)))))
       (goto-char (point-min))
-      (forward-char 41)
+      (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)))
@@ -531,46 +579,52 @@ find copied files."
   "Keymap for StGit major mode.")
 
 (unless stgit-mode-map
-  (setq stgit-mode-map (make-keymap))
-  (suppress-keymap stgit-mode-map)
-  (mapc (lambda (arg) (define-key stgit-mode-map (car arg) (cdr arg)))
-        '((" " .        stgit-mark)
-          ("m" .        stgit-mark)
-          ("\d" .       stgit-unmark-up)
-          ("u" .        stgit-unmark-down)
-          ("?" .        stgit-help)
-          ("h" .        stgit-help)
-          ("\C-p" .     stgit-previous-line)
-          ("\C-n" .     stgit-next-line)
-          ([up] .       stgit-previous-line)
-          ([down] .     stgit-next-line)
-          ("p" .        stgit-previous-patch)
-          ("n" .        stgit-next-patch)
-          ("\M-{" .     stgit-previous-patch)
-          ("\M-}" .     stgit-next-patch)
-          ("s" .        stgit-git-status)
-          ("g" .        stgit-reload)
-          ("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)
-          ("\r" .       stgit-select)
-          ("o" .        stgit-find-file-other-window)
-          (">" .        stgit-push-next)
-          ("<" .        stgit-pop-next)
-          ("P" .        stgit-push-or-pop)
-          ("G" .        stgit-goto)
-          ("=" .        stgit-show)
-          ("D" .        stgit-delete)
-          ([(control ?/)] . stgit-undo)
-          ("\C-_" .     stgit-undo)
-          ("B" .        stgit-branch)
-          ("q" .        stgit-quit))))
+  (let ((toggle-map (make-keymap)))
+    (suppress-keymap toggle-map)
+    (mapc (lambda (arg) (define-key toggle-map (car arg) (cdr arg)))
+          '(("t" .        stgit-toggle-worktree)))
+    (setq stgit-mode-map (make-keymap))
+    (suppress-keymap stgit-mode-map)
+    (mapc (lambda (arg) (define-key stgit-mode-map (car arg) (cdr arg)))
+          `((" " .        stgit-mark)
+            ("m" .        stgit-mark)
+            ("\d" .       stgit-unmark-up)
+            ("u" .        stgit-unmark-down)
+            ("?" .        stgit-help)
+            ("h" .        stgit-help)
+            ("\C-p" .     stgit-previous-line)
+            ("\C-n" .     stgit-next-line)
+            ([up] .       stgit-previous-line)
+            ([down] .     stgit-next-line)
+            ("p" .        stgit-previous-patch)
+            ("n" .        stgit-next-patch)
+            ("\M-{" .     stgit-previous-patch)
+            ("\M-}" .     stgit-next-patch)
+            ("s" .        stgit-git-status)
+            ("g" .        stgit-reload)
+            ("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)
+            ("\r" .       stgit-select)
+            ("o" .        stgit-find-file-other-window)
+            ("i" .        stgit-file-toggle-index)
+            (">" .        stgit-push-next)
+            ("<" .        stgit-pop-next)
+            ("P" .        stgit-push-or-pop)
+            ("G" .        stgit-goto)
+            ("=" .        stgit-show)
+            ("D" .        stgit-delete)
+            ([(control ?/)] . stgit-undo)
+            ("\C-_" .     stgit-undo)
+            ("B" .        stgit-branch)
+            ("t" .        ,toggle-map)
+            ("q" .        stgit-quit)))))
 
 (defun stgit-mode ()
   "Major mode for interacting with StGit.
@@ -585,9 +639,21 @@ Commands:
   (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-show-worktree) stgit-default-show-worktree)
   (set-variable 'truncate-lines 't)
+  (add-hook 'after-save-hook 'stgit-update-saved-file)
   (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)))
+        (buffer (and gitdir (stgit-find-buffer gitdir))))
+    (when buffer
+      (with-current-buffer buffer
+        (stgit-refresh-worktree)))))
+
 (defun stgit-add-mark (patchsym)
   "Mark the patch PATCHSYM."
   (setq stgit-marked-patches (cons patchsym stgit-marked-patches)))
@@ -789,7 +855,12 @@ If PATCHSYM is a keyword, returns PATCHSYM unmodified."
                                  (if stgit-expand-find-copies-harder
                                      '("--find-copies-harder")
                                    '("-C")))
-                            (list (concat patch-id "^") patch-id)
+                            (cond ((eq patch-id :index)
+                                   '("--cached"))
+                                  ((eq patch-id :work)
+                                   nil)
+                                  (t
+                                   (list (concat patch-id "^") patch-id)))
                             '("--")
                               (if (stgit-file-copy-or-rename patched-file)
                                   (list (stgit-file-cr-from patched-file)
@@ -805,6 +876,34 @@ If PATCHSYM is a keyword, returns PATCHSYM unmodified."
       (goto-char (point-min))
       (diff-mode))))
 
+(defun stgit-move-change-to-index (file)
+  "Copies the workspace state of FILE to index, using git add or git rm"
+  (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))))))
+
+(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)))
+
+(defun stgit-file-toggle-index ()
+  "Move modified file in or out of the index."
+  (interactive)
+  (let ((patched-file (stgit-patched-file-at-point)))
+    (unless patched-file
+      (error "No file on the current line"))
+    (let ((patch-name (stgit-patch-name-at-point)))
+      (cond ((eq patch-name :work)
+             (stgit-move-change-to-index (stgit-file-file patched-file)))
+            ((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")))))
+  (stgit-refresh-worktree)
+  (stgit-refresh-index))
+
 (defun stgit-edit ()
   "Edit the patch on the current line."
   (interactive)
@@ -1037,4 +1136,29 @@ With prefix argument, refresh the marked patch or the patch under point."
     (stgit-refresh-git-status))
   (stgit-reload))
 
+(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
+  "Show work tree and index in the stgit buffer.
+
+See `stgit-default-show-worktree' for its default value.")
+
+(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'."
+  (interactive)
+  (setq stgit-show-worktree
+        (if (numberp arg)
+            (> arg 0)
+          (not stgit-show-worktree)))
+  (stgit-reload))
+
 (provide 'stgit)