Merge branch 'jc/attr'
[gitweb.git] / contrib / emacs / git.el
index 08d6404bcbafced3c1a3ade06355865ae75a0f33..f60017948f4eb45f6aa7fe0fc60ca1507cec98f2 100644 (file)
@@ -1,6 +1,6 @@
 ;;; git.el --- A user interface for git
 
-;; Copyright (C) 2005, 2006 Alexandre Julliard <julliard@winehq.org>
+;; Copyright (C) 2005, 2006, 2007 Alexandre Julliard <julliard@winehq.org>
 
 ;; Version: 1.0
 
@@ -49,6 +49,7 @@
 
 (eval-when-compile (require 'cl))
 (require 'ewoc)
+(require 'log-edit)
 
 
 ;;;; Customizations
@@ -74,10 +75,11 @@ then to `add-log-mailing-address' and then to `user-mail-address'."
   :type '(choice (const :tag "Default" nil)
                  (string :tag "Email")))
 
-(defcustom git-commits-coding-system 'utf-8
+(defcustom git-commits-coding-system nil
   "Default coding system for the log message of git commits."
   :group 'git
-  :type 'coding-system)
+  :type '(choice (const :tag "From repository config" nil)
+                 (coding-system)))
 
 (defcustom git-append-signed-off-by nil
   "Whether to append a Signed-off-by line to the commit message before editing."
@@ -147,6 +149,13 @@ 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:\\)\\(.*\\)$"
+     (1 font-lock-keyword-face)
+     (2 font-lock-function-name-face))
+    (,(concat "^\\(" (regexp-quote git-log-msg-separator) "\\)$")
+     (1 font-lock-comment-face))))
+
 (defun git-get-env-strings (env)
   "Build a list of NAME=VALUE strings from a list of environment strings."
   (mapcar (lambda (entry) (concat (car entry) "=" (cdr entry))) env))
@@ -204,6 +213,23 @@ and returns the process output as a string."
     (error "Failed to run \"git %s\":\n%s" (mapconcat (lambda (x) x) args " ") (buffer-string)))
   (message "Running git %s...done" (car args)))
 
