;;; 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))
(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) "\\)$")
"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."
(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))
(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."
(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."
(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"))))))
(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
(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")))
-
-(defun git-commit-tree (buffer tree head)
- "Call git-commit-tree with buffer as input and return the resulting commit SHA1."
+ (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 parent)
+ "Create a commit and possibly update HEAD.
+Create a commit with the message in BUFFER using the tree with hash TREE.
+Use PARENT as the parent of the new commit. If PARENT is the current \"HEAD\",
+update the \"HEAD\" reference to the new commit."
(let ((author-name (git-get-committer-name))
(author-email (git-get-committer-email))
(subject "commit (initial): ")
author-date log-start log-end args coding-system-for-write)
- (when head
+ (when parent
(setq subject "commit: ")
(push "-p" args)
- (push head args))
+ (push parent args))
(with-current-buffer buffer
(goto-char (point-min))
(if
(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)
(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 parent 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."
(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))))
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)
(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
(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)
(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"
(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."
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 ()
(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))))
(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
(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."
(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."
(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))))))
(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)
(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 ()
(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 ()
(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
(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"))))
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))
(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)."
(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 ()
(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))
(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
(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))
(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)))
(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
(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)
(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."
(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)
(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)
`("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]
["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]
(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."