(eval-when-compile (require 'cl))
(require 'ewoc)
+(require 'log-edit)
;;;; Customizations
;;;; ------------------------------------------------------------
(defgroup git nil
- "Git user interface")
+ "A user interface for the git versioning system."
+ :group 'tools)
(defcustom git-committer-name nil
"User name to use for commits.
-The default is to fall back to `add-log-full-name' and then `user-full-name'."
+The default is to fall back to the repository config,
+then to `add-log-full-name' and then to `user-full-name'."
:group 'git
:type '(choice (const :tag "Default" nil)
(string :tag "Name")))
(defcustom git-committer-email nil
"Email address to use for commits.
-The default is to fall back to `add-log-mailing-address' and then `user-mail-address'."
+The default is to fall back to the git repository config,
+then to `add-log-mailing-address' and then to `user-mail-address'."
:group 'git
: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."
:group 'git
:type 'boolean)
+(defcustom git-reuse-status-buffer t
+ "Whether `git-status' should try to reuse an existing buffer
+if there is already one that displays the same directory."
+ :group 'git
+ :type 'boolean)
+
(defcustom git-per-dir-ignore-file ".gitignore"
"Name of the per-directory ignore file."
:group 'git
:type 'string)
+
(defface git-status-face
'((((class color) (background light)) (:foreground "purple")))
"Git mode face used to highlight added and modified files."
(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))
(append (git-get-env-strings env) (list "git") args))
(apply #'call-process "git" nil buffer nil args)))
+(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."
+ (with-temp-buffer
+ (and (eq 0 (apply #' git-call-process-env t env args))
+ (buffer-string))))
+
(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))
(defun git-get-string-sha1 (string)
"Read a SHA1 from the specified string."
- (let ((pos (string-match "[0-9a-f]\\{40\\}" string)))
- (and pos (substring string pos (match-end 0)))))
+ (and string
+ (string-match "[0-9a-f]\\{40\\}" string)
+ (match-string 0 string)))
(defun git-get-committer-name ()
"Return the name to use as GIT_COMMITTER_NAME."
; copied from log-edit
(or git-committer-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)))
"Return the email address to use as GIT_COMMITTER_EMAIL."
; copied from log-edit
(or git-committer-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-escape-file-name (name)
"Escape a file name if necessary."
(if (string-match "[\n\t\"\\]" name)
(set-buffer (find-file-noselect ignore-name))
(goto-char (point-max))
(unless (zerop (current-column)) (insert "\n"))
- (insert name "\n")
+ (insert "/" name "\n")
(sort-lines nil (point-min) (point-max))
(save-buffer))
(when created
(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
;;;; ------------------------------------------------------------
(defun git-rev-parse (rev)
"Parse a revision name and return its SHA1."
(git-get-string-sha1
- (with-output-to-string
- (with-current-buffer standard-output
- (git-call-process-env t nil "rev-parse" rev)))))
+ (git-call-process-env-string nil "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)))
+ (and str (car (split-string str "\n")))))
(defun git-symbolic-ref (ref)
"Wrapper for the git-symbolic-ref command."
- (car
- (split-string
- (with-output-to-string
- (with-current-buffer standard-output
- (git-call-process-env t nil "symbolic-ref" ref)))
- "\n")))
+ (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)
"Update a reference by calling git-update-ref."
(defun git-write-tree (&optional index-file)
"Call git-write-tree and return the resulting tree SHA1 as a string."
(git-get-string-sha1
- (with-output-to-string
- (with-current-buffer standard-output
- (git-call-process-env t
- (if index-file `(("GIT_INDEX_FILE" . ,index-file)) nil)
- "write-tree")))))
+ (git-call-process-env-string (and index-file `(("GIT_INDEX_FILE" . ,index-file))) "write-tree")))
(defun git-commit-tree (buffer tree head)
"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)
+ author-date log-start log-end args coding-system-for-write)
(when head
(push "-p" args)
(push head args))
(push "-p" args)
(push (match-string 1) args))))
(setq log-start (point-min)))
- (setq log-end (point-max)))
+ (setq log-end (point-max))
+ (setq coding-system-for-write buffer-file-coding-system))
(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)
+ (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)))))
(propertize
(concat " ("
(if (eq state 'copy) "copied from "
- (if (eq (git-fileinfo->state info) 'added) "renamed to "
- "renamed from "))
+ (if (eq (git-fileinfo->state info) 'added) "renamed from "
+ "renamed to "))
(git-escape-file-name (git-fileinfo->orig-name info))
")") 'face 'git-status-face)
"")))
(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)
(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))
(unless git-status (error "Not in git-status buffer."))
(ewoc-goto-prev git-status n))
+(defun git-next-unmerged-file (&optional n)
+ "Move the selection down N unmerged files."
+ (interactive "p")
+ (unless git-status (error "Not in git-status buffer."))
+ (let* ((last (ewoc-locate git-status))
+ (node (ewoc-next git-status last)))
+ (while (and node (> n 0))
+ (when (eq 'unmerged (git-fileinfo->state (ewoc-data node)))
+ (setq n (1- n))
+ (setq last node))
+ (setq node (ewoc-next git-status node)))
+ (ewoc-goto-node git-status last)))
+
+(defun git-prev-unmerged-file (&optional n)
+ "Move the selection up N unmerged files."
+ (interactive "p")
+ (unless git-status (error "Not in git-status buffer."))
+ (let* ((last (ewoc-locate git-status))
+ (node (ewoc-prev git-status last)))
+ (while (and node (> n 0))
+ (when (eq 'unmerged (git-fileinfo->state (ewoc-data node)))
+ (setq n (1- n))
+ (setq last node))
+ (setq node (ewoc-prev git-status node)))
+ (ewoc-goto-node git-status last)))
+
(defun git-add-file ()
"Add marked file(s) to the index cache."
(interactive)
(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))))
(git-setup-diff-buffer
(apply #'git-run-command-buffer "*git-diff*" "diff-index" "-p" "-M" "HEAD" "--" (git-get-filenames files)))))
+(defun git-diff-file-merge-head (arg)
+ "Diff the marked file(s) against the first merge head (or the nth one with a numeric prefix)."
+ (interactive "p")
+ (let ((files (git-marked-files))
+ (merge-heads (git-get-merge-heads)))
+ (unless merge-heads (error "No merge in progress"))
+ (git-setup-diff-buffer
+ (apply #'git-run-command-buffer "*git-diff*" "diff-index" "-p" "-M"
+ (or (nth (1- arg) merge-heads) "HEAD") "--" (git-get-filenames files)))))
+
(defun git-diff-unmerged-file (stage)
"Diff the marked unmerged file(s) against the specified stage."
(let ((files (git-marked-files)))
(let ((buffer (get-buffer-create "*git-commit*"))
(merge-heads (git-get-merge-heads))
(dir default-directory)
+ (coding-system (git-get-commits-coding-system))
(sign-off git-append-signed-off-by))
(with-current-buffer buffer
(when (eq 0 (buffer-size))
'face 'git-header-face)
(propertize git-log-msg-separator 'face 'git-separator-face)
"\n")
- (cond ((and merge-heads (file-readable-p ".git/MERGE_MSG"))
+ (cond ((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))))
+ (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."
(when (eq 'unmerged (git-fileinfo->state info))
(smerge-mode))))
+(defun git-find-file-other-window ()
+ "Visit the current file in its own buffer in another window."
+ (interactive)
+ (unless git-status (error "Not in git-status buffer."))
+ (let ((info (ewoc-data (ewoc-locate git-status))))
+ (find-file-other-window (git-fileinfo->name info))
+ (when (eq 'unmerged (git-fileinfo->state info))
+ (smerge-mode))))
+
(defun git-find-file-imerge ()
"Visit the current file in interactive merge mode."
(interactive)
(let ((map (make-keymap))
(diff-map (make-sparse-keymap)))
(suppress-keymap map)
+ (define-key map "?" 'git-help)
+ (define-key map "h" 'git-help)
(define-key map " " 'git-next-file)
(define-key map "a" 'git-add-file)
(define-key map "c" 'git-commit-file)
(define-key map "m" 'git-mark-file)
(define-key map "M" 'git-mark-all)
(define-key map "n" 'git-next-file)
+ (define-key map "N" 'git-next-unmerged-file)
+ (define-key map "o" 'git-find-file-other-window)
(define-key map "p" 'git-prev-file)
+ (define-key map "P" 'git-prev-unmerged-file)
(define-key map "q" 'git-status-quit)
(define-key map "r" 'git-remove-file)
(define-key map "R" 'git-resolve-file)
(define-key diff-map "=" 'git-diff-file)
(define-key diff-map "e" 'git-diff-file-idiff)
(define-key diff-map "E" 'git-find-file-imerge)
+ (define-key diff-map "h" 'git-diff-file-merge-head)
(define-key diff-map "m" 'git-diff-file-mine)
(define-key diff-map "o" 'git-diff-file-other)
(setq git-status-mode-map map)))
(set (make-local-variable 'list-buffers-directory) default-directory)
(run-hooks 'git-status-mode-hook)))
+(defun git-find-status-buffer (dir)
+ "Find the git status buffer handling a specified directory."
+ (let ((list (buffer-list))
+ (fulldir (expand-file-name dir))
+ found)
+ (while (and list (not found))
+ (let ((buffer (car list)))
+ (with-current-buffer buffer
+ (when (and list-buffers-directory
+ (string-equal fulldir (expand-file-name list-buffers-directory))
+ (string-match "\\*git-status\\*$" (buffer-name buffer)))
+ (setq found buffer))))
+ (setq list (cdr list)))
+ found))
+
(defun git-status (dir)
"Entry point into git-status mode."
(interactive "DSelect directory: ")
(setq dir (git-get-top-dir dir))
(if (file-directory-p (concat (file-name-as-directory dir) ".git"))
- (let ((buffer (create-file-buffer (expand-file-name "*git-status*" dir))))
+ (let ((buffer (or (and git-reuse-status-buffer (git-find-status-buffer dir))
+ (create-file-buffer (expand-file-name "*git-status*" dir)))))
(switch-to-buffer buffer)
(cd dir)
(git-status-mode)
(goto-char (point-min)))
(message "%s is not a git working tree." dir)))
+(defun git-help ()
+ "Display help for Git mode."
+ (interactive)
+ (describe-function 'git-status-mode))
+
(provide 'git)
;;; git.el ends here