+(defun git-run-hook (hook env &rest args)
+  "Run a git hook and display its output if any."
+  (let ((dir default-directory)
+        (hook-name (expand-file-name (concat ".git/hooks/" hook))))
+    (or (not (file-executable-p hook-name))
+        (let (status (buffer (get-buffer-create "*Git Hook Output*")))
+          (with-current-buffer buffer
+            (erase-buffer)
+            (cd dir)
+            (setq status
+                  (if env
+                      (apply #'call-process "env" nil (list buffer t) nil
+                             (append (git-get-env-strings env) (list hook-name) args))
+                    (apply #'call-process hook-name nil (list buffer t) nil args))))
+          (display-message-or-buffer buffer)
+          (eq 0 status)))))
+
 (defun git-get-string-sha1 (string)
   "Read a SHA1 from the specified string."
   (and string
@@ -214,7 +240,7 @@ and returns the process output as a string."
   "Return the name to use as GIT_COMMITTER_NAME."
   ; copied from log-edit
   (or git-committer-name
-      (git-repo-config "user.name")
+      (git-config "user.name")
       (and (boundp 'add-log-full-name) add-log-full-name)
       (and (fboundp 'user-full-name) (user-full-name))
       (and (boundp 'user-full-name) user-full-name)))
@@ -223,11 +249,30 @@ and returns the process output as a string."
   "Return the email address to use as GIT_COMMITTER_EMAIL."
   ; copied from log-edit
   (or git-committer-email
-      (git-repo-config "user.email")
+      (git-config "user.email")
       (and (boundp 'add-log-mailing-address) add-log-mailing-address)
       (and (fboundp 'user-mail-address) (user-mail-address))
       (and (boundp 'user-mail-address) user-mail-address)))
 
+(defun git-get-commits-coding-system ()
+  "Return the coding system to use for commits."
+  (let ((repo-config (git-config "i18n.commitencoding")))
+    (or git-commits-coding-system
+        (and repo-config
+             (fboundp 'locale-charset-to-coding-system)
+             (locale-charset-to-coding-system repo-config))
+      'utf-8)))
+
+(defun git-get-logoutput-coding-system ()
+  "Return the coding system used for git-log output."
+  (let ((repo-config (or (git-config "i18n.logoutputencoding")
+                         (git-config "i18n.commitencoding"))))
+    (or git-commits-coding-system
+        (and repo-config
+             (fboundp 'locale-charset-to-coding-system)
+             (locale-charset-to-coding-system repo-config))
+      'utf-8)))
+
 (defun git-escape-file-name (name)
   "Escape a file name if necessary."
   (if (string-match "[\n\t\"\\]" name)
@@ -272,6 +317,15 @@ and returns the process output as a string."
     (git-run-command nil nil "update-index" "--info-only" "--add" "--" (file-relative-name ignore-name)))
   (git-add-status-file (if created 'added 'modified) (file-relative-name ignore-name))))
 
+; propertize definition for XEmacs, stolen from erc-compat
+(eval-when-compile
+  (unless (fboundp 'propertize)
+    (defun propertize (string &rest props)
+      (let ((string (copy-sequence string)))
+        (while props
+          (put-text-property 0 (length string) (nth 0 props) (nth 1 props) string)
+          (setq props (cddr props)))
+        string))))
 
 ;;;; Wrappers for basic git commands
 ;;;; ------------------------------------------------------------
@@ -281,9 +335,9 @@ and returns the process output as a string."
   (git-get-string-sha1
    (git-call-process-env-string nil "rev-parse" rev)))
 
-(defun git-repo-config (key)
+(defun git-config (key)
   "Retrieve the value associated to KEY in the git repository config file."
-  (let ((str (git-call-process-env-string nil "repo-config" key)))
+  (let ((str (git-call-process-env-string nil "config" key)))
     (and str (car (split-string str "\n")))))
 
 (defun git-symbolic-ref (ref)
@@ -291,9 +345,15 @@ and returns the process output as a string."
   (let ((str (git-call-process-env-string nil "symbolic-ref" ref)))
     (and str (car (split-string str "\n")))))
 
-(defun git-update-ref (ref val &optional oldval)
+(defun git-update-ref (ref newval &optional oldval reason)
   "Update a reference by calling git-update-ref."
-  (apply #'git-call-process-env nil nil "update-ref" ref val (if oldval (list oldval))))
+  (let ((args (and oldval (list oldval))))
+    (push newval args)
+    (push ref args)
+    (when reason
+     (push reason args)
+     (push "-m" args))
+    (eq 0 (apply #'git-call-process-env nil nil "update-ref" args))))
 
 (defun git-read-tree (tree &optional index-file)
   "Read a tree into the index file."
@@ -310,8 +370,10 @@ and returns the process output as a string."
   "Call git-commit-tree with buffer as input and return the resulting commit SHA1."
   (let ((author-name (git-get-committer-name))
         (author-email (git-get-committer-email))
-        author-date log-start log-end args)
+        (subject "commit (initial): ")
+        author-date log-start log-end args coding-system-for-write)
     (when head
+      (setq subject "commit: ")
       (push "-p" args)
       (push head args))
     (with-current-buffer buffer
@@ -330,22 +392,29 @@ and returns the process output as a string."
             (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): ")
                 (push "-p" args)
                 (push (match-string 1) args))))
         (setq log-start (point-min)))
-      (setq log-end (point-max)))
-    (git-get-string-sha1
-     (with-output-to-string
-       (with-current-buffer standard-output
-         (let ((coding-system-for-write git-commits-coding-system)
-               (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))))))))
+      (setq log-end (point-max))
+      (goto-char log-start)
+      (when (re-search-forward ".*$" nil t)
+        (setq subject (concat subject (match-string 0))))
+      (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))))
 
 (defun git-empty-db-p ()
   "Check if the git db is empty (no commit done yet)."
@@ -362,6 +431,14 @@ and returns the process output as a string."
           (push (match-string 0) heads))))
     (nreverse heads)))
 
+(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)))
+      (if (and descr (string-match "\\`\\([0-9a-f]\\{40\\}\\) *\\(.*\\)$" descr))
+          (concat (substring (match-string 1 descr) 0 10) " - " (match-string 2 descr))
+        descr))))
+
 ;;;; File info structure
 ;;;; ------------------------------------------------------------
 
@@ -440,11 +517,10 @@ and returns the process output as a string."
 
 (defun git-fileinfo-prettyprint (info)
   "Pretty-printer for the git-fileinfo structure."
-  (insert (format "   %s %s %s  %s%s"
-                  (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))
+  (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-parse-status (status)
@@ -530,7 +606,7 @@ and returns the process output as a string."
   "Refresh the ewoc header and footer."
   (let ((branch (git-symbolic-ref "HEAD"))
         (head (if (git-empty-db-p) "Nothing committed yet"
-                (substring (git-rev-parse "HEAD") 0 10)))
+                (git-get-commit-description "HEAD")))
         (merge-heads (git-get-merge-heads)))
     (ewoc-set-hf status
                  (format "Directory:  %s\nBranch:     %s\nHead:       %s%s\n"
@@ -541,7 +617,7 @@ and returns the process output as a string."
                          head
                          (if merge-heads
                              (concat "\nMerging:    "
-                                     (mapconcat (lambda (str) (substring str 0 10)) merge-heads " "))
+                                     (mapconcat (lambda (str) (git-get-commit-description str)) merge-heads "\n            "))
                            ""))
                  (if (ewoc-nth status 0) "" "    No changes."))))
 
@@ -564,6 +640,20 @@ and returns the process output as a string."
     (when modified
       (apply #'git-run-command nil env "update-index" "--" (git-get-filenames modified)))))
 
+(defun git-run-pre-commit-hook ()
+  "Run the pre-commit hook if any."
+  (unless git-status (error "Not in git-status buffer."))
+  (let ((files (git-marked-files-state 'added 'deleted 'modified)))
+    (or (not files)
+        (not (file-executable-p ".git/hooks/pre-commit"))
+        (let ((index-file (make-temp-file "gitidx")))
+          (unwind-protect
+            (let ((head-tree (unless (git-empty-db-p) (git-rev-parse "HEAD^{tree}"))))
+              (git-read-tree head-tree index-file)
+              (git-update-index index-file files)
+              (git-run-hook "pre-commit" `(("GIT_INDEX_FILE" . ,index-file))))
+          (delete-file index-file))))))
+
 (defun git-do-commit ()
   "Perform the actual commit using the current buffer as log message."
   (interactive)
@@ -587,15 +677,16 @@ and returns the process output as a string."
                       (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)))
-                            (git-update-ref "HEAD" commit head)
                             (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)
                             (when (file-directory-p ".git/rr-cache")
                               (git-run-command nil nil "rerere"))
                             (git-refresh-files)
                             (git-refresh-ewoc-hf git-status)
-                            (message "Committed %s." commit))
+                            (message "Committed %s." commit)
+                            (git-run-hook "post-commit" nil))
                         (message "Commit aborted."))))
                 (message "No files to commit.")))
           (delete-file index-file))))))
@@ -776,7 +867,7 @@ and returns the process output as a string."
   (interactive)
   (let ((files (git-marked-files-state 'unmerged)))
     (when files
-      (apply #'git-run-command nil nil "update-index" "--info-only" "--" (git-get-filenames files))
+      (apply #'git-run-command nil nil "update-index" "--" (git-get-filenames files))
       (git-set-files-state files 'modified)
       (git-refresh-files))))
 
@@ -864,42 +955,82 @@ and returns the process output as a string."
   (with-current-buffer log-edit-parent-buffer
     (git-get-filenames (git-marked-files-state 'added 'deleted 'modified))))
 
-(defun git-commit-file ()
-  "Commit the marked file(s), asking for a commit message."
-  (interactive)
+(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))
+        (case-fold-search t))
+    (goto-char (point-min))
+    (unless (re-search-forward (concat "^" (regexp-quote sign-off)) nil t)
+      (goto-char (point-min))
+      (unless (re-search-forward "^Signed-off-by: " nil t)
+        (setq sign-off (concat "\n" sign-off)))
+      (goto-char (point-max))
+      (insert sign-off "\n"))))
+
+(defun git-setup-log-buffer (buffer &optional author-name author-email subject date msg)
+  "Setup the log buffer for a commit."
   (unless git-status (error "Not in git-status buffer."))
-  (let ((buffer (get-buffer-create "*git-commit*"))
-        (merge-heads (git-get-merge-heads))
+  (let ((merge-heads (git-get-merge-heads))
         (dir default-directory)
+        (committer-name (git-get-committer-name))
+        (committer-email (git-get-committer-email))
         (sign-off git-append-signed-off-by))
     (with-current-buffer buffer
-      (when (eq 0 (buffer-size))
-        (cd dir)
-        (erase-buffer)
-        (insert
-         (propertize
-          (format "Author: %s <%s>\n%s"
-                  (git-get-committer-name) (git-get-committer-email)
-                  (if merge-heads
-                      (format "Parent: %s\n%s\n"
-                              (git-rev-parse "HEAD")
-                              (mapconcat (lambda (str) (concat "Parent: " str)) merge-heads "\n"))
-                    ""))
-          'face 'git-header-face)
-         (propertize git-log-msg-separator 'face 'git-separator-face)
-         "\n")
-        (cond ((and merge-heads (file-readable-p ".git/MERGE_MSG"))
-               (insert-file-contents ".git/MERGE_MSG"))
-              (sign-off
-               (insert (format "\n\nSigned-off-by: %s <%s>\n"
-                               (git-get-committer-name) (git-get-committer-email)))))))
-    (let ((log-edit-font-lock-keywords
-           `(("^\\(Author:\\|Date:\\|Parent:\\|Signed-off-by:\\)\\(.*\\)"
-              (1 font-lock-keyword-face)
-              (2 font-lock-function-name-face))
-             (,(concat "^\\(" (regexp-quote git-log-msg-separator) "\\)$")
-              (1 font-lock-comment-face)))))
-      (log-edit #'git-do-commit nil #'git-log-edit-files buffer))))
+      (cd dir)
+      (erase-buffer)
+      (insert
+       (propertize
+        (format "Author: %s <%s>\n%s%s"
+                (or author-name committer-name)
+                (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"))
+                  ""))
+        '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/MERGE_MSG")
+             (insert-file-contents ".git/MERGE_MSG")))
+      ; delete empty lines at end
+      (goto-char (point-min))
+      (when (re-search-forward "\n+\\'" nil t)
+        (replace-match "\n" t t))
+      (when sign-off (git-append-sign-off committer-name committer-email)))))
+
+(defun git-commit-file ()
+  "Commit the marked file(s), asking for a commit message."
+  (interactive)
+  (unless git-status (error "Not in git-status buffer."))
+  (when (git-run-pre-commit-hook)
+    (let ((buffer (get-buffer-create "*git-commit*"))
+          (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")
+          (with-temp-buffer
+            (insert-file-contents ".dotest/info")
+            (goto-char (point-min))
+            (when (re-search-forward "^Author: \\(.*\\)\nEmail: \\(.*\\)$" nil t)
+              (setq author-name (match-string 1))
+              (setq author-email (match-string 2)))
+            (goto-char (point-min))
+            (when (re-search-forward "^Subject: \\(.*\\)$" nil t)
+              (setq subject (match-string 1)))
+            (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))
+      (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))))
 
 (defun git-find-file ()
   "Visit the current file in its own buffer."