{"thread":{"id":"2693","subject":"Emacs mode for git","startedAt":"2005-11-27T22:25:36Z","lastAt":"2005-11-27T22:52:25Z","messageCount":2,"participants":["Alexandre Julliard","Junio C Hamano"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"12821","messageId":"87u0dx7mov.fsf@wine.dyndns.org","threadId":"2693","inReplyTo":null,"subject":"Emacs mode for git","fromName":"Alexandre Julliard","fromEmail":"julliard@winehq.org","sentAt":"2005-11-27T22:25:36Z","receivedAt":"2005-11-27T22:25:36Z","isPatch":false,"sender":{"key":"julliard@winehq.org","avatar":null},"body":"Here's the beginning of an Emacs mode for git. My primary goal at this\npoint is to replicate the behavior of the pcl-cvs mode.\n\nMost basic functions are implemented (diff, commit, revert, add/remove\nfiles, .gitignore, etc.) There is currently no support for multiple\nbranches or file merges. The code has been tested only on Emacs 21, it\nwill probably need some tweaking to run on XEmacs.\n\nFeedback welcome...\n\n-- \nAlexandre Julliard\njulliard@winehq.org\n\n\n\n;;; git.el --- A user interface for git\n\n;; Copyright (C) 2005 Alexandre Julliard <julliard@winehq.org>\n\n;; Version: 0.1\n\n;; This program is free software; you can redistribute it and/or\n;; modify it under the terms of the GNU General Public License as\n;; published by the Free Software Foundation; either version 2 of\n;; the License, or (at your option) any later version.\n;;\n;; This program is distributed in the hope that it will be\n;; useful, but WITHOUT ANY WARRANTY; without even the implied\n;; warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n;; PURPOSE.  See the GNU General Public License for more details.\n;;\n;; You should have received a copy of the GNU General Public\n;; License along with this program; if not, write to the Free\n;; Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,\n;; MA 02111-1307 USA\n\n;;; Commentary:\n\n;; This file contains an interface for the git version control\n;; system. It provides easy access to the most frequently used git\n;; commands. The user interface is as far as possible identical to\n;; that of the PCL-CVS mode.\n;;\n;; To install: put this file on the load-path and place the following\n;; in your .emacs file:\n;;\n;;    (require 'git)\n;;\n;; To start: `M-x git-status'\n;;\n;; TODO\n;;  - better handling of subprocess errors\n;;  - GIT_DIR support\n;;  - hook into file save (after-save-hook)\n;;  - diff against other branch\n;;  - commands for renaming files\n;;  - fetch and merge\n;;\n\n(eval-when-compile (require 'cl))\n(require 'ewoc)\n\n\n;;;; Faces\n;;;; ------------------------------------------------------------\n\n(defface git-status-face\n  '((((class color) (background light)) (:foreground \"purple\")))\n  \"Git mode face used to highlight renamed files.\")\n\n(defface git-unmerged-face\n  '((((class color) (background light)) (:foreground \"red\" :bold t)))\n  \"Git mode face used to highlight unmerged files.\")\n\n(defface git-unknown-face\n  '((((class color) (background light)) (:foreground \"goldenrod\" :bold t)))\n  \"Git mode face used to highlight unknown files.\")\n\n(defface git-uptodate-face\n  '((((class color) (background light)) (:foreground \"grey60\")))\n  \"Git mode face used to highlight up-to-date files.\")\n\n(defface git-ignored-face\n  '((((class color) (background light)) (:foreground \"grey60\")))\n  \"Git mode face used to highlight ignored files.\")\n\n(defface git-mark-face\n  '((((class color) (background light)) (:foreground \"red\" :bold t)))\n  \"Git mode face used for the file marks.\")\n\n(defface git-header-face\n  '((((class color) (background light)) (:foreground \"blue\")))\n  \"Git mode face used for commit headers.\")\n\n(defface git-separator-face\n  '((((class color) (background light)) (:foreground \"brown\")))\n  \"Git mode face used for commit separator.\")\n\n(defface git-permission-face\n  '((((class color) (background light)) (:foreground \"green\" :bold t)))\n  \"Git mode face used for permission changes.\")\n\n(defvar git-committer-name nil\n  \"*User name to use for commits.\nIf not set, fall back to `add-log-full-name' and then `user-full-name'.\")\n\n(defvar git-committer-email nil\n  \"*Email address to use for commits.\nIf not set, fall back to `add-log-mailing-address' and then `user-mail-address'.\")\n\n(defconst git-log-msg-separator \"--- log message follows this line ---\")\n\n(defconst git-per-dir-ignore-file \".gitignore\"\n  \"Name of the per-directory ignore file.\")\n\n\n;;;; Utilities\n;;;; ------------------------------------------------------------\n\n(defun git-get-env-strings (env)\n  \"Build a list of NAME=VALUE strings from a list of environment strings.\"\n  (mapcar (lambda (entry) (concat (car entry) \"=\" (cdr entry))) env))\n\n(defun git-call-process-env (program infile buffer display env &rest args)\n  \"Wrapper for call-process that sets environment strings.\"\n  (let ((coding-system-for-read 'utf-8)\n        (coding-system-for-write 'utf-8))\n    (if env\n        (apply #'call-process \"env\" infile buffer display\n               (append (git-get-env-strings env) (list program) args))\n      (apply #'call-process program infile buffer display args))))\n\n(defun git-run-process-region (buffer start end args)\n  \"Run a git process with a buffer region as input.\"\n  (let ((output-buffer (current-buffer))\n        (dir default-directory))\n    (with-current-buffer buffer\n      (let ((coding-system-for-read 'utf-8)  ; FIXME: should be configurable\n            (coding-system-for-write 'utf-8))\n        (cd dir)\n        (apply #'call-process-region start end (car args)\n               nil (list output-buffer nil) nil (cdr args))))))\n\n(defun git-run-command-buffer (buffer-name &rest args)\n  \"Run a git command, sending the output to a buffer named BUFFER-NAME.\"\n  (let ((dir default-directory)\n        (buffer (get-buffer-create buffer-name)))\n    (message \"Running %s...\" (car args))\n    (with-current-buffer buffer\n      (let ((default-directory dir)\n            (buffer-read-only nil))\n        (erase-buffer)\n        (apply #'git-call-process-env (car args) nil buffer nil nil (cdr args))))\n    (message \"Running %s...done\" (car args))\n    buffer))\n\n(defun git-run-command (buffer env &rest args)\n  (message \"Running %s...\" (car args))\n  (apply #'git-call-process-env (car args) nil buffer nil env (cdr args))\n  (message \"Running %s...done\" (car args)))\n\n(defun git-run-command-region (buffer start end env &rest args)\n  \"Run a git command with specified buffer region as input.\"\n  (message \"Running %s...\" (car args))\n  (unless (eq 0 (git-run-process-region\n                 buffer start end\n                 (if env (append (list \"env\") (git-get-env-strings env) args) args)))\n    (error \"Failed to run \\\"%s\\\":\\n%s\" (mapconcat (lambda (x) x) args \" \") (buffer-string)))\n  (message \"Running %s...done\" (car args)))\n\n(defun git-get-buffer-sha1 (buffer)\n  \"Read a SHA1 string from the specified buffer.\"\n  (with-current-buffer buffer\n    (goto-char (point-min))\n    (and (re-search-forward \"[0-9a-f]\\\\{40\\\\}\")\n         (match-string 0))))\n\n(defun git-get-string-sha1 (string)\n  \"Read a SHA1 from the specified string.\"\n  (let ((pos (string-match \"[0-9a-f]\\\\{40\\\\}\" string)))\n    (and pos (substring string pos (match-end 0)))))\n\n(defun git-get-committer-name ()\n  \"Return the name to use as GIT_COMMITTER_NAME.\"\n  ; copied from log-edit\n  (or git-committer-name\n      (and (boundp 'add-log-full-name) add-log-full-name)\n      (and (fboundp 'user-full-name) (user-full-name))\n      (and (boundp 'user-full-name) user-full-name)))\n\n(defun git-get-committer-email ()\n  \"Return the email address to use as GIT_COMMITTER_EMAIL.\"\n  ; copied from log-edit\n  (or git-committer-email\n      (and (boundp 'add-log-mailing-address) add-log-mailing-address)\n      (and (fboundp 'user-mail-address) (user-mail-address))\n      (and (boundp 'user-mail-address) user-mail-address)))\n\n(defun git-escape-file-name (name)\n  \"Escape a file name if necessary.\"\n  (if (string-match \"[\\n\\t\\\"\\\\]\" name)\n      (concat \"\\\"\"\n              (mapconcat (lambda (c)\n                   (case c\n                     (?\\n \"\\\\n\")\n                     (?\\t \"\\\\t\")\n                     (?\\\\ \"\\\\\\\\\")\n                     (?\\\" \"\\\\\\\"\")\n                     (t (char-to-string c))))\n                 name \"\")\n              \"\\\"\")\n    name))\n\n;stolen from pcl-cvs\n(defun git-append-to-ignore (file)\n  \"Add a file name to the ignore file in its directory.\"\n  (let* ((fullname (expand-file-name file))\n         (dir (file-name-directory fullname))\n         (name (file-name-nondirectory fullname)))\n  (save-window-excursion\n    (set-buffer (find-file-noselect (expand-file-name git-per-dir-ignore-file dir)))\n    (goto-char (point-max))\n    (unless (zerop (current-column)) (insert \"\\n\"))\n    (insert name \"\\n\")\n    (sort-lines nil (point-min) (point-max))\n    (save-buffer))))\n\n\n;;;; Wrappers for basic git commands\n;;;; ------------------------------------------------------------\n\n(defun git-rev-parse (rev)\n  \"Parse a revision name and return its SHA1.\"\n  (git-get-string-sha1\n   (with-output-to-string\n     (with-current-buffer standard-output\n       (git-call-process-env \"git-rev-parse\" nil t nil nil rev)))))\n\n(defun git-symbolic-ref (ref)\n  \"Wrapper for the git-symbolic-ref command.\"\n  (car\n   (split-string\n    (with-output-to-string\n      (with-current-buffer standard-output\n        (git-call-process-env \"git-symbolic-ref\" nil t nil nil ref)))\n    \"\\n\")))\n\n(defun git-update-ref (ref val &optional oldval)\n  \"Update a reference by calling git-update-ref.\"\n  (apply #'git-call-process-env \"git-update-ref\" nil nil nil nil ref val (if oldval (list oldval))))\n\n(defun git-read-tree (tree &optional index-file)\n  \"Read a tree into the index file.\"\n  (apply #'git-call-process-env\n         \"git-read-tree\" nil nil nil\n         (if index-file `((\"GIT_INDEX_FILE\" . ,index-file)) nil)\n         (if tree (list tree))))\n\n(defun git-write-tree (&optional index-file)\n  \"Call git-write-tree and return the resulting tree SHA1 as a string.\"\n  (git-get-string-sha1\n   (with-output-to-string\n     (with-current-buffer standard-output\n       (git-call-process-env\n        \"git-write-tree\" nil t nil\n        (if index-file `((\"GIT_INDEX_FILE\" . ,index-file)) nil))))))\n\n(defun git-commit-tree (buffer tree parents)\n  \"Call git-commit-tree with buffer as input and return the resulting commit SHA1.\"\n  (let ((author-name (git-get-committer-name))\n        (author-email (git-get-committer-email))\n        author-date log-start log-end args)\n    (with-current-buffer buffer\n      (goto-char (point-min))\n      (if\n          (setq log-start (re-search-forward (concat \"^\" git-log-msg-separator \"\\n\") nil t))\n          (progn\n            (goto-char (point-min))\n            (when (re-search-forward \"^Author: +\\\\(.*?\\\\) *<\\\\(.*\\\\)> *$\" nil t)\n              (setq author-name (match-string 1)\n                    author-email (match-string 2)))\n            (goto-char (point-min))\n            (when (re-search-forward \"^Date: +\\\\(.*\\\\)$\" nil t)\n              (setq author-date (match-string 1))))\n        (setq log-start (point-min)))\n      (setq log-end (point-max)))\n    (dolist (p parents)\n      (push \"-p\" args)\n      (push p args))\n    (git-get-string-sha1\n     (with-output-to-string\n       (with-current-buffer standard-output\n         (let ((env `((\"GIT_AUTHOR_NAME\" . ,author-name)\n                      (\"GIT_AUTHOR_EMAIL\" . ,author-email)\n                      (\"GIT_COMMITTER_NAME\" . ,(git-get-committer-name))\n                      (\"GIT_COMMITTER_EMAIL\" . ,(git-get-committer-email)))))\n           (when author-date (push `(\"GIT_AUTHOR_DATE\" . ,author-date) env))\n           (apply #'git-run-command-region\n                  buffer log-start log-end env\n                  \"git-commit-tree\" tree (nreverse args))))))))\n\n(defun git-empty-db-p ()\n  \"Check if the git db is empty (no commit done yet).\"\n  (not (file-exists-p \".git/HEAD\")))\n\n\n;;;; File info structure\n;;;; ------------------------------------------------------------\n\n; fileinfo structure stolen from pcl-cvs\n(defstruct (git-fileinfo\n            (:copier nil)\n            (:constructor git-create-fileinfo (state name &optional old-perm new-perm rename-state orig-name marked))\n            (:conc-name git-fileinfo->))\n  marked              ;; t/nil\n  state               ;; current state\n  name                ;; file name\n  old-perm new-perm   ;; permission flags\n  rename-state        ;; rename or copy state\n  orig-name           ;; original name for renames or copies\n  needs-refresh)      ;; whether file needs to be refreshed\n\n(defvar git-status nil)\n\n(defun git-clear-status (status)\n  \"Remove everything from the status list.\"\n  (ewoc-filter status (lambda (info) nil)))\n\n(defun git-set-files-state (files state)\n  \"Set the state of a list of files.\"\n  (dolist (info files)\n    (unless (eq (git-fileinfo->state info) state)\n      (setf (git-fileinfo->state info) state)\n      (setf (git-fileinfo->rename-state info) nil)\n      (setf (git-fileinfo->orig-name info) nil)\n      (setf (git-fileinfo->needs-refresh info) t))))\n\n(defun git-state-code (code)\n  \"Convert from a string to a added/deleted/modified state.\"\n  (case (string-to-char code)\n    (?M 'modified)\n    (?? 'unknown)\n    (?A 'added)\n    (?D 'deleted)\n    (?U 'unmerged)\n    (t nil)))\n\n(defun git-status-code-as-string (code)\n  \"Format a git status code as string.\"\n  (case code\n    ('modified (propertize \"Modified\" 'face 'git-status-face))\n    ('unknown  (propertize \"Unknown \" 'face 'git-unknown-face))\n    ('added    (propertize \"Added   \" 'face 'git-status-face))\n    ('deleted  (propertize \"Deleted \" 'face 'git-status-face))\n    ('unmerged (propertize \"Unmerged\" 'face 'git-unmerged-face))\n    ('uptodate (propertize \"Uptodate\" 'face 'git-uptodate-face))\n    ('ignored  (propertize \"Ignored \" 'face 'git-ignored-face))\n    (t \"?       \")))\n\n(defun git-rename-as-string (info)\n  \"Return a string describing the copy or rename associated with INFO, or an empty string if none.\"\n  (let ((state (git-fileinfo->rename-state info)))\n    (if state\n        (propertize\n         (concat \"   (\"\n                 (if (eq state 'copy) \"copied from \"\n                   (if (eq (git-fileinfo->state info) 'added) \"renamed to \"\n                     \"renamed from \"))\n                 (git-escape-file-name (git-fileinfo->orig-name info))\n                 \")\") 'face 'git-status-face)\n      \"\")))\n\n(defun git-permissions-as-string (old-perm new-perm)\n  \"Format a permission change as string.\"\n  (propertize\n   (if (or (not old-perm)\n           (not new-perm)\n           (eq 0 (logand #O111 (logxor old-perm new-perm))))\n       \"  \"\n     (if (eq 0 (logand #O111 old-perm)) \"+x\" \"-x\"))\n  'face 'git-permission-face))\n\n(defun git-fileinfo-prettyprint (info)\n  \"Pretty-printer for the git-fileinfo structure.\"\n  (insert (format \"   %s %s %s  %s%s\"\n                  (if (git-fileinfo->marked info) (propertize \"*\" 'face 'git-mark-face) \" \")\n                  (git-status-code-as-string (git-fileinfo->state info))\n                  (git-permissions-as-string (git-fileinfo->old-perm info) (git-fileinfo->new-perm info))\n                  (git-escape-file-name (git-fileinfo->name info))\n                  (git-rename-as-string info))))\n\n(defun git-parse-status (status)\n  \"Parse the output of git-diff-index in the current buffer.\"\n  (goto-char (point-min))\n  (while (re-search-forward\n          \":\\\\([0-7]\\\\{6\\\\}\\\\) \\\\([0-7]\\\\{6\\\\}\\\\) [0-9a-f]\\\\{40\\\\} [0-9a-f]\\\\{40\\\\} \\\\(\\\\([ADMU]\\\\)\\0\\\\([^\\0]+\\\\)\\\\|\\\\([CR]\\\\)[0-9]*\\0\\\\([^\\0]+\\\\)\\0\\\\([^\\0]+\\\\)\\\\)\\0\"\n          nil t 1)\n    (let ((old-perm (string-to-number (match-string 1) 8))\n          (new-perm (string-to-number (match-string 2) 8))\n          (state (or (match-string 4) (match-string 6)))\n          (name (or (match-string 5) (match-string 7)))\n          (new-name (match-string 8)))\n      (if new-name  ; copy or rename\n          (if (eq ?C (string-to-char state))\n              (ewoc-enter-last status (git-create-fileinfo 'added new-name old-perm new-perm 'copy name))\n            (ewoc-enter-last status (git-create-fileinfo 'deleted name 0 0 'rename new-name))\n            (ewoc-enter-last status (git-create-fileinfo 'added new-name old-perm new-perm 'rename name)))\n        (ewoc-enter-last status (git-create-fileinfo (git-state-code state) name old-perm new-perm))))))\n\n(defun git-find-status-file (status file)\n  \"Find a given file in the status ewoc and return its node.\"\n  (let ((node (ewoc-nth status 0)))\n    (while (and node (not (string= file (git-fileinfo->name (ewoc-data node)))))\n      (setq node (ewoc-next status node)))\n    node))\n\n(defun git-parse-ls-files (status default-state &optional skip-existing)\n  \"Parse the output of git-ls-files in the current buffer.\"\n  (goto-char (point-min))\n  (while (re-search-forward \"\\\\([HMRCK?]\\\\) \\\\([^\\0]*\\\\)\\0\" nil t 1)\n    (let ((state (match-string 1))\n          (name (match-string 2))\n          infolist)\n      (unless (and skip-existing (git-find-status-file status name))\n        (push (git-create-fileinfo (or (git-state-code state) default-state) name) infolist))\n      (dolist (info infolist)\n          (ewoc-enter-last status info)))))\n\n(defun git-add-status-file (state name &optional old-perm new-perm)\n  \"Add a new file to the status list (if not existing already) and return its node.\"\n  (unless git-status (error \"Not in git-status buffer.\"))\n  (or (git-find-status-file git-status name)\n      (ewoc-enter-last git-status (git-create-fileinfo state name))))\n\n(defun git-marked-files ()\n  \"Return a list of all marked files, or if none a list containing just the file at cursor position.\"\n  (unless git-status (error \"Not in git-status buffer.\"))\n  (or (ewoc-collect git-status (lambda (info) (git-fileinfo->marked info)))\n      (list (ewoc-data (ewoc-locate git-status)))))\n\n(defun git-marked-files-state (&rest states)\n  \"Return marked files that are in the specified states.\"\n  (let ((files (git-marked-files))\n        result)\n    (dolist (info files)\n      (when (memq (git-fileinfo->state info) states)\n        (push info result)))\n    result))\n\n(defun git-refresh-files ()\n  \"Refresh all files that need it and clear the needs-refresh flag.\"\n  (unless git-status (error \"Not in git-status buffer.\"))\n  (ewoc-map\n   (lambda (info)\n     (let ((refresh (git-fileinfo->needs-refresh info)))\n       (setf (git-fileinfo->needs-refresh info) nil)\n       refresh))\n   git-status)\n  ; move back to goal column\n  (when goal-column (move-to-column goal-column)))\n\n(defun git-refresh-ewoc-hf (status)\n  \"Refresh the ewoc header and footer.\"\n  (ewoc-set-hf status\n               (format \"Directory:  %s\\nBranch:     %s\\nHead:       %s\\n\"\n                       default-directory\n                       (git-symbolic-ref \"HEAD\")\n                       (if (git-empty-db-p) \"Nothing committed yet\" (git-rev-parse \"HEAD\")))\n               (if (ewoc-nth status 0) \"\" \"    No changes.\")))\n\n(defun git-check-exclude-file ()\n  \"Check for non empty .git/info/exclude file like git-status does.\"\n  (when (file-readable-p \".git/info/exclude\")\n    (with-temp-buffer\n      (insert-file-contents \".git/info/exclude\" nil nil nil t)\n      (goto-char (point-min))\n      (re-search-forward \"^[^#]\" nil t))))\n\n(defun git-get-filenames (files)\n  (mapcar (lambda (info) (git-fileinfo->name info)) files))\n\n(defun git-update-index (index-file files)\n  \"Run git-update-index on a list of files.\"\n  (let ((env (and index-file `((\"GIT_INDEX_FILE\" . ,index-file))))\n        added deleted modified)\n    (dolist (info files)\n      (case (git-fileinfo->state info)\n        ('added (push info added))\n        ('deleted (push info deleted))\n        ('modified (push info modified))))\n    (when added\n      (apply #'git-run-command nil env \"git-update-index\" \"--add\" \"--\" (git-get-filenames added)))\n    (when deleted\n      (apply #'git-run-command nil env \"git-update-index\" \"--remove\" \"--\" (git-get-filenames deleted)))\n    (when modified\n      (apply #'git-run-command nil env \"git-update-index\" \"--\" (git-get-filenames modified)))))\n\n(defun git-do-commit ()\n  \"Perform the actual commit using the current buffer as log message.\"\n  (interactive)\n  (let ((buffer (current-buffer))\n        (index-file (make-temp-file \"gitidx\")))\n    (with-current-buffer log-edit-parent-buffer\n      (unwind-protect\n          (let ((files (git-marked-files-state 'added 'deleted 'modified))\n                head head-tree)\n            (unless (git-empty-db-p)\n              (setq head (git-rev-parse \"HEAD\")\n                    head-tree (git-rev-parse \"HEAD^{tree}\")))\n            (if files\n                (progn\n                  (git-read-tree head-tree index-file)\n                  (git-update-index nil files)         ;update both the default index\n                  (git-update-index index-file files)  ;and the temporary one\n                  (let ((tree (git-write-tree index-file)))\n                    (if (or (not (string-equal tree head-tree))\n                            (yes-or-no-p \"The tree was not modified, do you really want to perform an empty commit? \"))\n                        (let ((commit (git-commit-tree buffer tree (if head (list head)))))\n                          (git-update-ref \"HEAD\" commit head)\n                          (git-set-files-state files 'uptodate)\n                          (git-refresh-files)\n                          (git-refresh-ewoc-hf git-status)\n                          (message \"Committed %s.\" commit))\n                      (message \"Commit aborted.\"))))\n              (message \"No files to commit.\")))\n        (delete-file index-file)))))\n\n\n;;;; Interactive functions\n;;;; ------------------------------------------------------------\n\n(defun git-mark-file ()\n  \"Mark the file that the cursor is on and move to the next one.\"\n  (interactive)\n  (unless git-status (error \"Not in git-status buffer.\"))\n  (let* ((pos (ewoc-locate git-status))\n         (info (ewoc-data pos)))\n    (setf (git-fileinfo->marked info) t)\n    (ewoc-invalidate git-status pos)\n    (ewoc-goto-next git-status 1)))\n\n(defun git-unmark-file ()\n  \"Unmark the file that the cursor is on and move to the next one.\"\n  (interactive)\n  (unless git-status (error \"Not in git-status buffer.\"))\n  (let* ((pos (ewoc-locate git-status))\n         (info (ewoc-data pos)))\n    (setf (git-fileinfo->marked info) nil)\n    (ewoc-invalidate git-status pos)\n    (ewoc-goto-next git-status 1)))\n\n(defun git-unmark-file-up ()\n  \"Unmark the file that the cursor is on and move to the previous one.\"\n  (interactive)\n  (unless git-status (error \"Not in git-status buffer.\"))\n  (let* ((pos (ewoc-locate git-status))\n         (info (ewoc-data pos)))\n    (setf (git-fileinfo->marked info) nil)\n    (ewoc-invalidate git-status pos)\n    (ewoc-goto-prev git-status 1)))\n\n(defun git-mark-all ()\n  \"Mark all files.\"\n  (interactive)\n  (unless git-status (error \"Not in git-status buffer.\"))\n  (ewoc-map (lambda (info) (setf (git-fileinfo->marked info) t) t) git-status)\n  ; move back to goal column after invalidate\n  (when goal-column (move-to-column goal-column)))\n\n(defun git-unmark-all ()\n  \"Unmark all files.\"\n  (interactive)\n  (unless git-status (error \"Not in git-status buffer.\"))\n  (ewoc-map (lambda (info) (setf (git-fileinfo->marked info) nil) t) git-status)\n  ; move back to goal column after invalidate\n  (when goal-column (move-to-column goal-column)))\n\n(defun git-toggle-all-marks ()\n  \"Toggle all file marks.\"\n  (interactive)\n  (unless git-status (error \"Not in git-status buffer.\"))\n  (ewoc-map (lambda (info) (setf (git-fileinfo->marked info) (not (git-fileinfo->marked info))) t) git-status)\n  ; move back to goal column after invalidate\n  (when goal-column (move-to-column goal-column)))\n\n(defun git-next-file (&optional n)\n  \"Move the selection down N files.\"\n  (interactive \"p\")\n  (unless git-status (error \"Not in git-status buffer.\"))\n  (ewoc-goto-next git-status n))\n\n(defun git-prev-file (&optional n)\n  \"Move the selection up N files.\"\n  (interactive \"p\")\n  (unless git-status (error \"Not in git-status buffer.\"))\n  (ewoc-goto-prev git-status n))\n\n(defun git-add-file ()\n  \"Add marked file(s) to the index cache.\"\n  (interactive)\n  (let ((files (git-marked-files-state 'unknown)))\n    (unless files\n      (push (ewoc-data\n             (git-add-status-file 'added (file-relative-name\n                                          (read-file-name \"File to add: \" nil nil t))))\n            files))\n    (apply #'git-run-command nil nil \"git-update-index\" \"--info-only\" \"--add\" \"--\" (git-get-filenames files))\n    (git-set-files-state files 'added)\n    (git-refresh-files)))\n\n(defun git-ignore-file ()\n  \"Add marked file(s) to the ignore list.\"\n  (interactive)\n  (let ((files (git-marked-files-state 'unknown)))\n    (unless files\n      (push (ewoc-data\n             (git-add-status-file 'unknown (file-relative-name\n                                            (read-file-name \"File to ignore: \" nil nil t))))\n            files))\n    (dolist (info files) (git-append-to-ignore (git-fileinfo->name info)))\n    (git-set-files-state files 'ignored)\n    (git-refresh-files)))\n\n(defun git-remove-file ()\n  \"Remove the marked file(s).\"\n  (interactive)\n  (let ((files (git-marked-files-state 'added 'modified 'unknown 'uptodate)))\n    (unless files\n      (push (ewoc-data\n             (git-add-status-file 'unknown (file-relative-name\n                                            (read-file-name \"File to remove: \" nil nil t))))\n            files))\n    (if (yes-or-no-p\n         (format \"Remove %d file%s? \" (length files) (if (> (length files) 1) \"s\" \"\")))\n        (progn\n          (dolist (info files)\n            (let ((name (git-fileinfo->name info)))\n              (when (file-exists-p name) (delete-file name))))\n          (apply #'git-run-command nil nil \"git-update-index\" \"--info-only\" \"--remove\" \"--\" (git-get-filenames files))\n          ; remove unknown files from the list, set the others to deleted\n          (ewoc-filter git-status\n                       (lambda (info files)\n                         (not (and (memq info files) (eq (git-fileinfo->state info) 'unknown))))\n                       files)\n          (git-set-files-state files 'deleted)\n          (git-refresh-files))\n      (message \"Aborting\"))))\n\n(defun git-revert-file ()\n  \"Revert changes to the marked file(s).\"\n  (interactive)\n  (let ((files (git-marked-files))\n        added deleted modified)\n    (when (and files\n               (yes-or-no-p\n                (format \"Revert %d file%s? \" (length files) (if (> (length files) 1) \"s\" \"\"))))\n      (dolist (info files)\n        (case (git-fileinfo->state info)\n          ('added (push info added))\n          ('deleted (push info deleted))\n          ('modified (push info modified))))\n          ; FIXME: unmerged state\n      (when added\n          (apply #'git-run-command nil nil \"git-update-index\" \"--force-remove\" \"--\" (git-get-filenames added))\n          (git-set-files-state added 'unknown))\n      (when modified\n          (apply #'git-run-command nil nil \"git-checkout-index\" \"-f\" \"-u\" \"--\" (git-get-filenames modified))\n          (git-set-files-state modified 'uptodate))\n      (when deleted\n        (error \"revert for deleted files not implemented yet\"))\n      (git-refresh-files))))\n\n(defun git-remove-handled ()\n  \"Remove handled files from the status list.\"\n  (interactive)\n  (ewoc-filter git-status\n               (lambda (info)\n                 (not (or (eq (git-fileinfo->state info) 'ignored)\n                          (eq (git-fileinfo->state info) 'uptodate))))))\n\n(defun git-diff-file ()\n  \"Diff the marked file(s) against HEAD.\"\n  (interactive)\n  (let* ((files (git-marked-files))\n         (buffer (apply #'git-run-command-buffer \"*git-diff*\" \"git-diff-index\" \"-p\" \"-M\" \"HEAD\" \"--\" (git-get-filenames files))))\n    (with-current-buffer buffer\n      (diff-mode)\n      (goto-char (point-min))\n      (setq buffer-read-only t))\n    (display-buffer buffer)))\n\n(defun git-log-file ()\n  \"Display a log of changes to the marked file(s).\"\n  (interactive)\n  (let* ((files (git-marked-files))\n         (buffer (apply #'git-run-command-buffer \"*git-log*\" \"git-rev-list\" \"--pretty\" \"HEAD\" \"--\" (git-get-filenames files))))\n    (with-current-buffer buffer\n      ; (git-log-mode)  FIXME: implement log mode\n      (goto-char (point-min))\n      (setq buffer-read-only t))\n    (display-buffer buffer)))\n\n(defun git-commit-file (arg)\n  \"Commit the marked file(s), asking for a commit message. With prefix arg, add Author and Date fields for easy editing.\"\n  (interactive \"P\")\n  (let ((buffer (get-buffer-create \"*git-commit*\")))\n    (with-current-buffer buffer\n      (when arg\n        ; FIXME: should get these from the merge info if any\n        (erase-buffer)\n        (insert\n         (propertize\n          (format \"Author: %s <%s>\\nDate: %s\"\n                  (git-get-committer-name)\n                  (git-get-committer-email)\n                  (current-time-string))\n          'face 'git-header-face)\n         \"\\n\"\n         (propertize git-log-msg-separator 'face 'git-separator-face)\n         \"\\n\")))\n    (log-edit #'git-do-commit nil nil buffer)))\n\n(defun git-find-file ()\n  \"Visit the current file in its own buffer.\"\n  (interactive)\n  (unless git-status (error \"Not in git-status buffer.\"))\n  (let ((info (ewoc-data (ewoc-locate git-status))))\n    (find-file (git-fileinfo->name info))))\n\n(defun git-view-file ()\n  \"View the current file in its own buffer.\"\n  (interactive)\n  (unless git-status (error \"Not in git-status buffer.\"))\n  (let ((info (ewoc-data (ewoc-locate git-status))))\n    (view-file (git-fileinfo->name info))))\n\n(defun git-refresh-status ()\n  \"Refresh the git status buffer.\"\n  (interactive)\n  (let* ((status git-status)\n         (pos (ewoc-locate status))\n         (cur-name (and pos (git-fileinfo->name (ewoc-data pos)))))\n    (unless status (error \"Not in git-status buffer.\"))\n    (git-clear-status status)\n    (git-run-command nil nil \"git-update-index\" \"--info-only\" \"--refresh\")\n    (if (git-empty-db-p)\n        ; we need some special handling for an empty db\n        (with-temp-buffer\n          (git-run-command t nil \"git-ls-files\" \"-z\" \"-t\" \"-c\")\n          (git-parse-ls-files status 'added))\n      (with-temp-buffer\n        (git-run-command t nil \"git-diff-index\" \"-z\" \"-M\" \"HEAD\")\n        (git-parse-status status)))\n    (when (git-check-exclude-file)\n      (with-temp-buffer\n        (git-run-command t nil \"git-ls-files\" \"-z\" \"-t\" \"-o\"\n                         \"--exclude-from=.git/info/exclude\"\n                         (concat \"--exclude-per-directory=\" git-per-dir-ignore-file))\n        (git-parse-ls-files status 'unknown)))\n    (git-refresh-files)\n    (git-refresh-ewoc-hf status)\n    ; move point to the current file name if any\n    (let ((node (and cur-name (git-find-status-file status cur-name))))\n      (when node (ewoc-goto-node status node)))))\n\n(defun git-status-quit ()\n  \"Quit git-status mode.\"\n  (interactive)\n  (bury-buffer))\n\n;;;; Major Mode\n;;;; ------------------------------------------------------------\n\n(defvar git-status-mode-hook nil\n  \"Run after `git-status-mode' is setup.\")\n\n(defvar git-status-mode-map nil\n  \"Keymap for git major mode.\")\n\n(defvar git-status nil\n  \"List of all files managed by the git-status mode.\")\n\n(unless git-status-mode-map\n  (let ((map (make-keymap)))\n    (suppress-keymap map)\n    (define-key map \" \"   'git-next-file)\n    (define-key map \"a\"   'git-add-file)\n    (define-key map \"c\"   'git-commit-file)\n    (define-key map \"d\"   'git-diff-file)\n    (define-key map \"=\"   'git-diff-file)\n    (define-key map \"f\"   'git-find-file)\n    (define-key map [RET] 'git-find-file)\n    (define-key map \"g\"   'git-refresh-status)\n    (define-key map \"i\"   'git-ignore-file)\n    (define-key map \"l\"   'git-log-file)\n    (define-key map \"m\"   'git-mark-file)\n    (define-key map \"M\"   'git-mark-all)\n    (define-key map \"n\"   'git-next-file)\n    (define-key map \"p\"   'git-prev-file)\n    (define-key map \"q\"   'git-status-quit)\n    (define-key map \"r\"   'git-remove-file)\n    (define-key map \"T\"   'git-toggle-all-marks)\n    (define-key map \"u\"   'git-unmark-file)\n    (define-key map \"U\"   'git-revert-file)\n    (define-key map \"v\"   'git-view-file)\n    (define-key map \"x\"   'git-remove-handled)\n    (define-key map \"\\C-?\" 'git-unmark-file-up)\n    (define-key map \"\\M-\\C-?\" 'git-unmark-all)\n    (setq git-status-mode-map map)))\n\n;; git mode should only run in the *git status* buffer\n(put 'git-status-mode 'mode-class 'special)\n\n(defun git-status-mode ()\n  \"Major mode for interacting with Git.\nCommands:\n\\\\{git-status-mode-map}\"\n  (kill-all-local-variables)\n  (buffer-disable-undo)\n  (setq mode-name \"git status\"\n        major-mode 'git-status-mode\n        goal-column 17\n        buffer-read-only t)\n  (use-local-map git-status-mode-map)\n  (let ((buffer-read-only nil))\n    (erase-buffer)\n  (let ((status (ewoc-create 'git-fileinfo-prettyprint \"\" \"\")))\n    (set (make-local-variable 'git-status) status))\n  (run-hooks 'git-status-mode-hook)))\n\n(defun git-status (dir)\n  \"Entry point into git-status mode.\"\n  (interactive \"DSelect directory: \")\n  (if (file-directory-p (concat (file-name-as-directory dir) \".git\"))\n      (let ((buffer (create-file-buffer (expand-file-name \"*git-status*\" dir))))\n        (switch-to-buffer buffer)\n        (git-status-mode)\n        (cd dir)\n        (git-refresh-status)\n        (goto-char (point-min)))\n    (message \"%s is not a git working tree.\" dir)))\n\n(provide 'git)\n;;; git.el ends here\n"},{"id":"12823","messageId":"7v7jatael2.fsf@assigned-by-dhcp.cox.net","threadId":"2693","inReplyTo":"87u0dx7mov.fsf@wine.dyndns.org","subject":"Re: Emacs mode for git","fromName":"Junio C Hamano","fromEmail":"junkio@cox.net","sentAt":"2005-11-27T22:52:25Z","receivedAt":"2005-11-27T22:52:25Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Alexandre Julliard <julliard@winehq.org> writes:\n\n> Here's the beginning of an Emacs mode for git. My primary goal at this\n> point is to replicate the behavior of the pcl-cvs mode.\n\nSounds nice -- I might switch to this myself if it is easy to\nuse (currently my fingers are trained to run git from\ncompilation mode buffer, so I need to switch anyway).\n\nI regularly use the vc interface for working with CVS and never\nhave used PCL-CVS myself.\n"}]}