Build in checkout
[gitweb.git] / contrib / emacs / git.el
index 7b4a0d3602ee542ceb35d0fa3010813064b16c10..d8a06381f4140a777f03fda67f68838e4aa3e493 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
@@ -220,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."
@@ -312,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
@@ -338,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
@@ -485,14 +485,14 @@ 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."
@@ -511,14 +511,7 @@ and returns the process output as a string."
 (defun git-set-filenames-state (status files state)
   "Set the state of a list of named files."
   (when files
-    (git-status-filenames-map status
-                              (lambda (info state)
-                                (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)))
-                              files state)
+    (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))))))
 
@@ -530,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)
@@ -544,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)))
@@ -569,11 +590,14 @@ 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))))
+  (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."
@@ -584,9 +608,8 @@ and returns the process output as a string."
   (let ((info (pop infolist))
         (node (ewoc-nth status 0)))
     (while info
-      (setf (git-fileinfo->needs-refresh info) t)
       (cond ((not node)
-             (ewoc-enter-last status info)
+            (setq node (ewoc-enter-last status info))
              (setq info (pop infolist)))
             ((string-lessp (git-fileinfo->name (ewoc-data node))
                            (git-fileinfo->name info))
@@ -595,21 +618,23 @@ and returns the process output as a string."
                            (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
-             (ewoc-enter-before status node info)
+            (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 (infolist)
+  (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))
@@ -622,10 +647,10 @@ Return the list of files that haven't been handled."
                 (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 files (delete name files))
-          (when new-name (setq files (delete new-name files))))))
+         (setq remaining (delete name remaining))
+         (when new-name (setq remaining (delete new-name remaining))))))
     (git-insert-info-list status infolist)
-    files))
+    remaining))
 
 (defun git-find-status-file (status file)
   "Find a given file in the status ewoc and return its node."
@@ -639,7 +664,7 @@ Return the list of files that haven't been handled."
 Return the list of files that haven't been handled."
   (let (infolist)
     (with-temp-buffer
-      (apply #'git-run-command t nil "ls-files" "-z" (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 "\\([^\0]*\\)\0" nil t 1)
         (let ((name (match-string 1)))
@@ -648,10 +673,27 @@ Return the list of files that haven't been handled."
     (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)
@@ -678,11 +720,11 @@ Return the list of files that haven't been handled."
 (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."))
-  (unless files
-    (when git-show-uptodate (git-run-ls-files git-status nil 'uptodate "-c")))
+  (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 git-status files 'added "-c")
+             (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)))
@@ -754,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."
@@ -790,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
@@ -800,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)
@@ -848,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)))
 
@@ -856,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)))
 
@@ -912,8 +959,9 @@ Return the list of files that haven't been handled."
   (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."
@@ -922,7 +970,8 @@ 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)."
@@ -935,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 ()
@@ -954,18 +1004,20 @@ 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."
@@ -992,9 +1044,11 @@ Return the list of files that haven't been handled."
   (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))
+        (git-refresh-ewoc-hf git-status)
+        (message "Inserting ignored files...done"))
     (git-remove-handled)))
 
 (defun git-toggle-show-unknown ()
@@ -1002,9 +1056,11 @@ Return the list of files that haven't been handled."
   (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))
+        (git-refresh-ewoc-hf git-status)
+        (message "Inserting unknown files...done"))
     (git-remove-handled)))
 
 (defun git-setup-diff-buffer (buffer)
@@ -1016,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."
@@ -1089,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))
@@ -1161,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))))
@@ -1207,7 +1273,8 @@ Return the list of files that haven't been handled."
          (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
@@ -1219,6 +1286,7 @@ Return the list of files that haven't been handled."
                                 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)))))
 
@@ -1288,7 +1356,47 @@ Return the list of files that haven't been handled."
     (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)))
+    (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)
@@ -1340,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)