Code

Merge branch 'maint-1.6.0' into maint
[git.git] / contrib / emacs / git.el
index 4fa853fae76dc2ac132e489f5f1b630b3fc0f1de..eace9c18eb1d17075836694ce664a009f3e02038 100644 (file)
@@ -1,6 +1,6 @@
 ;;; git.el --- A user interface for git
 
-;; Copyright (C) 2005, 2006, 2007 Alexandre Julliard <julliard@winehq.org>
+;; Copyright (C) 2005, 2006, 2007, 2008, 2009 Alexandre Julliard <julliard@winehq.org>
 
 ;; Version: 1.0
 
 ;; To start: `M-x git-status'
 ;;
 ;; TODO
-;;  - portability to XEmacs
 ;;  - diff against other branch
 ;;  - renaming files from the status buffer
 ;;  - creating tags
 ;;  - fetch/pull
-;;  - switching branches
 ;;  - revlist browser
 ;;  - git-show-branch browser
-;;  - menus
+;;
+
+;;; Compatibility:
+;;
+;; This file works on GNU Emacs 21 or later. It may work on older
+;; versions but this is not guaranteed.
+;;
+;; It may work on XEmacs 21, provided that you first install the ewoc
+;; and log-edit packages.
 ;;
 
 (eval-when-compile (require 'cl))
@@ -173,7 +179,7 @@ if there is already one that displays the same directory."
 (defconst git-log-msg-separator "--- log message follows this line ---")
 
 (defvar git-log-edit-font-lock-keywords
-  `(("^\\(Author:\\|Date:\\|Parent:\\|Signed-off-by:\\)\\(.*\\)$"
+  `(("^\\(Author:\\|Date:\\|Merge:\\|Signed-off-by:\\)\\(.*\\)$"
      (1 font-lock-keyword-face)
      (2 font-lock-function-name-face))
     (,(concat "^\\(" (regexp-quote git-log-msg-separator) "\\)$")
@@ -183,11 +189,9 @@ if there is already one that displays the same directory."
   "Build a list of NAME=VALUE strings from a list of environment strings."
   (mapcar (lambda (entry) (concat (car entry) "=" (cdr entry))) env))
 
-(defun git-call-process-env (buffer env &rest args)
+(defun git-call-process (buffer &rest args)
   "Wrapper for call-process that sets environment strings."
-  (let ((process-environment (append (git-get-env-strings env)
-                                     process-environment)))
-    (apply #'call-process "git" nil buffer nil args)))
+  (apply #'call-process "git" nil buffer nil args))
 
 (defun git-call-process-display-error (&rest args)
   "Wrapper for call-process that displays error messages."
@@ -197,17 +201,26 @@ if there is already one that displays the same directory."
                (let ((default-directory dir)
                      (buffer-read-only nil))
                  (erase-buffer)
-                 (eq 0 (apply 'call-process "git" nil (list buffer t) nil args))))))
+                 (eq 0 (apply #'git-call-process (list buffer t) args))))))
     (unless ok (display-message-or-buffer buffer))
     ok))
 
-(defun git-call-process-env-string (env &rest args)
-  "Wrapper for call-process that sets environment strings,
-and returns the process output as a string, or nil if the git failed."
+(defun git-call-process-string (&rest args)
+  "Wrapper for call-process that returns the process output as a string,
+or nil if the git command failed."
   (with-temp-buffer
-    (and (eq 0 (apply #' git-call-process-env t env args))
+    (and (eq 0 (apply #'git-call-process t args))
          (buffer-string))))
 
+(defun git-call-process-string-display-error (&rest args)
+  "Wrapper for call-process that displays error message and returns
+the process output as a string, or nil if the git command failed."
+  (with-temp-buffer
+    (if (eq 0 (apply #'git-call-process (list t t) args))
+        (buffer-string)
+      (display-message-or-buffer (current-buffer))
+      nil)))
+
 (defun git-run-process-region (buffer start end program args)
   "Run a git process with a buffer region as input."
   (let ((output-buffer (current-buffer))
@@ -215,7 +228,7 @@ and returns the process output as a string, or nil if the git failed."
     (with-current-buffer buffer
       (cd dir)
       (apply #'call-process-region start end program
-             nil (list output-buffer nil) nil args))))
+             nil (list output-buffer t) nil args))))
 
 (defun git-run-command-buffer (buffer-name &rest args)
   "Run a git command, sending the output to a buffer named BUFFER-NAME."
@@ -226,19 +239,21 @@ and returns the process output as a string, or nil if the git failed."
       (let ((default-directory dir)
             (buffer-read-only nil))
         (erase-buffer)
-        (apply #'git-call-process-env buffer nil args)))
+        (apply #'git-call-process buffer args)))
     (message "Running git %s...done" (car args))
     buffer))
 
 (defun git-run-command-region (buffer start end env &rest args)
   "Run a git command with specified buffer region as input."
-  (unless (eq 0 (if env
-                    (git-run-process-region
-                     buffer start end "env"
-                     (append (git-get-env-strings env) (list "git") args))
+  (with-temp-buffer
+    (if (eq 0 (if env
                   (git-run-process-region
-                   buffer start end "git" args)))
-    (error "Failed to run \"git %s\":\n%s" (mapconcat (lambda (x) x) args " ") (buffer-string))))
+                   buffer start end "env"
+                   (append (git-get-env-strings env) (list "git") args))
+                (git-run-process-region buffer start end "git" args)))
+        (buffer-string)
+      (display-message-or-buffer (current-buffer))
+      nil)))
 
 (defun git-run-hook (hook env &rest args)
   "Run a git hook and display its output if any."
@@ -327,7 +342,7 @@ and returns the process output as a string, or nil if the git failed."
   (let ((cdup (with-output-to-string
                 (with-current-buffer standard-output
                   (cd dir)
-                  (unless (eq 0 (call-process "git" nil t nil "rev-parse" "--show-cdup"))
+                  (unless (eq 0 (git-call-process t "rev-parse" "--show-cdup"))
                     (error "cannot find top-level git tree for %s." dir))))))
     (expand-file-name (concat (file-name-as-directory dir)
                               (car (split-string cdup "\n"))))))
@@ -348,8 +363,8 @@ and returns the process output as a string, or nil if the git failed."
     (sort-lines nil (point-min) (point-max))
     (save-buffer))
   (when created
-    (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)))
+    (git-call-process nil "update-index" "--add" "--" (file-relative-name ignore-name)))
+  (git-update-status-files (list (file-relative-name ignore-name)))))
 
 ; propertize definition for XEmacs, stolen from erc-compat
 (eval-when-compile
@@ -367,38 +382,52 @@ and returns the process output as a string, or nil if the git failed."
 (defun git-rev-parse (rev)
   "Parse a revision name and return its SHA1."
   (git-get-string-sha1
-   (git-call-process-env-string nil "rev-parse" rev)))
+   (git-call-process-string "rev-parse" rev)))
 
 (defun git-config (key)
   "Retrieve the value associated to KEY in the git repository config file."
-  (let ((str (git-call-process-env-string nil "config" key)))
+  (let ((str (git-call-process-string "config" key)))
     (and str (car (split-string str "\n")))))
 
 (defun git-symbolic-ref (ref)
   "Wrapper for the git-symbolic-ref command."
-  (let ((str (git-call-process-env-string nil "symbolic-ref" ref)))
+  (let ((str (git-call-process-string "symbolic-ref" ref)))
     (and str (car (split-string str "\n")))))
 
 (defun git-update-ref (ref newval &optional oldval reason)
   "Update a reference by calling git-update-ref."
   (let ((args (and oldval (list oldval))))
-    (push newval args)
+    (when newval (push newval args))
     (push ref args)
     (when reason
      (push reason args)
      (push "-m" args))
+    (unless newval (push "-d" args))
     (apply 'git-call-process-display-error "update-ref" args)))
 
+(defun git-for-each-ref (&rest specs)
+  "Return a list of refs using git-for-each-ref.
+Each entry is a cons of (SHORT-NAME . FULL-NAME)."
+  (let (refs)
+    (with-temp-buffer
+      (apply #'git-call-process t "for-each-ref" "--format=%(refname)" specs)
+      (goto-char (point-min))
+      (while (re-search-forward "^[^/\n]+/[^/\n]+/\\(.+\\)$" nil t)
+       (push (cons (match-string 1) (match-string 0)) refs)))
+    (nreverse refs)))
+
 (defun git-read-tree (tree &optional index-file)
   "Read a tree into the index file."
-  (apply #'git-call-process-env nil
-         (if index-file `(("GIT_INDEX_FILE" . ,index-file)) nil)
-         "read-tree" (if tree (list tree))))
+  (let ((process-environment
+         (append (and index-file (list (concat "GIT_INDEX_FILE=" index-file))) process-environment)))
+    (apply 'git-call-process-display-error "read-tree" (if tree (list tree)))))
 
 (defun git-write-tree (&optional index-file)
   "Call git-write-tree and return the resulting tree SHA1 as a string."
-  (git-get-string-sha1
-   (git-call-process-env-string (and index-file `(("GIT_INDEX_FILE" . ,index-file))) "write-tree")))
+  (let ((process-environment
+         (append (and index-file (list (concat "GIT_INDEX_FILE=" index-file))) process-environment)))
+    (git-get-string-sha1
+     (git-call-process-string-display-error "write-tree"))))
 
 (defun git-commit-tree (buffer tree head)
   "Call git-commit-tree with buffer as input and return the resulting commit SHA1."
@@ -424,11 +453,11 @@ and returns the process output as a string, or nil if the git failed."
             (when (re-search-forward "^Date: +\\(.*\\)$" nil t)
               (setq author-date (match-string 1)))
             (goto-char (point-min))
-            (while (re-search-forward "^Parent: +\\([0-9a-f]+\\)" nil t)
-              (unless (string-equal head (match-string 1))
-                (setq subject "commit (merge): ")
+            (when (re-search-forward "^Merge: +\\(.*\\)" nil t)
+              (setq subject "commit (merge): ")
+              (dolist (parent (split-string (match-string 1) " +" t))
                 (push "-p" args)
-                (push (match-string 1) args))))
+                (push parent args))))
         (setq log-start (point-min)))
       (setq log-end (point-max))
       (goto-char log-start)
@@ -437,22 +466,20 @@ and returns the process output as a string, or nil if the git failed."
       (setq coding-system-for-write buffer-file-coding-system))
     (let ((commit
            (git-get-string-sha1
-            (with-output-to-string
-              (with-current-buffer standard-output
-                (let ((env `(("GIT_AUTHOR_NAME" . ,author-name)
-                             ("GIT_AUTHOR_EMAIL" . ,author-email)
-                             ("GIT_COMMITTER_NAME" . ,(git-get-committer-name))
-                             ("GIT_COMMITTER_EMAIL" . ,(git-get-committer-email)))))
-                  (when author-date (push `("GIT_AUTHOR_DATE" . ,author-date) env))
-                  (apply #'git-run-command-region
-                         buffer log-start log-end env
-                         "commit-tree" tree (nreverse args))))))))
-      (and (git-update-ref "HEAD" commit head subject)
-           commit))))
+            (let ((env `(("GIT_AUTHOR_NAME" . ,author-name)
+                         ("GIT_AUTHOR_EMAIL" . ,author-email)
+                         ("GIT_COMMITTER_NAME" . ,(git-get-committer-name))
+                         ("GIT_COMMITTER_EMAIL" . ,(git-get-committer-email)))))
+              (when author-date (push `("GIT_AUTHOR_DATE" . ,author-date) env))
+              (apply #'git-run-command-region
+                     buffer log-start log-end env
+                     "commit-tree" tree (nreverse args))))))
+      (when commit (git-update-ref "HEAD" commit head subject))
+      commit)))
 
 (defun git-empty-db-p ()
   "Check if the git db is empty (no commit done yet)."
-  (not (eq 0 (call-process "git" nil nil nil "rev-parse" "--verify" "HEAD"))))
+  (not (eq 0 (git-call-process nil "rev-parse" "--verify" "HEAD"))))
 
 (defun git-get-merge-heads ()
   "Retrieve the merge heads from the MERGE_HEAD file if present."
@@ -468,7 +495,7 @@ and returns the process output as a string, or nil if the git failed."
 (defun git-get-commit-description (commit)
   "Get a one-line description of COMMIT."
   (let ((coding-system-for-read (git-get-logoutput-coding-system)))
-    (let ((descr (git-call-process-env-string nil "log" "--max-count=1" "--pretty=oneline" commit)))
+    (let ((descr (git-call-process-string "log" "--max-count=1" "--pretty=oneline" commit)))
       (if (and descr (string-match "\\`\\([0-9a-f]\\{40\\}\\) *\\(.*\\)$" descr))
           (concat (substring (match-string 1 descr) 0 10) " - " (match-string 2 descr))
         descr))))
@@ -487,14 +514,11 @@ and returns the process output as a string, or nil if the git failed."
   old-perm new-perm   ;; permission flags
   rename-state        ;; rename or copy state
   orig-name           ;; original name for renames or copies
+  needs-update        ;; whether file needs to be updated
   needs-refresh)      ;; whether file needs to be refreshed
 
 (defvar git-status nil)
 
-(defun git-clear-status (status)
-  "Remove everything from the status list."
-  (ewoc-filter status (lambda (info) nil)))
-
 (defun git-set-fileinfo-state (info state)
   "Set the state of a file info."
   (unless (eq (git-fileinfo->state info) state)
@@ -502,24 +526,26 @@ and returns the process output as a string, or nil if the git failed."
          (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-update 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."
+  "Apply FUNC to the status files names in the FILES list.
+The list must be sorted."
   (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)
+        (let* ((info (ewoc-data node))
+               (name (git-fileinfo->name info)))
+          (if (string-lessp name file)
               (setq node (ewoc-next status node))
-            (if (string-equal (git-fileinfo->name info) file)
+            (if (string-equal name 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."
+  "Set the state of a list of named files. The list must be sorted"
   (when files
     (git-status-filenames-map status #'git-set-fileinfo-state files state)
     (unless state  ;; delete files whose state has been set to nil
@@ -553,29 +579,29 @@ and returns the process output as a string, or nil if the git failed."
   (let* ((old-type (lsh (or old-perm 0) -9))
         (new-type (lsh (or new-perm 0) -9))
         (str (case new-type
-               (?\100  ;; file
+               (64  ;; file
                 (case old-type
-                  (?\100 nil)
-                  (?\120 "   (type change symlink -> file)")
-                  (?\160 "   (type change subproject -> file)")))
-                (?\120  ;; symlink
+                  (64 nil)
+                  (80 "   (type change symlink -> file)")
+                  (112 "   (type change subproject -> file)")))
+                (80  ;; symlink
                  (case old-type
-                   (?\100 "   (type change file -> symlink)")
-                   (?\160 "   (type change subproject -> symlink)")
+                   (64 "   (type change file -> symlink)")
+                   (112 "   (type change subproject -> symlink)")
                    (t "   (symlink)")))
-                 (?\160  ;; subproject
+                 (112  ;; subproject
                   (case old-type
-                    (?\100 "   (type change file -> subproject)")
-                    (?\120 "   (type change symlink -> subproject)")
+                    (64 "   (type change file -> subproject)")
+                    (80 "   (type change symlink -> subproject)")
                     (t "   (subproject)")))
-                  (?\110 nil)  ;; directory (internal, not a real git state)
-                 (?\000  ;; deleted or unknown
+                  (72 nil)  ;; directory (internal, not a real git state)
+                 (0  ;; deleted or unknown
                   (case old-type
-                    (?\120 "   (symlink)")
-                    (?\160 "   (subproject)")))
+                    (80 "   (symlink)")
+                    (112 "   (subproject)")))
                  (t (format "   (unknown type %o)" new-type)))))
     (cond (str (propertize str 'face 'git-status-face))
-          ((eq new-type ?\110) "/")
+          ((eq new-type 72) "/")
           (t ""))))
 
 (defun git-rename-as-string (info)
@@ -612,39 +638,52 @@ and returns the process output as a string, or nil if the git failed."
                    (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)))
+(defun git-update-node-fileinfo (node info)
+  "Update the fileinfo of the specified node. The names are assumed to match already."
+  (let ((data (ewoc-data node)))
+    (setf
+     ;; preserve the marked flag
+     (git-fileinfo->marked info) (git-fileinfo->marked data)
+     (git-fileinfo->needs-update data) nil)
+    (when (not (equal info data))
+      (setf (git-fileinfo->needs-refresh info) t
+            (ewoc-data node) info))))
+
+(defun git-insert-info-list (status infolist files)
+  "Insert a sorted list of file infos in the status buffer, replacing existing ones if any."
+  (let* ((info (pop infolist))
+         (node (ewoc-nth status 0))
+         (name (and info (git-fileinfo->name info)))
+         remaining)
     (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)))))))
+      (let ((nodename (and node (git-fileinfo->name (ewoc-data node)))))
+        (while (and files (string-lessp (car files) name))
+          (push (pop files) remaining))
+        (when (and files (string-equal (car files) name))
+          (setq files (cdr files)))
+        (cond ((not nodename)
+               (setq node (ewoc-enter-last status info))
+               (setq info (pop infolist))
+               (setq name (and info (git-fileinfo->name info))))
+              ((string-lessp nodename name)
+               (setq node (ewoc-next status node)))
+              ((string-equal nodename name)
+               ;; preserve the marked flag
+               (git-update-node-fileinfo node info)
+               (setq info (pop infolist))
+               (setq name (and info (git-fileinfo->name info))))
+              (t
+               (setq node (ewoc-enter-before status node info))
+               (setq info (pop infolist))
+               (setq name (and info (git-fileinfo->name info)))))))
+    (nconc (nreverse remaining) files)))
 
 (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 ((remaining (copy-sequence files))
-       infolist)
+  (let (infolist)
     (with-temp-buffer
-      (apply #'git-call-process-env t nil "diff-index" "-z" "-M" "HEAD" "--" files)
+      (apply #'git-call-process t "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\\} \\(\\([ADMUT]\\)\0\\([^\0]+\\)\\|\\([CR]\\)[0-9]*\0\\([^\0]+\\)\0\\([^\0]+\\)\\)\0"
@@ -659,11 +698,12 @@ Return the list of files that haven't been handled."
                   (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))
+            (push (git-create-fileinfo (git-state-code state) name old-perm new-perm) infolist)))))
+    (setq infolist (sort (nreverse infolist)
+                         (lambda (info1 info2)
+                           (string-lessp (git-fileinfo->name info1)
+                                         (git-fileinfo->name info2)))))
+    (git-insert-info-list status infolist files)))
 
 (defun git-find-status-file (status file)
   "Find a given file in the status ewoc and return its node."
@@ -677,42 +717,40 @@ 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-call-process-env t nil "ls-files" "-z" (append options (list "--") files))
+      (apply #'git-call-process t "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)))
           (push (git-create-fileinfo default-state name 0
                                      (if (string-equal "/" (match-string 2)) (lsh ?\110 9) 0))
-                infolist)
-          (setq files (delete name files)))))
-    (git-insert-info-list status infolist)
-    files))
+                infolist))))
+    (setq infolist (nreverse infolist))  ;; assume it is sorted already
+    (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)
+  (let (infolist)
     (with-temp-buffer
-      (apply #'git-call-process-env t nil "ls-files" "-z" "-s" "-c" "--" files)
+      (apply #'git-call-process t "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))
+         (push (git-create-fileinfo default-state name old-perm new-perm) infolist))))
+    (setq infolist (nreverse infolist))  ;; assume it is sorted already
+    (git-insert-info-list status infolist files)))
 
 (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-call-process-env t nil "ls-files" "-z" "-u" "--" files)
