Code

Merge branch 'js/maint-daemon' into maint
[git.git] / contrib / emacs / git.el
index 280557ecd4b0065ecc6c26cd8fda77906655fe75..0312d891fd1ddfe87f452611c24af7178313092e 100644 (file)
@@ -36,7 +36,6 @@
 ;; TODO
 ;;  - portability to XEmacs
 ;;  - better handling of subprocess errors
-;;  - hook into file save (after-save-hook)
 ;;  - diff against other branch
 ;;  - renaming files from the status buffer
 ;;  - creating tags
@@ -50,6 +49,7 @@
 (eval-when-compile (require 'cl))
 (require 'ewoc)
 (require 'log-edit)
+(require 'easymenu)
 
 
 ;;;; Customizations
@@ -97,6 +97,21 @@ if there is already one that displays the same directory."
   :group 'git
   :type 'string)
 
+(defcustom git-show-uptodate nil
+  "Whether to display up-to-date files."
+  :group 'git
+  :type 'boolean)
+
+(defcustom git-show-ignored nil
+  "Whether to display ignored files."
+  :group 'git
+  :type 'boolean)
+
+(defcustom git-show-unknown t
+  "Whether to display unknown files."
+  :group 'git
+  :type 'boolean)
+
 
 (defface git-status-face
   '((((class color) (background light)) (:foreground "purple"))
@@ -205,22 +220,15 @@ and returns the process output as a string."
     (message "Running git %s...done" (car args))
     buffer))
 
-(defun git-run-command (buffer env &rest args)
-  (message "Running git %s..." (car args))
-  (apply #'git-call-process-env buffer env args)
-  (message "Running git %s...done" (car args)))
-
 (defun git-run-command-region (buffer start end env &rest args)
   "Run a git command with specified buffer region as input."
-  (message "Running git %s..." (car args))
   (unless (eq 0 (if env
                     (git-run-process-region
                      buffer start end "env"
                      (append (git-get-env-strings env) (list "git") args))
                   (git-run-process-region
                    buffer start end "git" args)))
-    (error "Failed to run \"git %s\":\n%s" (mapconcat (lambda (x) x) args " ") (buffer-string)))
-  (message "Running git %s...done" (car args)))
+    (error "Failed to run \"git %s\":\n%s" (mapconcat (lambda (x) x) args " ") (buffer-string))))
 
 (defun git-run-hook (hook env &rest args)
   "Run a git hook and display its output if any."
@@ -297,6 +305,13 @@ and returns the process output as a string."
               "\"")
     name))
 
+(defun git-success-message (text files)
+  "Print a success message after having handled FILES."
+  (let ((n (length files)))
+    (if (equal n 1)
+        (message "%s %s" text (car files))
+      (message "%s %d files" text n))))
+
 (defun git-get-top-dir (dir)
   "Retrieve the top-level directory of a git tree."
   (let ((cdup (with-output-to-string
@@ -323,7 +338,7 @@ and returns the process output as a string."
     (sort-lines nil (point-min) (point-max))
     (save-buffer))
   (when created
-    (git-run-command nil nil "update-index" "--add" "--" (file-relative-name ignore-name)))
+    (git-call-process-env nil nil "update-index" "--add" "--" (file-relative-name ignore-name)))
   (git-update-status-files (list (file-relative-name ignore-name)) 'unknown)))
 
 ; propertize definition for XEmacs, stolen from erc-compat
@@ -470,14 +485,35 @@ and returns the process output as a string."
   "Remove everything from the status list."
   (ewoc-filter status (lambda (info) nil)))
 
-(defun git-set-files-state (files state)
-  "Set the state of a list of files."
-  (dolist (info files)
-    (unless (eq (git-fileinfo->state info) state)
-      (setf (git-fileinfo->state info) state)
-      (setf (git-fileinfo->rename-state info) nil)
-      (setf (git-fileinfo->orig-name info) nil)
-      (setf (git-fileinfo->needs-refresh info) t))))
+(defun git-set-fileinfo-state (info state)
+  "Set the state of a file info."
+  (unless (eq (git-fileinfo->state info) state)
+    (setf (git-fileinfo->state info) state
+         (git-fileinfo->new-perm info) (git-fileinfo->old-perm info)
+          (git-fileinfo->rename-state info) nil
+          (git-fileinfo->orig-name info) nil
+          (git-fileinfo->needs-refresh info) t)))
+
+(defun git-status-filenames-map (status func files &rest args)
+  "Apply FUNC to the status files names in the FILES list."
+  (when files
+    (setq files (sort files #'string-lessp))
+    (let ((file (pop files))
+          (node (ewoc-nth status 0)))
+      (while (and file node)
+        (let ((info (ewoc-data node)))
+          (if (string-lessp (git-fileinfo->name info) file)
+              (setq node (ewoc-next status node))
+            (if (string-equal (git-fileinfo->name info) file)
+                (apply func info args))
+            (setq file (pop files))))))))
+
+(defun git-set-filenames-state (status files state)
+  "Set the state of a list of named files."
+  (when files
+    (git-status-filenames-map status #'git-set-fileinfo-state files state)
+    (unless state  ;; delete files whose state has been set to nil
+      (ewoc-filter status (lambda (info) (git-fileinfo->state info))))))
 
 (defun git-state-code (code)
   "Convert from a string to a added/deleted/modified state."
@@ -487,6 +523,7 @@ and returns the process output as a string."
     (?A 'added)
     (?D 'deleted)
     (?U 'unmerged)
+    (?T 'modified)
     (t nil)))
 
 (defun git-status-code-as-string (code)
@@ -501,6 +538,33 @@ and returns the process output as a string."
     ('ignored  (propertize "Ignored " 'face 'git-ignored-face))
     (t "?       ")))
 
+(defun git-file-type-as-string (old-perm new-perm)
+  "Return a string describing the file type based on its permissions."
+  (let* ((old-type (lsh (or old-perm 0) -9))
+        (new-type (lsh (or new-perm 0) -9))
+        (str (case new-type
+               (?\100  ;; file
+                (case old-type
+                  (?\100 nil)
+                  (?\120 "   (type change symlink -> file)")
+                  (?\160 "   (type change subproject -> file)")))
+                (?\120  ;; symlink
+                 (case old-type
+                   (?\100 "   (type change file -> symlink)")
+                   (?\160 "   (type change subproject -> symlink)")
+                   (t "   (symlink)")))
+                 (?\160  ;; subproject
+                  (case old-type
+                    (?\100 "   (type change file -> subproject)")
+                    (?\120 "   (type change symlink -> subproject)")
+                    (t "   (subproject)")))
+                 (?\000  ;; deleted or unknown
+                  (case old-type
+                    (?\120 "   (symlink)")
+                    (?\160 "   (subproject)")))
+                 (t (format "   (unknown type %o)" new-type)))))
+    (if str (propertize str 'face 'git-status-face) "")))
+
 (defun git-rename-as-string (info)
   "Return a string describing the copy or rename associated with INFO, or an empty string if none."
   (let ((state (git-fileinfo->rename-state info)))
@@ -526,30 +590,51 @@ and returns the process output as a string."
 
 (defun git-fileinfo-prettyprint (info)
   "Pretty-printer for the git-fileinfo structure."
-  (insert (concat "   " (if (git-fileinfo->marked info) (propertize "*" 'face 'git-mark-face) " ")
-                  " " (git-status-code-as-string (git-fileinfo->state info))
-                  " " (git-permissions-as-string (git-fileinfo->old-perm info) (git-fileinfo->new-perm info))
-                  "  " (git-escape-file-name (git-fileinfo->name info))
-                  (git-rename-as-string info))))
-
-(defun git-insert-fileinfo (status info &optional refresh)
-  "Insert INFO in the status buffer, optionally refreshing an existing one."
-  (let ((node (and refresh
-                   (git-find-status-file status (git-fileinfo->name info)))))
-    (setf (git-fileinfo->needs-refresh info) t)
-    (when node   ;preserve the marked flag
-      (setf (git-fileinfo->marked info) (git-fileinfo->marked (ewoc-data node))))
-    (if node (setf (ewoc-data node) info) (ewoc-enter-last status info))))
+  (let ((old-perm (git-fileinfo->old-perm info))
+       (new-perm (git-fileinfo->new-perm info)))
+    (insert (concat "   " (if (git-fileinfo->marked info) (propertize "*" 'face 'git-mark-face) " ")
+                   " " (git-status-code-as-string (git-fileinfo->state info))
+                   " " (git-permissions-as-string old-perm new-perm)
+                   "  " (git-escape-file-name (git-fileinfo->name info))
+                   (git-file-type-as-string old-perm new-perm)
+                   (git-rename-as-string info)))))
+
+(defun git-insert-info-list (status infolist)
+  "Insert a list of file infos in the status buffer, replacing existing ones if any."
+  (setq infolist (sort infolist
+                       (lambda (info1 info2)
+                         (string-lessp (git-fileinfo->name info1)
+                                       (git-fileinfo->name info2)))))
+  (let ((info (pop infolist))
+        (node (ewoc-nth status 0)))
+    (while info
+      (cond ((not node)
+            (setq node (ewoc-enter-last status info))
+             (setq info (pop infolist)))
+            ((string-lessp (git-fileinfo->name (ewoc-data node))
+                           (git-fileinfo->name info))
+             (setq node (ewoc-next status node)))
+            ((string-equal (git-fileinfo->name (ewoc-data node))
+                           (git-fileinfo->name info))
+              ;; preserve the marked flag
+              (setf (git-fileinfo->marked info) (git-fileinfo->marked (ewoc-data node)))
+             (setf (git-fileinfo->needs-refresh info) t)
+              (setf (ewoc-data node) info)
+              (setq info (pop infolist)))
+            (t
+            (setq node (ewoc-enter-before status node info))
+             (setq info (pop infolist)))))))
 
 (defun git-run-diff-index (status files)
   "Run git-diff-index on FILES and parse the results into STATUS.
 Return the list of files that haven't been handled."
-  (let ((refresh files))
+  (let ((remaining (copy-sequence files))
+       infolist)
     (with-temp-buffer
-      (apply #'git-run-command t nil "diff-index" "-z" "-M" "HEAD" "--" files)
+      (apply #'git-call-process-env t nil "diff-index" "-z" "-M" "HEAD" "--" files)
       (goto-char (point-min))
       (while (re-search-forward
-              ":\\([0-7]\\{6\\}\\) \\([0-7]\\{6\\}\\) [0-9a-f]\\{40\\} [0-9a-f]\\{40\\} \\(\\([ADMU]\\)\0\\([^\0]+\\)\\|\\([CR]\\)[0-9]*\0\\([^\0]+\\)\0\\([^\0]+\\)\\)\0"
+             ":\\([0-7]\\{6\\}\\) \\([0-7]\\{6\\}\\) [0-9a-f]\\{40\\} [0-9a-f]\\{40\\} \\(\\([ADMUT]\\)\0\\([^\0]+\\)\\|\\([CR]\\)[0-9]*\0\\([^\0]+\\)\0\\([^\0]+\\)\\)\0"
               nil t 1)
         (let ((old-perm (string-to-number (match-string 1) 8))
               (new-perm (string-to-number (match-string 2) 8))
@@ -558,13 +643,14 @@ Return the list of files that haven't been handled."
               (new-name (match-string 8)))
           (if new-name  ; copy or rename
               (if (eq ?C (string-to-char state))
-                  (git-insert-fileinfo status (git-create-fileinfo 'added new-name old-perm new-perm 'copy name) refresh)
-                (git-insert-fileinfo status (git-create-fileinfo 'deleted name 0 0 'rename new-name) refresh)
-                (git-insert-fileinfo status (git-create-fileinfo 'added new-name old-perm new-perm 'rename name)) refresh)
-            (git-insert-fileinfo status (git-create-fileinfo (git-state-code state) name old-perm new-perm) refresh))
-          (setq files (delete name files))
-          (when new-name (setq files (delete new-name files)))))))
-  files)
+                  (push (git-create-fileinfo 'added new-name old-perm new-perm 'copy name) infolist)
+                (push (git-create-fileinfo 'deleted name 0 0 'rename new-name) infolist)
+                (push (git-create-fileinfo 'added new-name old-perm new-perm 'rename name) infolist))
+            (push (git-create-fileinfo (git-state-code state) name old-perm new-perm) infolist))
+         (setq remaining (delete name remaining))
+         (when new-name (setq remaining (delete new-name remaining))))))
+    (git-insert-info-list status infolist)
+    remaining))
 
 (defun git-find-status-file (status file)
   "Find a given file in the status ewoc and return its node."
@@ -576,27 +662,43 @@ Return the list of files that haven't been handled."
 (defun git-run-ls-files (status files default-state &rest options)
   "Run git-ls-files on FILES and parse the results into STATUS.
 Return the list of files that haven't been handled."
-  (let ((refresh files))
+  (let (infolist)
     (with-temp-buffer
-      (apply #'git-run-command t nil "ls-files" "-z" "-t" (append options (list "--") files))
+      (apply #'git-call-process-env t nil "ls-files" "-z" (append options (list "--") files))
       (goto-char (point-min))
-      (while (re-search-forward "\\([HMRCK?]\\) \\([^\0]*\\)\0" nil t 1)
-        (let ((state (match-string 1))
-              (name (match-string 2)))
-          (git-insert-fileinfo status (git-create-fileinfo (or (git-state-code state) default-state) name) refresh)
-          (setq files (delete name files))))))
-  files)
+      (while (re-search-forward "\\([^\0]*\\)\0" nil t 1)
+        (let ((name (match-string 1)))
+          (push (git-create-fileinfo default-state name) infolist)
+          (setq files (delete name files)))))
+    (git-insert-info-list status infolist)
+    files))
+
+(defun git-run-ls-files-cached (status files default-state)
+  "Run git-ls-files -c on FILES and parse the results into STATUS.
+Return the list of files that haven't been handled."
+  (let ((remaining (copy-sequence files))
+       infolist)
+    (with-temp-buffer
+      (apply #'git-call-process-env t nil "ls-files" "-z" "-s" "-c" "--" files)
+      (goto-char (point-min))
+      (while (re-search-forward "\\([0-7]\\{6\\}\\) [0-9a-f]\\{40\\} 0\t\\([^\0]+\\)\0" nil t)
+       (let* ((new-perm (string-to-number (match-string 1) 8))
+              (old-perm (if (eq default-state 'added) 0 new-perm))
+              (name (match-string 2)))
+         (push (git-create-fileinfo default-state name old-perm new-perm) infolist)
+         (setq remaining (delete name remaining)))))
+    (git-insert-info-list status infolist)
+    remaining))
 
 (defun git-run-ls-unmerged (status files)
   "Run git-ls-files -u on FILES and parse the results into STATUS."
   (with-temp-buffer
-    (apply #'git-run-command t nil "ls-files" "-z" "-u" "--" files)
+    (apply #'git-call-process-env t nil "ls-files" "-z" "-u" "--" files)
     (goto-char (point-min))
     (let (unmerged-files)
       (while (re-search-forward "[0-7]\\{6\\} [0-9a-f]\\{40\\} [123]\t\\([^\0]+\\)\0" nil t)
-        (let ((node (git-find-status-file status (match-string 1))))
-          (when node (push (ewoc-data node) unmerged-files))))
-      (git-set-files-state unmerged-files 'unmerged))))
+        (push (match-string 1) unmerged-files))
+      (git-set-filenames-state status unmerged-files 'unmerged))))
 
 (defun git-get-exclude-files ()
   "Get the list of exclude files to pass to git-ls-files."
@@ -608,34 +710,30 @@ Return the list of files that haven't been handled."
       (push config files))
     files))
 
+(defun git-run-ls-files-with-excludes (status files default-state &rest options)
+  "Run git-ls-files on FILES with appropriate --exclude-from options."
+  (let ((exclude-files (git-get-exclude-files)))
+    (apply #'git-run-ls-files status files default-state
+           (concat "--exclude-per-directory=" git-per-dir-ignore-file)
+           (append options (mapcar (lambda (f) (concat "--exclude-from=" f)) exclude-files)))))
+
 (defun git-update-status-files (files &optional default-state)
   "Update the status of FILES from the index."
   (unless git-status (error "Not in git-status buffer."))
-  (let* ((status git-status)
-         (remaining-files
+  (when (or git-show-uptodate files)
+    (git-run-ls-files-cached git-status files 'uptodate))
+  (let* ((remaining-files
           (if (git-empty-db-p) ; we need some special handling for an empty db
-              (git-run-ls-files status files 'added "-c")
-            (git-run-diff-index status files))))
-    (git-run-ls-unmerged status files)
-    (when (or (not files) remaining-files)
-      (let ((exclude-files (git-get-exclude-files)))
-        (setq remaining-files (apply #'git-run-ls-files status remaining-files 'unknown "-o"
-                                     (concat "--exclude-per-directory=" git-per-dir-ignore-file)
-                                     (mapcar (lambda (f) (concat "--exclude-from=" f)) exclude-files)))))
-    ; mark remaining files with the default state (or remove them if nil)
-    (when remaining-files
-      (if default-state
-          (ewoc-map (lambda (info)
-                      (when (member (git-fileinfo->name info) remaining-files)
-                        (git-set-files-state (list info) default-state))
-                      nil)
-                    status)
-        (ewoc-filter status
-                     (lambda (info files)
-                       (not (member (git-fileinfo->name info) files)))
-                     remaining-files)))
+             (git-run-ls-files-cached git-status files 'added)
+            (git-run-diff-index git-status files))))
+    (git-run-ls-unmerged git-status files)
+    (when (or remaining-files (and git-show-unknown (not files)))
+      (setq remaining-files (git-run-ls-files-with-excludes git-status remaining-files 'unknown "-o")))
+    (when (or remaining-files (and git-show-ignored (not files)))
+      (setq remaining-files (git-run-ls-files-with-excludes git-status remaining-files 'ignored "-o" "-i")))
+    (git-set-filenames-state git-status remaining-files default-state)
     (git-refresh-files)
-    (git-refresh-ewoc-hf status)))
+    (git-refresh-ewoc-hf git-status)))
 
 (defun git-marked-files ()
   "Return a list of all marked files, or if none a list containing just the file at cursor position."
@@ -698,11 +796,11 @@ Return the list of files that haven't been handled."
         ('deleted (push info deleted))
         ('modified (push info modified))))
     (when added
-      (apply #'git-run-command nil env "update-index" "--add" "--" (git-get-filenames added)))
+      (apply #'git-call-process-env nil env "update-index" "--add" "--" (git-get-filenames added)))
     (when deleted
-      (apply #'git-run-command nil env "update-index" "--remove" "--" (git-get-filenames deleted)))
+      (apply #'git-call-process-env nil env "update-index" "--remove" "--" (git-get-filenames deleted)))
     (when modified
-      (apply #'git-run-command nil env "update-index" "--" (git-get-filenames modified)))))
+      (apply #'git-call-process-env nil env "update-index" "--" (git-get-filenames modified)))))
 
 (defun git-run-pre-commit-hook ()
   "Run the pre-commit hook if any."
@@ -734,6 +832,7 @@ Return the list of files that haven't been handled."
                       head-tree (git-rev-parse "HEAD^{tree}")))
               (if files
                   (progn
+                    (message "Running git commit...")
                     (git-read-tree head-tree index-file)
                     (git-update-index nil files)         ;update both the default index
                     (git-update-index index-file files)  ;and the temporary one
@@ -744,8 +843,9 @@ Return the list of files that haven't been handled."
                             (condition-case nil (delete-file ".git/MERGE_HEAD") (error nil))
                             (condition-case nil (delete-file ".git/MERGE_MSG") (error nil))
                             (with-current-buffer buffer (erase-buffer))
-                            (git-set-files-state files 'uptodate)
-                            (git-run-command nil nil "rerere")
+                           (git-update-status-files (git-get-filenames files) 'uptodate)
+                            (git-call-process-env nil nil "rerere")
+                            (git-call-process-env nil nil "gc" "--auto")
                             (git-refresh-files)
                             (git-refresh-ewoc-hf git-status)
                             (message "Committed %s." commit)
@@ -792,7 +892,8 @@ Return the list of files that haven't been handled."
   "Mark all files."
   (interactive)
   (unless git-status (error "Not in git-status buffer."))
-  (ewoc-map (lambda (info) (setf (git-fileinfo->marked info) t) t) git-status)
+  (ewoc-map (lambda (info) (unless (git-fileinfo->marked info)
+                             (setf (git-fileinfo->marked info) t))) git-status)
   ; move back to goal column after invalidate
   (when goal-column (move-to-column goal-column)))
 
@@ -800,7 +901,9 @@ Return the list of files that haven't been handled."
   "Unmark all files."
   (interactive)
   (unless git-status (error "Not in git-status buffer."))
-  (ewoc-map (lambda (info) (setf (git-fileinfo->marked info) nil) t) git-status)
+  (ewoc-map (lambda (info) (when (git-fileinfo->marked info)
+                             (setf (git-fileinfo->marked info) nil)
+                             t)) git-status)
   ; move back to goal column after invalidate
   (when goal-column (move-to-column goal-column)))
 
@@ -853,11 +956,12 @@ Return the list of files that haven't been handled."
 (defun git-add-file ()
   "Add marked file(s) to the index cache."
   (interactive)
-  (let ((files (git-get-filenames (git-marked-files-state 'unknown))))
+  (let ((files (git-get-filenames (git-marked-files-state 'unknown 'ignored))))
     (unless files
       (push (file-relative-name (read-file-name "File to add: " nil nil t)) files))
-    (apply #'git-run-command nil nil "update-index" "--add" "--" files)
-    (git-update-status-files files 'uptodate)))
+    (apply #'git-call-process-env nil nil "update-index" "--add" "--" files)
+    (git-update-status-files files 'uptodate)
+    (git-success-message "Added" files)))
 
 (defun git-ignore-file ()
   "Add marked file(s) to the ignore list."
@@ -866,12 +970,13 @@ Return the list of files that haven't been handled."
     (unless files
       (push (file-relative-name (read-file-name "File to ignore: " nil nil t)) files))
     (dolist (f files) (git-append-to-ignore f))
-    (git-update-status-files files 'ignored)))
+    (git-update-status-files files 'ignored)
+    (git-success-message "Ignored" files)))
 
 (defun git-remove-file ()
   "Remove the marked file(s)."
   (interactive)
-  (let ((files (git-get-filenames (git-marked-files-state 'added 'modified 'unknown 'uptodate))))
+  (let ((files (git-get-filenames (git-marked-files-state 'added 'modified 'unknown 'uptodate 'ignored))))
     (unless files
       (push (file-relative-name (read-file-name "File to remove: " nil nil t)) files))
     (if (yes-or-no-p
@@ -879,8 +984,9 @@ Return the list of files that haven't been handled."
         (progn
           (dolist (name files)
             (when (file-exists-p name) (delete-file name)))
-          (apply #'git-run-command nil nil "update-index" "--remove" "--" files)
-          (git-update-status-files files nil))
+          (apply #'git-call-process-env nil nil "update-index" "--remove" "--" files)
+          (git-update-status-files files nil)
+          (git-success-message "Removed" files))
       (message "Aborting"))))
 
 (defun git-revert-file ()
@@ -898,29 +1004,65 @@ Return the list of files that haven't been handled."
           ('unmerged (push (git-fileinfo->name info) modified))
           ('modified (push (git-fileinfo->name info) modified))))
       (when added
-        (apply #'git-run-command nil nil "update-index" "--force-remove" "--" added))
+        (apply #'git-call-process-env nil nil "update-index" "--force-remove" "--" added))
       (when modified
-        (apply #'git-run-command nil nil "checkout" "HEAD" modified))
-      (git-update-status-files (append added modified) 'uptodate))))
+        (apply #'git-call-process-env nil nil "checkout" "HEAD" modified))
+      (git-update-status-files (append added modified) 'uptodate)
+      (git-success-message "Reverted" (git-get-filenames files)))))
 
 (defun git-resolve-file ()
   "Resolve conflicts in marked file(s)."
   (interactive)
   (let ((files (git-get-filenames (git-marked-files-state 'unmerged))))
     (when files
-      (apply #'git-run-command nil nil "update-index" "--" files)
-      (git-update-status-files files 'uptodate))))
+      (apply #'git-call-process-env nil nil "update-index" "--" files)
+      (git-update-status-files files 'uptodate)
+      (git-success-message "Resolved" files))))
 
 (defun git-remove-handled ()
   "Remove handled files from the status list."
   (interactive)
   (ewoc-filter git-status
                (lambda (info)
-                 (not (or (eq (git-fileinfo->state info) 'ignored)
-                          (eq (git-fileinfo->state info) 'uptodate)))))
+                 (case (git-fileinfo->state info)
+                   ('ignored git-show-ignored)
+                   ('uptodate git-show-uptodate)
+                   ('unknown git-show-unknown)
+                   (t t))))
   (unless (ewoc-nth git-status 0)  ; refresh header if list is empty
     (git-refresh-ewoc-hf git-status)))
 
+(defun git-toggle-show-uptodate ()
+  "Toogle the option for showing up-to-date files."
+  (interactive)
+  (if (setq git-show-uptodate (not git-show-uptodate))
+      (git-refresh-status)
+    (git-remove-handled)))
+
+(defun git-toggle-show-ignored ()
+  "Toogle the option for showing ignored files."
+  (interactive)
+  (if (setq git-show-ignored (not git-show-ignored))
+      (progn
+        (message "Inserting ignored files...")
+        (git-run-ls-files-with-excludes git-status nil 'ignored "-o" "-i")
+        (git-refresh-files)
+        (git-refresh-ewoc-hf git-status)
+        (message "Inserting ignored files...done"))
+    (git-remove-handled)))
+
+(defun git-toggle-show-unknown ()
+  "Toogle the option for showing unknown files."
+  (interactive)
+  (if (setq git-show-unknown (not git-show-unknown))
+      (progn
+        (message "Inserting unknown files...")
+        (git-run-ls-files-with-excludes git-status nil 'unknown "-o")
+        (git-refresh-files)
+        (git-refresh-ewoc-hf git-status)
+        (message "Inserting unknown files...done"))
+    (git-remove-handled)))
+
 (defun git-setup-diff-buffer (buffer)
   "Setup a buffer for displaying a diff."
   (let ((dir default-directory))
@@ -930,7 +1072,9 @@ Return the list of files that haven't been handled."
       (setq default-directory dir)
       (setq buffer-read-only t)))
   (display-buffer buffer)
-  (shrink-window-if-larger-than-buffer))
+  ; shrink window only if it displays the status buffer
+  (when (eq (window-buffer) (current-buffer))
+    (shrink-window-if-larger-than-buffer)))
 
 (defun git-diff-file ()
   "Diff the marked file(s) against HEAD."
@@ -1003,6 +1147,11 @@ Return the list of files that haven't been handled."
   (with-current-buffer log-edit-parent-buffer
     (git-get-filenames (git-marked-files-state 'added 'deleted 'modified))))
 
+(defun git-log-edit-diff ()
+  "Run a diff of the current files being committed from a log-edit buffer."
+  (with-current-buffer log-edit-parent-buffer
+    (git-diff-file)))
+
 (defun git-append-sign-off (name email)
   "Append a Signed-off-by entry to the current buffer, avoiding duplicates."
   (let ((sign-off (format "Signed-off-by: %s <%s>" name email))
@@ -1075,7 +1224,10 @@ Return the list of files that haven't been handled."
             (when (re-search-forward "^Date: \\(.*\\)$" nil t)
               (setq date (match-string 1)))))
         (git-setup-log-buffer buffer author-name author-email subject date))
-      (log-edit #'git-do-commit nil #'git-log-edit-files buffer)
+      (if (boundp 'log-edit-diff-function)
+         (log-edit 'git-do-commit nil '((log-edit-listfun . git-log-edit-files)
+                                        (log-edit-diff-function . git-log-edit-diff)) buffer)
+       (log-edit 'git-do-commit nil 'git-log-edit-files buffer))
       (setq font-lock-keywords (font-lock-compile-keywords git-log-edit-font-lock-keywords))
       (setq buffer-file-coding-system coding-system)
       (re-search-forward (regexp-quote (concat git-log-msg-separator "\n")) nil t))))
@@ -1118,12 +1270,23 @@ Return the list of files that haven't been handled."
   (interactive)
   (let* ((status git-status)
          (pos (ewoc-locate status))
+         (marked-files (git-get-filenames (ewoc-collect status (lambda (info) (git-fileinfo->marked info)))))
          (cur-name (and pos (git-fileinfo->name (ewoc-data pos)))))
     (unless status (error "Not in git-status buffer."))
-    (git-run-command nil nil "update-index" "--refresh")
+    (message "Refreshing git status...")
+    (git-call-process-env nil nil "update-index" "--refresh")
     (git-clear-status status)
     (git-update-status-files nil)
+    ; restore file marks
+    (when marked-files
+      (git-status-filenames-map status
+                                (lambda (info)
+                                        (setf (git-fileinfo->marked info) t)
+                                        (setf (git-fileinfo->needs-refresh info) t))
+                                marked-files)
+      (git-refresh-files))
     ; move point to the current file name if any
+    (message "Refreshing git status...done")
     (let ((node (and cur-name (git-find-status-file status cur-name))))
       (when node (ewoc-goto-node status node)))))
 
@@ -1146,7 +1309,8 @@ Return the list of files that haven't been handled."
 
 (unless git-status-mode-map
   (let ((map (make-keymap))
-        (diff-map (make-sparse-keymap)))
+        (diff-map (make-sparse-keymap))
+        (toggle-map (make-sparse-keymap)))
     (suppress-keymap map)
     (define-key map "?"   'git-help)
     (define-key map "h"   'git-help)
@@ -1170,6 +1334,7 @@ Return the list of files that haven't been handled."
     (define-key map "q"   'git-status-quit)
     (define-key map "r"   'git-remove-file)
     (define-key map "R"   'git-resolve-file)
+    (define-key map "t"    toggle-map)
     (define-key map "T"   'git-toggle-all-marks)
     (define-key map "u"   'git-unmark-file)
     (define-key map "U"   'git-revert-file)
@@ -1186,7 +1351,52 @@ Return the list of files that haven't been handled."
     (define-key diff-map "h" 'git-diff-file-merge-head)
     (define-key diff-map "m" 'git-diff-file-mine)
     (define-key diff-map "o" 'git-diff-file-other)
-    (setq git-status-mode-map map)))
+    ; the toggle submap
+    (define-key toggle-map "u" 'git-toggle-show-uptodate)
+    (define-key toggle-map "i" 'git-toggle-show-ignored)
+    (define-key toggle-map "k" 'git-toggle-show-unknown)
+    (define-key toggle-map "m" 'git-toggle-all-marks)
+    (setq git-status-mode-map map))
+  (easy-menu-define git-menu git-status-mode-map
+    "Git Menu"
+    `("Git"
+      ["Refresh" git-refresh-status t]
+      ["Commit" git-commit-file t]
+      ("Merge"
+       ["Next Unmerged File" git-next-unmerged-file t]
+       ["Prev Unmerged File" git-prev-unmerged-file t]
+       ["Mark as Resolved" git-resolve-file t]
+       ["Interactive Merge File" git-find-file-imerge t]
+       ["Diff Against Common Base File" git-diff-file-base t]
+       ["Diff Combined" git-diff-file-combined t]
+       ["Diff Against Merge Head" git-diff-file-merge-head t]
+       ["Diff Against Mine" git-diff-file-mine t]
+       ["Diff Against Other" git-diff-file-other t])
+      "--------"
+      ["Add File" git-add-file t]
+      ["Revert File" git-revert-file t]
+      ["Ignore File" git-ignore-file t]
+      ["Remove File" git-remove-file t]
+      "--------"
+      ["Find File" git-find-file t]
+      ["View File" git-view-file t]
+      ["Diff File" git-diff-file t]
+      ["Interactive Diff File" git-diff-file-idiff t]
+      ["Log" git-log-file t]
+      "--------"
+      ["Mark" git-mark-file t]
+      ["Mark All" git-mark-all t]
+      ["Unmark" git-unmark-file t]
+      ["Unmark All" git-unmark-all t]
+      ["Toggle All Marks" git-toggle-all-marks t]
+      ["Hide Handled Files" git-remove-handled t]
+      "--------"
+      ["Show Uptodate Files" git-toggle-show-uptodate :style toggle :selected git-show-uptodate]
+      ["Show Ignored Files" git-toggle-show-ignored :style toggle :selected git-show-ignored]
+      ["Show Unknown Files" git-toggle-show-unknown :style toggle :selected git-show-unknown]
+      "--------"
+      ["Quit" git-status-quit t])))
+
 
 ;; git mode should only run in the *git status* buffer
 (put 'git-status-mode 'mode-class 'special)
@@ -1207,6 +1417,9 @@ Commands:
   (let ((status (ewoc-create 'git-fileinfo-prettyprint "" "")))
     (set (make-local-variable 'git-status) status))
   (set (make-local-variable 'list-buffers-directory) default-directory)
+  (make-local-variable 'git-show-uptodate)
+  (make-local-variable 'git-show-ignored)
+  (make-local-variable 'git-show-unknown)
   (run-hooks 'git-status-mode-hook)))
 
 (defun git-find-status-buffer (dir)
@@ -1219,7 +1432,7 @@ Commands:
         (with-current-buffer buffer
           (when (and list-buffers-directory
                      (string-equal fulldir (expand-file-name list-buffers-directory))
-                     (string-match "\\*git-status\\*$" (buffer-name buffer)))
+                    (eq major-mode 'git-status-mode))
             (setq found buffer))))
       (setq list (cdr list)))
     found))
@@ -1235,9 +1448,24 @@ Commands:
         (cd dir)
         (git-status-mode)
         (git-refresh-status)
-        (goto-char (point-min)))
+        (goto-char (point-min))
+        (add-hook 'after-save-hook 'git-update-saved-file))
     (message "%s is not a git working tree." dir)))
 
+(defun git-update-saved-file ()
+  "Update the corresponding git-status buffer when a file is saved.
+Meant to be used in `after-save-hook'."
+  (let* ((file (expand-file-name buffer-file-name))
+         (dir (condition-case nil (git-get-top-dir (file-name-directory file)) (error nil)))
+         (buffer (and dir (git-find-status-buffer dir))))
+    (when buffer
+      (with-current-buffer buffer
+        (let ((filename (file-relative-name file dir)))
+          ; skip files located inside the .git directory
+          (unless (string-match "^\\.git/" filename)
+            (git-call-process-env nil nil "add" "--refresh" "--" filename)
+            (git-update-status-files (list filename) 'uptodate)))))))
+
 (defun git-help ()
   "Display help for Git mode."
   (interactive)