+    (apply #'git-call-process t "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)
         (push (match-string 1) unmerged-files))
+      (setq unmerged-files (nreverse unmerged-files))  ;; assume it is sorted already
       (git-set-filenames-state status unmerged-files 'unmerged))))
 
 (defun git-get-exclude-files ()
@@ -732,12 +770,19 @@ Return the list of files that haven't been handled."
            (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."
+(defun git-update-status-files (&optional files mark-files)
+  "Update the status of FILES from the index.
+The FILES list must be sorted."
   (unless git-status (error "Not in git-status buffer."))
-  (when (or git-show-uptodate files)
-    (git-run-ls-files-cached git-status files 'uptodate))
-  (let* ((remaining-files
+  ;; set the needs-update flag on existing files
+  (if files
+      (git-status-filenames-map
+       git-status (lambda (info) (setf (git-fileinfo->needs-update info) t)) files)
+    (ewoc-map (lambda (info) (setf (git-fileinfo->needs-update info) t) nil) git-status)
+    (git-call-process nil "update-index" "--refresh")
+    (when git-show-uptodate
+      (git-run-ls-files-cached git-status nil 'uptodate)))
+  (let ((remaining-files
           (if (git-empty-db-p) ; we need some special handling for an empty db
              (git-run-ls-files-cached git-status files 'added)
             (git-run-diff-index git-status files))))
@@ -746,13 +791,17 @@ Return the list of files that haven't been handled."
       (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)
+    (unless files
+      (setq remaining-files (git-get-filenames (ewoc-collect git-status #'git-fileinfo->needs-update))))
+    (when remaining-files
+      (setq remaining-files (git-run-ls-files-cached git-status remaining-files 'uptodate)))
+    (git-set-filenames-state git-status remaining-files nil)
+    (when mark-files (git-mark-files git-status files))
     (git-refresh-files)
     (git-refresh-ewoc-hf git-status)))
 
 (defun git-mark-files (status files)
   "Mark all the specified FILES, and unmark the others."
-  (setq files (sort files #'string-lessp))
   (let ((file (and files (pop files)))
         (node (ewoc-nth status 0)))
     (while node
@@ -778,13 +827,13 @@ Return the list of files that haven't been handled."
       (list (ewoc-data (ewoc-locate git-status)))))
 
 (defun git-marked-files-state (&rest states)
-  "Return marked files that are in the specified states."
+  "Return a sorted list of marked files that are in the specified states."
   (let ((files (git-marked-files))
         result)
     (dolist (info files)
       (when (memq (git-fileinfo->state info) states)
         (push info result)))
-    result))
+    (nreverse result)))
 
 (defun git-refresh-files ()
   "Refresh all files that need it and clear the needs-refresh flag."
@@ -824,19 +873,18 @@ Return the list of files that haven't been handled."
 
 (defun git-update-index (index-file files)
   "Run git-update-index on a list of files."
-  (let ((env (and index-file `(("GIT_INDEX_FILE" . ,index-file))))
+  (let ((process-environment (append (and index-file (list (concat "GIT_INDEX_FILE=" index-file)))
+                                     process-environment))
         added deleted modified)
     (dolist (info files)
       (case (git-fileinfo->state info)
         ('added (push info added))
         ('deleted (push info deleted))
         ('modified (push info modified))))
-    (when added
-      (apply #'git-call-process-env nil env "update-index" "--add" "--" (git-get-filenames added)))
-    (when deleted
-      (apply #'git-call-process-env nil env "update-index" "--remove" "--" (git-get-filenames deleted)))
-    (when modified
-      (apply #'git-call-process-env nil env "update-index" "--" (git-get-filenames modified)))))
+    (and
+     (or (not added) (apply #'git-call-process-display-error "update-index" "--add" "--" (git-get-filenames added)))
+     (or (not deleted) (apply #'git-call-process-display-error "update-index" "--remove" "--" (git-get-filenames deleted)))
+     (or (not modified) (apply #'git-call-process-display-error "update-index" "--" (git-get-filenames modified))))))
 
 (defun git-run-pre-commit-hook ()
   "Run the pre-commit hook if any."
@@ -862,33 +910,30 @@ Return the list of files that haven't been handled."
           (message "You cannot commit unmerged files, resolve them first.")
         (unwind-protect
             (let ((files (git-marked-files-state 'added 'deleted 'modified))
-                  head head-tree)
+                  head tree head-tree)
               (unless (git-empty-db-p)
                 (setq head (git-rev-parse "HEAD")
                       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
-                    (let ((tree (git-write-tree index-file)))
-                      (if (or (not (string-equal tree head-tree))
-                              (yes-or-no-p "The tree was not modified, do you really want to perform an empty commit? "))
-                          (let ((commit (git-commit-tree buffer tree head)))
-                            (when commit
-                              (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-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)
-                              (git-run-hook "post-commit" nil)))
-                        (message "Commit aborted."))))
-                (message "No files to commit.")))
+              (message "Running git commit...")
+              (when
+                  (and
+                   (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
+                   (setq tree (git-write-tree index-file)))
+                (if (or (not (string-equal tree head-tree))
+                        (yes-or-no-p "The tree was not modified, do you really want to perform an empty commit? "))
+                    (let ((commit (git-commit-tree buffer tree head)))
+                      (when commit
+                        (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-update-status-files (git-get-filenames files))
+                        (git-call-process nil "rerere")
+                        (git-call-process nil "gc" "--auto")
+                        (message "Committed %s." commit)
+                        (git-run-hook "post-commit" nil)))
+                  (message "Commit aborted."))))
           (delete-file index-file))))))
 
 
@@ -990,6 +1035,11 @@ Return the list of files that haven't been handled."
       (setq node (ewoc-prev git-status node)))
     (ewoc-goto-node git-status last)))
 
+(defun git-insert-file (file)
+  "Insert file(s) into the git-status buffer."
+  (interactive "fInsert file: ")
+  (git-update-status-files (list (file-relative-name file))))
+
 (defun git-add-file ()
   "Add marked file(s) to the index cache."
   (interactive)
@@ -998,7 +1048,7 @@ Return the list of files that haven't been handled."
     (unless files
       (push (file-relative-name (read-file-name "File to add: " nil nil t)) files))
     (when (apply 'git-call-process-display-error "update-index" "--add" "--" files)
-      (git-update-status-files files 'uptodate)
+      (git-update-status-files files)
       (git-success-message "Added" files))))
 
 (defun git-ignore-file ()
@@ -1008,7 +1058,7 @@ 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)
     (git-success-message "Ignored" files)))
 
 (defun git-remove-file ()
@@ -1018,7 +1068,9 @@ Return the list of files that haven't been handled."
     (unless files
       (push (file-relative-name (read-file-name "File to remove: " nil nil t)) files))
     (if (yes-or-no-p
-         (format "Remove %d file%s? " (length files) (if (> (length files) 1) "s" "")))
+         (if (cdr files)
+             (format "Remove %d files? " (length files))
+           (format "Remove %s? " (car files))))
         (progn
           (dolist (name files)
             (ignore-errors
@@ -1026,7 +1078,7 @@ Return the list of files that haven't been handled."
                   (delete-directory name)
                 (delete-file name))))
           (when (apply 'git-call-process-display-error "update-index" "--remove" "--" files)
-            (git-update-status-files files nil)
+            (git-update-status-files files)
             (git-success-message "Removed" files)))
       (message "Aborting"))))
 
@@ -1037,7 +1089,9 @@ Return the list of files that haven't been handled."
         added modified)
     (when (and files
                (yes-or-no-p
-                (format "Revert %d file%s? " (length files) (if (> (length files) 1) "s" ""))))
+                (if (cdr files)
+                    (format "Revert %d files? " (length files))
+                  (format "Revert %s? " (git-fileinfo->name (car files))))))
       (dolist (info files)
         (case (git-fileinfo->state info)
           ('added (push (git-fileinfo->name info) added))
@@ -1053,13 +1107,14 @@ Return the list of files that haven't been handled."
                  (or (not added)
                      (apply 'git-call-process-display-error "update-index" "--force-remove" "--" added))
                  (or (not modified)
-                     (apply 'git-call-process-display-error "checkout" "HEAD" modified)))))
-        (git-update-status-files (append added modified) 'uptodate)
+                     (apply 'git-call-process-display-error "checkout" "HEAD" modified))))
+            (names (git-get-filenames files)))
+        (git-update-status-files names)
         (when ok
           (dolist (file modified)
             (let ((buffer (get-file-buffer file)))
               (when buffer (with-current-buffer buffer (revert-buffer t t t)))))
-          (git-success-message "Reverted" (git-get-filenames files)))))))
+          (git-success-message "Reverted" names))))))
 
 (defun git-resolve-file ()
   "Resolve conflicts in marked file(s)."
@@ -1067,7 +1122,7 @@ Return the list of files that haven't been handled."
   (let ((files (git-get-filenames (git-marked-files-state 'unmerged))))
     (when files
       (when (apply 'git-call-process-display-error "update-index" "--" files)
-        (git-update-status-files files 'uptodate)
+        (git-update-status-files files)
         (git-success-message "Resolved" files)))))
 
 (defun git-remove-handled ()
@@ -1225,11 +1280,10 @@ Return the list of files that haven't been handled."
       (goto-char (point-max))
       (insert sign-off "\n"))))
 
-(defun git-setup-log-buffer (buffer &optional author-name author-email subject date msg)
+(defun git-setup-log-buffer (buffer &optional merge-heads author-name author-email subject date msg)
   "Setup the log buffer for a commit."
   (unless git-status (error "Not in git-status buffer."))
-  (let ((merge-heads (git-get-merge-heads))
-        (dir default-directory)
+  (let ((dir default-directory)
         (committer-name (git-get-committer-name))
         (committer-email (git-get-committer-email))
         (sign-off git-append-signed-off-by))
@@ -1243,17 +1297,16 @@ Return the list of files that haven't been handled."
                 (or author-email committer-email)
                 (if date (format "Date: %s\n" date) "")
                 (if merge-heads
-                    (format "Parent: %s\n%s\n"
-                            (git-rev-parse "HEAD")
-                            (mapconcat (lambda (str) (concat "Parent: " str)) merge-heads "\n"))
+                    (format "Merge: %s\n"
+                            (mapconcat 'identity merge-heads " "))
                   ""))
         'face 'git-header-face)
        (propertize git-log-msg-separator 'face 'git-separator-face)
        "\n")
       (when subject (insert subject "\n\n"))
       (cond (msg (insert msg "\n"))
-            ((file-readable-p ".dotest/msg")
-             (insert-file-contents ".dotest/msg"))
+            ((file-readable-p ".git/rebase-apply/msg")
+             (insert-file-contents ".git/rebase-apply/msg"))
             ((file-readable-p ".git/MERGE_MSG")
              (insert-file-contents ".git/MERGE_MSG")))
       ; delete empty lines at end
@@ -1272,9 +1325,9 @@ Return the list of files that haven't been handled."
           (coding-system (git-get-commits-coding-system))
           author-name author-email subject date)
       (when (eq 0 (buffer-size buffer))
-        (when (file-readable-p ".dotest/info")
+        (when (file-readable-p ".git/rebase-apply/info")
           (with-temp-buffer
-            (insert-file-contents ".dotest/info")
+            (insert-file-contents ".git/rebase-apply/info")
             (goto-char (point-min))
             (when (re-search-forward "^Author: \\(.*\\)\nEmail: \\(.*\\)$" nil t)
               (setq author-name (match-string 1))
@@ -1285,22 +1338,25 @@ Return the list of files that haven't been handled."
             (goto-char (point-min))
             (when (re-search-forward "^Date: \\(.*\\)$" nil t)
               (setq date (match-string 1)))))
-        (git-setup-log-buffer buffer author-name author-email subject date))
+        (git-setup-log-buffer buffer (git-get-merge-heads) author-name author-email subject date))
       (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 paragraph-separate (concat (regexp-quote git-log-msg-separator) "$\\|Author: \\|Date: \\|Merge: \\|Signed-off-by: \\|\f\\|[        ]*$"))
       (setq buffer-file-coding-system coding-system)
       (re-search-forward (regexp-quote (concat git-log-msg-separator "\n")) nil t))))
 
 (defun git-setup-commit-buffer (commit)
   "Setup the commit buffer with the contents of COMMIT."
-  (let (author-name author-email subject date msg)
+  (let (parents author-name author-email subject date msg)
     (with-temp-buffer
       (let ((coding-system (git-get-logoutput-coding-system)))
-        (git-call-process-env t nil "log" "-1" "--pretty=medium" commit)
+        (git-call-process t "log" "-1" "--pretty=medium" "--abbrev=40" commit)
         (goto-char (point-min))
+        (when (re-search-forward "^Merge: *\\(.*\\)$" nil t)
+          (setq parents (cdr (split-string (match-string 1) " +"))))
         (when (re-search-forward "^Author: *\\(.*\\) <\\(.*\\)>$" nil t)
           (setq author-name (match-string 1))
           (setq author-email (match-string 2)))
@@ -1312,18 +1368,48 @@ Return the list of files that haven't been handled."
         (setq subject (pop msg))
         (while (and msg (zerop (length (car msg))) (pop msg)))))
     (git-setup-log-buffer (get-buffer-create "*git-commit*")
-                          author-name author-email subject date
+                          parents author-name author-email subject date
                           (mapconcat #'identity msg "\n"))))
 
 (defun git-get-commit-files (commit)
-  "Retrieve the list of files modified by COMMIT."
+  "Retrieve a sorted list of files modified by COMMIT."
   (let (files)
     (with-temp-buffer
-      (git-call-process-env t nil "diff-tree" "-r" "-z" "--name-only" "--no-commit-id" commit)
+      (git-call-process t "diff-tree" "-m" "-r" "-z" "--name-only" "--no-commit-id" "--root" commit)
       (goto-char (point-min))
       (while (re-search-forward "\\([^\0]*\\)\0" nil t 1)
         (push (match-string 1) files)))
-    files))
+    (sort files #'string-lessp)))
+
+(defun git-read-commit-name (prompt &optional default)
+  "Ask for a commit name, with completion for local branch, remote branch and tag."
+  (completing-read prompt
+                   (list* "HEAD" "ORIG_HEAD" "FETCH_HEAD" (mapcar #'car (git-for-each-ref)))
+                  nil nil nil nil default))
+
+(defun git-checkout (branch &optional merge)
+  "Checkout a branch, tag, or any commit.
+Use a prefix arg if git should merge while checking out."
+  (interactive
+   (list (git-read-commit-name "Checkout: ")
+         current-prefix-arg))
+  (unless git-status (error "Not in git-status buffer."))
+  (let ((args (list branch "--")))
+    (when merge (push "-m" args))
+    (when (apply #'git-call-process-display-error "checkout" args)
+      (git-update-status-files))))
+
+(defun git-branch (branch)
+  "Create a branch from the current HEAD and switch to it."
+  (interactive (list (git-read-commit-name "Branch: ")))
+  (unless git-status (error "Not in git-status buffer."))
+  (if (git-rev-parse (concat "refs/heads/" branch))
+      (if (yes-or-no-p (format "Branch %s already exists, replace it? " branch))
+          (and (git-call-process-display-error "branch" "-f" branch)
+               (git-call-process-display-error "checkout" branch))
+        (message "Canceled."))
+    (git-call-process-display-error "checkout" "-b" branch))
+    (git-refresh-ewoc-hf git-status))
 
 (defun git-amend-commit ()
   "Undo the last commit on HEAD, and set things up to commit an
@@ -1333,13 +1419,52 @@ amended version of it."
   (when (git-empty-db-p) (error "No commit to amend."))
   (let* ((commit (git-rev-parse "HEAD"))
          (files (git-get-commit-files commit)))
-    (when (git-call-process-display-error "reset" "--soft" "HEAD^")
-      (git-update-status-files (copy-sequence files) 'uptodate)
-      (git-mark-files git-status files)
-      (git-refresh-files)
+    (when (if (git-rev-parse "HEAD^")
+              (git-call-process-display-error "reset" "--soft" "HEAD^")
+            (and (git-update-ref "ORIG_HEAD" commit)
+                 (git-update-ref "HEAD" nil commit)))
+      (git-update-status-files files t)
       (git-setup-commit-buffer commit)
       (git-commit-file))))
 
+(defun git-cherry-pick-commit (arg)
+  "Cherry-pick a commit."
+  (interactive (list (git-read-commit-name "Cherry-pick commit: ")))
+  (unless git-status (error "Not in git-status buffer."))
+  (let ((commit (git-rev-parse (concat arg "^0"))))
+    (unless commit (error "Not a valid commit '%s'." arg))
+    (when (git-rev-parse (concat commit "^2"))
+      (error "Cannot cherry-pick a merge commit."))
+    (let ((files (git-get-commit-files commit))
+          (ok (git-call-process-display-error "cherry-pick" "-n" commit)))
+      (git-update-status-files files ok)
+      (with-current-buffer (git-setup-commit-buffer commit)
+        (goto-char (point-min))
+        (if (re-search-forward "^\n*Signed-off-by:" nil t 1)
+            (goto-char (match-beginning 0))
+          (goto-char (point-max)))
+        (insert "(cherry picked from commit " commit ")\n"))
+      (when ok (git-commit-file)))))
+
+(defun git-revert-commit (arg)
+  "Revert a commit."
+  (interactive (list (git-read-commit-name "Revert commit: ")))
+  (unless git-status (error "Not in git-status buffer."))
+  (let ((commit (git-rev-parse (concat arg "^0"))))
+    (unless commit (error "Not a valid commit '%s'." arg))
+    (when (git-rev-parse (concat commit "^2"))
+      (error "Cannot revert a merge commit."))
+    (let ((files (git-get-commit-files commit))
+          (subject (git-get-commit-description commit))
+          (ok (git-call-process-display-error "revert" "-n" commit)))
+      (git-update-status-files files ok)
+      (when (string-match "^[0-9a-f]+ - \\(.*\\)$" subject)
+        (setq subject (match-string 1 subject)))
+      (git-setup-log-buffer (get-buffer-create "*git-commit*")
+                            (git-get-merge-heads) nil nil (format "Revert \"%s\"" subject) nil
+                            (format "This reverts commit %s.\n" commit))
+      (when ok (git-commit-file)))))
+
 (defun git-find-file ()
   "Visit the current file in its own buffer."
   (interactive)
@@ -1377,27 +1502,10 @@ amended version of it."
 (defun git-refresh-status ()
   "Refresh the git status buffer."
   (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."))
-    (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)))))
+  (unless git-status (error "Not in git-status buffer."))
+  (message "Refreshing git status...")
+  (git-update-status-files)
+  (message "Refreshing git status...done"))
 
 (defun git-status-quit ()
   "Quit git-status mode."
@@ -1434,6 +1542,7 @@ amended version of it."
     (define-key map "\r"  'git-find-file)
     (define-key map "g"   'git-refresh-status)
     (define-key map "i"   'git-ignore-file)
+    (define-key map "I"   'git-insert-file)
     (define-key map "l"   'git-log-file)
     (define-key map "m"   'git-mark-file)
     (define-key map "M"   'git-mark-all)
@@ -1455,6 +1564,10 @@ amended version of it."
     (define-key map "\M-\C-?" 'git-unmark-all)
     ; the commit submap
     (define-key commit-map "\C-a" 'git-amend-commit)
+    (define-key commit-map "\C-b" 'git-branch)
+    (define-key commit-map "\C-o" 'git-checkout)
+    (define-key commit-map "\C-p" 'git-cherry-pick-commit)
+    (define-key commit-map "\C-v" 'git-revert-commit)
     ; the diff submap
     (define-key diff-map "b" 'git-diff-file-base)
     (define-key diff-map "c" 'git-diff-file-combined)
@@ -1475,6 +1588,10 @@ amended version of it."
     `("Git"
       ["Refresh" git-refresh-status t]
       ["Commit" git-commit-file t]
+      ["Checkout..." git-checkout t]
+      ["New Branch..." git-branch t]
+      ["Cherry-pick Commit..." git-cherry-pick-commit t]
+      ["Revert Commit..." git-revert-commit t]
       ("Merge"
        ["Next Unmerged File" git-next-unmerged-file t]
        ["Prev Unmerged File" git-prev-unmerged-file t]
@@ -1490,6 +1607,7 @@ amended version of it."
       ["Revert File" git-revert-file t]
       ["Ignore File" git-ignore-file t]
       ["Remove File" git-remove-file t]
+      ["Insert File" git-insert-file t]
       "--------"
       ["Find File" git-find-file t]
       ["View File" git-view-file t]
@@ -1576,8 +1694,8 @@ Meant to be used in `after-save-hook'."
         (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)))))))
+            (git-call-process nil "add" "--refresh" "--" filename)
+            (git-update-status-files (list filename))))))))
 
 (defun git-help ()
   "Display help for Git mode."