Merge branch 'jc/clone' into next
[git.git] / contrib / emacs / git.el
1 ;;; git.el --- A user interface for git
2
3 ;; Copyright (C) 2005, 2006 Alexandre Julliard <julliard@winehq.org>
4
5 ;; Version: 1.0
6
7 ;; This program is free software; you can redistribute it and/or
8 ;; modify it under the terms of the GNU General Public License as
9 ;; published by the Free Software Foundation; either version 2 of
10 ;; the License, or (at your option) any later version.
11 ;;
12 ;; This program is distributed in the hope that it will be
13 ;; useful, but WITHOUT ANY WARRANTY; without even the implied
14 ;; warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
15 ;; PURPOSE.  See the GNU General Public License for more details.
16 ;;
17 ;; You should have received a copy of the GNU General Public
18 ;; License along with this program; if not, write to the Free
19 ;; Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
20 ;; MA 02111-1307 USA
21
22 ;;; Commentary:
23
24 ;; This file contains an interface for the git version control
25 ;; system. It provides easy access to the most frequently used git
26 ;; commands. The user interface is as far as possible identical to
27 ;; that of the PCL-CVS mode.
28 ;;
29 ;; To install: put this file on the load-path and place the following
30 ;; in your .emacs file:
31 ;;
32 ;;    (require 'git)
33 ;;
34 ;; To start: `M-x git-status'
35 ;;
36 ;; TODO
37 ;;  - portability to XEmacs
38 ;;  - better handling of subprocess errors
39 ;;  - hook into file save (after-save-hook)
40 ;;  - diff against other branch
41 ;;  - renaming files from the status buffer
42 ;;  - creating tags
43 ;;  - fetch/pull
44 ;;  - switching branches
45 ;;  - revlist browser
46 ;;  - git-show-branch browser
47 ;;  - menus
48 ;;
49
50 (eval-when-compile (require 'cl))
51 (require 'ewoc)
52
53
54 ;;;; Customizations
55 ;;;; ------------------------------------------------------------
56
57 (defgroup git nil
58   "Git user interface")
59
60 (defcustom git-committer-name nil
61   "User name to use for commits.
62 The default is to fall back to `add-log-full-name' and then `user-full-name'."
63   :group 'git
64   :type '(choice (const :tag "Default" nil)
65                  (string :tag "Name")))
66
67 (defcustom git-committer-email nil
68   "Email address to use for commits.
69 The default is to fall back to `add-log-mailing-address' and then `user-mail-address'."
70   :group 'git
71   :type '(choice (const :tag "Default" nil)
72                  (string :tag "Email")))
73
74 (defcustom git-commits-coding-system 'utf-8
75   "Default coding system for the log message of git commits."
76   :group 'git
77   :type 'coding-system)
78
79 (defcustom git-append-signed-off-by nil
80   "Whether to append a Signed-off-by line to the commit message before editing."
81   :group 'git
82   :type 'boolean)
83
84 (defcustom git-per-dir-ignore-file ".gitignore"
85   "Name of the per-directory ignore file."
86   :group 'git
87   :type 'string)
88
89 (defface git-status-face
90   '((((class color) (background light)) (:foreground "purple")))
91   "Git mode face used to highlight added and modified files."
92   :group 'git)
93
94 (defface git-unmerged-face
95   '((((class color) (background light)) (:foreground "red" :bold t)))
96   "Git mode face used to highlight unmerged files."
97   :group 'git)
98
99 (defface git-unknown-face
100   '((((class color) (background light)) (:foreground "goldenrod" :bold t)))
101   "Git mode face used to highlight unknown files."
102   :group 'git)
103
104 (defface git-uptodate-face
105   '((((class color) (background light)) (:foreground "grey60")))
106   "Git mode face used to highlight up-to-date files."
107   :group 'git)
108
109 (defface git-ignored-face
110   '((((class color) (background light)) (:foreground "grey60")))
111   "Git mode face used to highlight ignored files."
112   :group 'git)
113
114 (defface git-mark-face
115   '((((class color) (background light)) (:foreground "red" :bold t)))
116   "Git mode face used for the file marks."
117   :group 'git)
118
119 (defface git-header-face
120   '((((class color) (background light)) (:foreground "blue")))
121   "Git mode face used for commit headers."
122   :group 'git)
123
124 (defface git-separator-face
125   '((((class color) (background light)) (:foreground "brown")))
126   "Git mode face used for commit separator."
127   :group 'git)
128
129 (defface git-permission-face
130   '((((class color) (background light)) (:foreground "green" :bold t)))
131   "Git mode face used for permission changes."
132   :group 'git)
133
134
135 ;;;; Utilities
136 ;;;; ------------------------------------------------------------
137
138 (defconst git-log-msg-separator "--- log message follows this line ---")
139
140 (defun git-get-env-strings (env)
141   "Build a list of NAME=VALUE strings from a list of environment strings."
142   (mapcar (lambda (entry) (concat (car entry) "=" (cdr entry))) env))
143
144 (defun git-call-process-env (buffer env &rest args)
145   "Wrapper for call-process that sets environment strings."
146   (if env
147       (apply #'call-process "env" nil buffer nil
148              (append (git-get-env-strings env) (list "git") args))
149     (apply #'call-process "git" nil buffer nil args)))
150
151 (defun git-run-process-region (buffer start end program args)
152   "Run a git process with a buffer region as input."
153   (let ((output-buffer (current-buffer))
154         (dir default-directory))
155     (with-current-buffer buffer
156       (cd dir)
157       (apply #'call-process-region start end program
158              nil (list output-buffer nil) nil args))))
159
160 (defun git-run-command-buffer (buffer-name &rest args)
161   "Run a git command, sending the output to a buffer named BUFFER-NAME."
162   (let ((dir default-directory)
163         (buffer (get-buffer-create buffer-name)))
164     (message "Running git %s..." (car args))
165     (with-current-buffer buffer
166       (let ((default-directory dir)
167             (buffer-read-only nil))
168         (erase-buffer)
169         (apply #'git-call-process-env buffer nil args)))
170     (message "Running git %s...done" (car args))
171     buffer))
172
173 (defun git-run-command (buffer env &rest args)
174   (message "Running git %s..." (car args))
175   (apply #'git-call-process-env buffer env args)
176   (message "Running git %s...done" (car args)))
177
178 (defun git-run-command-region (buffer start end env &rest args)
179   "Run a git command with specified buffer region as input."
180   (message "Running git %s..." (car args))
181   (unless (eq 0 (if env
182                     (git-run-process-region
183                      buffer start end "env"
184                      (append (git-get-env-strings env) (list "git") args))
185                   (git-run-process-region
186                    buffer start end "git" args)))
187     (error "Failed to run \"git %s\":\n%s" (mapconcat (lambda (x) x) args " ") (buffer-string)))
188   (message "Running git %s...done" (car args)))
189
190 (defun git-get-string-sha1 (string)
191   "Read a SHA1 from the specified string."
192   (let ((pos (string-match "[0-9a-f]\\{40\\}" string)))
193     (and pos (substring string pos (match-end 0)))))
194
195 (defun git-get-committer-name ()
196   "Return the name to use as GIT_COMMITTER_NAME."
197   ; copied from log-edit
198   (or git-committer-name
199       (and (boundp 'add-log-full-name) add-log-full-name)
200       (and (fboundp 'user-full-name) (user-full-name))
201       (and (boundp 'user-full-name) user-full-name)))
202
203 (defun git-get-committer-email ()
204   "Return the email address to use as GIT_COMMITTER_EMAIL."
205   ; copied from log-edit
206   (or git-committer-email
207       (and (boundp 'add-log-mailing-address) add-log-mailing-address)
208       (and (fboundp 'user-mail-address) (user-mail-address))
209       (and (boundp 'user-mail-address) user-mail-address)))
210
211 (defun git-escape-file-name (name)
212   "Escape a file name if necessary."
213   (if (string-match "[\n\t\"\\]" name)
214       (concat "\""
215               (mapconcat (lambda (c)
216                    (case c
217                      (?\n "\\n")
218                      (?\t "\\t")
219                      (?\\ "\\\\")
220                      (?\" "\\\"")
221                      (t (char-to-string c))))
222                  name "")
223               "\"")
224     name))
225
226 (defun git-get-top-dir (dir)
227   "Retrieve the top-level directory of a git tree."
228   (let ((cdup (with-output-to-string
229                 (with-current-buffer standard-output
230                   (cd dir)
231                   (unless (eq 0 (call-process "git" nil t nil "rev-parse" "--show-cdup"))
232                     (error "cannot find top-level git tree for %s." dir))))))
233     (expand-file-name (concat (file-name-as-directory dir)
234                               (car (split-string cdup "\n"))))))
235
236 ;stolen from pcl-cvs
237 (defun git-append-to-ignore (file)
238   "Add a file name to the ignore file in its directory."
239   (let* ((fullname (expand-file-name file))
240          (dir (file-name-directory fullname))
241          (name (file-name-nondirectory fullname))
242          (ignore-name (expand-file-name git-per-dir-ignore-file dir))
243          (created (not (file-exists-p ignore-name))))
244   (save-window-excursion
245     (set-buffer (find-file-noselect ignore-name))
246     (goto-char (point-max))
247     (unless (zerop (current-column)) (insert "\n"))
248     (insert name "\n")
249     (sort-lines nil (point-min) (point-max))
250     (save-buffer))
251   (when created
252     (git-run-command nil nil "update-index" "--info-only" "--add" "--" (file-relative-name ignore-name)))
253   (git-add-status-file (if created 'added 'modified) (file-relative-name ignore-name))))
254
255
256 ;;;; Wrappers for basic git commands
257 ;;;; ------------------------------------------------------------
258
259 (defun git-rev-parse (rev)
260   "Parse a revision name and return its SHA1."
261   (git-get-string-sha1
262    (with-output-to-string
263      (with-current-buffer standard-output
264        (git-call-process-env t nil "rev-parse" rev)))))
265
266 (defun git-symbolic-ref (ref)
267   "Wrapper for the git-symbolic-ref command."
268   (car
269    (split-string
270     (with-output-to-string
271       (with-current-buffer standard-output
272         (git-call-process-env t nil "symbolic-ref" ref)))
273     "\n")))
274
275 (defun git-update-ref (ref val &optional oldval)
276   "Update a reference by calling git-update-ref."
277   (apply #'git-call-process-env nil nil "update-ref" ref val (if oldval (list oldval))))
278
279 (defun git-read-tree (tree &optional index-file)
280   "Read a tree into the index file."
281   (apply #'git-call-process-env nil
282          (if index-file `(("GIT_INDEX_FILE" . ,index-file)) nil)
283          "read-tree" (if tree (list tree))))
284
285 (defun git-write-tree (&optional index-file)
286   "Call git-write-tree and return the resulting tree SHA1 as a string."
287   (git-get-string-sha1
288    (with-output-to-string
289      (with-current-buffer standard-output
290        (git-call-process-env t
291         (if index-file `(("GIT_INDEX_FILE" . ,index-file)) nil)
292         "write-tree")))))
293
294 (defun git-commit-tree (buffer tree head)
295   "Call git-commit-tree with buffer as input and return the resulting commit SHA1."
296   (let ((author-name (git-get-committer-name))
297         (author-email (git-get-committer-email))
298         author-date log-start log-end args)
299     (when head
300       (push "-p" args)
301       (push head args))
302     (with-current-buffer buffer
303       (goto-char (point-min))
304       (if
305           (setq log-start (re-search-forward (concat "^" (regexp-quote git-log-msg-separator) "\n") nil t))
306           (save-restriction
307             (narrow-to-region (point-min) log-start)
308             (goto-char (point-min))
309             (when (re-search-forward "^Author: +\\(.*?\\) *<\\(.*\\)> *$" nil t)
310               (setq author-name (match-string 1)
311                     author-email (match-string 2)))
312             (goto-char (point-min))
313             (when (re-search-forward "^Date: +\\(.*\\)$" nil t)
314               (setq author-date (match-string 1)))
315             (goto-char (point-min))
316             (while (re-search-forward "^Parent: +\\([0-9a-f]+\\)" nil t)
317               (unless (string-equal head (match-string 1))
318                 (push "-p" args)
319                 (push (match-string 1) args))))
320         (setq log-start (point-min)))
321       (setq log-end (point-max)))
322     (git-get-string-sha1
323      (with-output-to-string
324        (with-current-buffer standard-output
325          (let ((coding-system-for-write git-commits-coding-system)
326                (env `(("GIT_AUTHOR_NAME" . ,author-name)
327                       ("GIT_AUTHOR_EMAIL" . ,author-email)
328                       ("GIT_COMMITTER_NAME" . ,(git-get-committer-name))
329                       ("GIT_COMMITTER_EMAIL" . ,(git-get-committer-email)))))
330            (when author-date (push `("GIT_AUTHOR_DATE" . ,author-date) env))
331            (apply #'git-run-command-region
332                   buffer log-start log-end env
333                   "commit-tree" tree (nreverse args))))))))
334
335 (defun git-empty-db-p ()
336   "Check if the git db is empty (no commit done yet)."
337   (not (eq 0 (call-process "git" nil nil nil "rev-parse" "--verify" "HEAD"))))
338
339 (defun git-get-merge-heads ()
340   "Retrieve the merge heads from the MERGE_HEAD file if present."
341   (let (heads)
342     (when (file-readable-p ".git/MERGE_HEAD")
343       (with-temp-buffer
344         (insert-file-contents ".git/MERGE_HEAD" nil nil nil t)
345         (goto-char (point-min))
346         (while (re-search-forward "[0-9a-f]\\{40\\}" nil t)
347           (push (match-string 0) heads))))
348     (nreverse heads)))
349
350 ;;;; File info structure
351 ;;;; ------------------------------------------------------------
352
353 ; fileinfo structure stolen from pcl-cvs
354 (defstruct (git-fileinfo
355             (:copier nil)
356             (:constructor git-create-fileinfo (state name &optional old-perm new-perm rename-state orig-name marked))
357             (:conc-name git-fileinfo->))
358   marked              ;; t/nil
359   state               ;; current state
360   name                ;; file name
361   old-perm new-perm   ;; permission flags
362   rename-state        ;; rename or copy state
363   orig-name           ;; original name for renames or copies
364   needs-refresh)      ;; whether file needs to be refreshed
365
366 (defvar git-status nil)
367
368 (defun git-clear-status (status)
369   "Remove everything from the status list."
370   (ewoc-filter status (lambda (info) nil)))
371
372 (defun git-set-files-state (files state)
373   "Set the state of a list of files."
374   (dolist (info files)
375     (unless (eq (git-fileinfo->state info) state)
376       (setf (git-fileinfo->state info) state)
377       (setf (git-fileinfo->rename-state info) nil)
378       (setf (git-fileinfo->orig-name info) nil)
379       (setf (git-fileinfo->needs-refresh info) t))))
380
381 (defun git-state-code (code)
382   "Convert from a string to a added/deleted/modified state."
383   (case (string-to-char code)
384     (?M 'modified)
385     (?? 'unknown)
386     (?A 'added)
387     (?D 'deleted)
388     (?U 'unmerged)
389     (t nil)))
390
391 (defun git-status-code-as-string (code)
392   "Format a git status code as string."
393   (case code
394     ('modified (propertize "Modified" 'face 'git-status-face))
395     ('unknown  (propertize "Unknown " 'face 'git-unknown-face))
396     ('added    (propertize "Added   " 'face 'git-status-face))
397     ('deleted  (propertize "Deleted " 'face 'git-status-face))
398     ('unmerged (propertize "Unmerged" 'face 'git-unmerged-face))
399     ('uptodate (propertize "Uptodate" 'face 'git-uptodate-face))
400     ('ignored  (propertize "Ignored " 'face 'git-ignored-face))
401     (t "?       ")))
402
403 (defun git-rename-as-string (info)
404   "Return a string describing the copy or rename associated with INFO, or an empty string if none."
405   (let ((state (git-fileinfo->rename-state info)))
406     (if state
407         (propertize
408          (concat "   ("
409                  (if (eq state 'copy) "copied from "
410                    (if (eq (git-fileinfo->state info) 'added) "renamed to "
411                      "renamed from "))
412                  (git-escape-file-name (git-fileinfo->orig-name info))
413                  ")") 'face 'git-status-face)
414       "")))
415
416 (defun git-permissions-as-string (old-perm new-perm)
417   "Format a permission change as string."
418   (propertize
419    (if (or (not old-perm)
420            (not new-perm)
421            (eq 0 (logand ?\111 (logxor old-perm new-perm))))
422        "  "
423      (if (eq 0 (logand ?\111 old-perm)) "+x" "-x"))
424   'face 'git-permission-face))
425
426 (defun git-fileinfo-prettyprint (info)
427   "Pretty-printer for the git-fileinfo structure."
428   (insert (format "   %s %s %s  %s%s"
429                   (if (git-fileinfo->marked info) (propertize "*" 'face 'git-mark-face) " ")
430                   (git-status-code-as-string (git-fileinfo->state info))
431                   (git-permissions-as-string (git-fileinfo->old-perm info) (git-fileinfo->new-perm info))
432                   (git-escape-file-name (git-fileinfo->name info))
433                   (git-rename-as-string info))))
434
435 (defun git-parse-status (status)
436   "Parse the output of git-diff-index in the current buffer."
437   (goto-char (point-min))
438   (while (re-search-forward
439           ":\\([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"
440           nil t 1)
441     (let ((old-perm (string-to-number (match-string 1) 8))
442           (new-perm (string-to-number (match-string 2) 8))
443           (state (or (match-string 4) (match-string 6)))
444           (name (or (match-string 5) (match-string 7)))
445           (new-name (match-string 8)))
446       (if new-name  ; copy or rename
447           (if (eq ?C (string-to-char state))
448               (ewoc-enter-last status (git-create-fileinfo 'added new-name old-perm new-perm 'copy name))
449             (ewoc-enter-last status (git-create-fileinfo 'deleted name 0 0 'rename new-name))
450             (ewoc-enter-last status (git-create-fileinfo 'added new-name old-perm new-perm 'rename name)))
451         (ewoc-enter-last status (git-create-fileinfo (git-state-code state) name old-perm new-perm))))))
452
453 (defun git-find-status-file (status file)
454   "Find a given file in the status ewoc and return its node."
455   (let ((node (ewoc-nth status 0)))
456     (while (and node (not (string= file (git-fileinfo->name (ewoc-data node)))))
457       (setq node (ewoc-next status node)))
458     node))
459
460 (defun git-parse-ls-files (status default-state &optional skip-existing)
461   "Parse the output of git-ls-files in the current buffer."
462   (goto-char (point-min))
463   (let (infolist)
464     (while (re-search-forward "\\([HMRCK?]\\) \\([^\0]*\\)\0" nil t 1)
465       (let ((state (match-string 1))
466             (name (match-string 2)))
467         (unless (and skip-existing (git-find-status-file status name))
468           (push (git-create-fileinfo (or (git-state-code state) default-state) name) infolist))))
469     (dolist (info (nreverse infolist))
470       (ewoc-enter-last status info))))
471
472 (defun git-parse-ls-unmerged (status)
473   "Parse the output of git-ls-files -u in the current buffer."
474   (goto-char (point-min))
475   (let (files)
476     (while (re-search-forward "[0-7]\\{6\\} [0-9a-f]\\{40\\} [123]\t\\([^\0]+\\)\0" nil t)
477       (let ((node (git-find-status-file status (match-string 1))))
478         (when node (push (ewoc-data node) files))))
479     (git-set-files-state files 'unmerged)))
480
481 (defun git-add-status-file (state name)
482   "Add a new file to the status list (if not existing already) and return its node."
483   (unless git-status (error "Not in git-status buffer."))
484   (or (git-find-status-file git-status name)
485       (ewoc-enter-last git-status (git-create-fileinfo state name))))
486
487 (defun git-marked-files ()
488   "Return a list of all marked files, or if none a list containing just the file at cursor position."
489   (unless git-status (error "Not in git-status buffer."))
490   (or (ewoc-collect git-status (lambda (info) (git-fileinfo->marked info)))
491       (list (ewoc-data (ewoc-locate git-status)))))
492
493 (defun git-marked-files-state (&rest states)
494   "Return marked files that are in the specified states."
495   (let ((files (git-marked-files))
496         result)
497     (dolist (info files)
498       (when (memq (git-fileinfo->state info) states)
499         (push info result)))
500     result))
501
502 (defun git-refresh-files ()
503   "Refresh all files that need it and clear the needs-refresh flag."
504   (unless git-status (error "Not in git-status buffer."))
505   (ewoc-map
506    (lambda (info)
507      (let ((refresh (git-fileinfo->needs-refresh info)))
508        (setf (git-fileinfo->needs-refresh info) nil)
509        refresh))
510    git-status)
511   ; move back to goal column
512   (when goal-column (move-to-column goal-column)))
513
514 (defun git-refresh-ewoc-hf (status)
515   "Refresh the ewoc header and footer."
516   (let ((branch (git-symbolic-ref "HEAD"))
517         (head (if (git-empty-db-p) "Nothing committed yet"
518                 (substring (git-rev-parse "HEAD") 0 10)))
519         (merge-heads (git-get-merge-heads)))
520     (ewoc-set-hf status
521                  (format "Directory:  %s\nBranch:     %s\nHead:       %s%s\n"
522                          default-directory
523                          (if (string-match "^refs/heads/" branch)
524                              (substring branch (match-end 0))
525                            branch)
526                          head
527                          (if merge-heads
528                              (concat "\nMerging:    "
529                                      (mapconcat (lambda (str) (substring str 0 10)) merge-heads " "))
530                            ""))
531                  (if (ewoc-nth status 0) "" "    No changes."))))
532
533 (defun git-get-filenames (files)
534   (mapcar (lambda (info) (git-fileinfo->name info)) files))
535
536 (defun git-update-index (index-file files)
537   "Run git-update-index on a list of files."
538   (let ((env (and index-file `(("GIT_INDEX_FILE" . ,index-file))))
539         added deleted modified)
540     (dolist (info files)
541       (case (git-fileinfo->state info)
542         ('added (push info added))
543         ('deleted (push info deleted))
544         ('modified (push info modified))))
545     (when added
546       (apply #'git-run-command nil env "update-index" "--add" "--" (git-get-filenames added)))
547     (when deleted
548       (apply #'git-run-command nil env "update-index" "--remove" "--" (git-get-filenames deleted)))
549     (when modified
550       (apply #'git-run-command nil env "update-index" "--" (git-get-filenames modified)))))
551
552 (defun git-do-commit ()
553   "Perform the actual commit using the current buffer as log message."
554   (interactive)
555   (let ((buffer (current-buffer))
556         (index-file (make-temp-file "gitidx")))
557     (with-current-buffer log-edit-parent-buffer
558       (if (git-marked-files-state 'unmerged)
559           (message "You cannot commit unmerged files, resolve them first.")
560         (unwind-protect
561             (let ((files (git-marked-files-state 'added 'deleted 'modified))
562                   head head-tree)
563               (unless (git-empty-db-p)
564                 (setq head (git-rev-parse "HEAD")
565                       head-tree (git-rev-parse "HEAD^{tree}")))
566               (if files
567                   (progn
568                     (git-read-tree head-tree index-file)
569                     (git-update-index nil files)         ;update both the default index
570                     (git-update-index index-file files)  ;and the temporary one
571                     (let ((tree (git-write-tree index-file)))
572                       (if (or (not (string-equal tree head-tree))
573                               (yes-or-no-p "The tree was not modified, do you really want to perform an empty commit? "))
574                           (let ((commit (git-commit-tree buffer tree head)))
575                             (git-update-ref "HEAD" commit head)
576                             (condition-case nil (delete-file ".git/MERGE_HEAD") (error nil))
577                             (with-current-buffer buffer (erase-buffer))
578                             (git-set-files-state files 'uptodate)
579                             (git-refresh-files)
580                             (git-refresh-ewoc-hf git-status)
581                             (message "Committed %s." commit))
582                         (message "Commit aborted."))))
583                 (message "No files to commit.")))
584           (delete-file index-file))))))
585
586
587 ;;;; Interactive functions
588 ;;;; ------------------------------------------------------------
589
590 (defun git-mark-file ()
591   "Mark the file that the cursor is on and move to the next one."
592   (interactive)
593   (unless git-status (error "Not in git-status buffer."))
594   (let* ((pos (ewoc-locate git-status))
595          (info (ewoc-data pos)))
596     (setf (git-fileinfo->marked info) t)
597     (ewoc-invalidate git-status pos)
598     (ewoc-goto-next git-status 1)))
599
600 (defun git-unmark-file ()
601   "Unmark the file that the cursor is on and move to the next one."
602   (interactive)
603   (unless git-status (error "Not in git-status buffer."))
604   (let* ((pos (ewoc-locate git-status))
605          (info (ewoc-data pos)))
606     (setf (git-fileinfo->marked info) nil)
607     (ewoc-invalidate git-status pos)
608     (ewoc-goto-next git-status 1)))
609
610 (defun git-unmark-file-up ()
611   "Unmark the file that the cursor is on and move to the previous one."
612   (interactive)
613   (unless git-status (error "Not in git-status buffer."))
614   (let* ((pos (ewoc-locate git-status))
615          (info (ewoc-data pos)))
616     (setf (git-fileinfo->marked info) nil)
617     (ewoc-invalidate git-status pos)
618     (ewoc-goto-prev git-status 1)))
619
620 (defun git-mark-all ()
621   "Mark all files."
622   (interactive)
623   (unless git-status (error "Not in git-status buffer."))
624   (ewoc-map (lambda (info) (setf (git-fileinfo->marked info) t) t) git-status)
625   ; move back to goal column after invalidate
626   (when goal-column (move-to-column goal-column)))
627
628 (defun git-unmark-all ()
629   "Unmark all files."
630   (interactive)
631   (unless git-status (error "Not in git-status buffer."))
632   (ewoc-map (lambda (info) (setf (git-fileinfo->marked info) nil) t) git-status)
633   ; move back to goal column after invalidate
634   (when goal-column (move-to-column goal-column)))
635
636 (defun git-toggle-all-marks ()
637   "Toggle all file marks."
638   (interactive)
639   (unless git-status (error "Not in git-status buffer."))
640   (ewoc-map (lambda (info) (setf (git-fileinfo->marked info) (not (git-fileinfo->marked info))) t) git-status)
641   ; move back to goal column after invalidate
642   (when goal-column (move-to-column goal-column)))
643
644 (defun git-next-file (&optional n)
645   "Move the selection down N files."
646   (interactive "p")
647   (unless git-status (error "Not in git-status buffer."))
648   (ewoc-goto-next git-status n))
649
650 (defun git-prev-file (&optional n)
651   "Move the selection up N files."
652   (interactive "p")
653   (unless git-status (error "Not in git-status buffer."))
654   (ewoc-goto-prev git-status n))
655
656 (defun git-add-file ()
657   "Add marked file(s) to the index cache."
658   (interactive)
659   (let ((files (git-marked-files-state 'unknown)))
660     (unless files
661       (push (ewoc-data
662              (git-add-status-file 'added (file-relative-name
663                                           (read-file-name "File to add: " nil nil t))))
664             files))
665     (apply #'git-run-command nil nil "update-index" "--info-only" "--add" "--" (git-get-filenames files))
666     (git-set-files-state files 'added)
667     (git-refresh-files)))
668
669 (defun git-ignore-file ()
670   "Add marked file(s) to the ignore list."
671   (interactive)
672   (let ((files (git-marked-files-state 'unknown)))
673     (unless files
674       (push (ewoc-data
675              (git-add-status-file 'unknown (file-relative-name
676                                             (read-file-name "File to ignore: " nil nil t))))
677             files))
678     (dolist (info files) (git-append-to-ignore (git-fileinfo->name info)))
679     (git-set-files-state files 'ignored)
680     (git-refresh-files)))
681
682 (defun git-remove-file ()
683   "Remove the marked file(s)."
684   (interactive)
685   (let ((files (git-marked-files-state 'added 'modified 'unknown 'uptodate)))
686     (unless files
687       (push (ewoc-data
688              (git-add-status-file 'unknown (file-relative-name
689                                             (read-file-name "File to remove: " nil nil t))))
690             files))
691     (if (yes-or-no-p
692          (format "Remove %d file%s? " (length files) (if (> (length files) 1) "s" "")))
693         (progn
694           (dolist (info files)
695             (let ((name (git-fileinfo->name info)))
696               (when (file-exists-p name) (delete-file name))))
697           (apply #'git-run-command nil nil "update-index" "--info-only" "--remove" "--" (git-get-filenames files))
698           ; remove unknown files from the list, set the others to deleted
699           (ewoc-filter git-status
700                        (lambda (info files)
701                          (not (and (memq info files) (eq (git-fileinfo->state info) 'unknown))))
702                        files)
703           (git-set-files-state files 'deleted)
704           (git-refresh-files)
705           (unless (ewoc-nth git-status 0)  ; refresh header if list is empty
706             (git-refresh-ewoc-hf git-status)))
707       (message "Aborting"))))
708
709 (defun git-revert-file ()
710   "Revert changes to the marked file(s)."
711   (interactive)
712   (let ((files (git-marked-files))
713         added modified)
714     (when (and files
715                (yes-or-no-p
716                 (format "Revert %d file%s? " (length files) (if (> (length files) 1) "s" ""))))
717       (dolist (info files)
718         (case (git-fileinfo->state info)
719           ('added (push info added))
720           ('deleted (push info modified))
721           ('unmerged (push info modified))
722           ('modified (push info modified))))
723       (when added
724           (apply #'git-run-command nil nil "update-index" "--force-remove" "--" (git-get-filenames added))
725           (git-set-files-state added 'unknown))
726       (when modified
727           (apply #'git-run-command nil nil "checkout" "HEAD" (git-get-filenames modified))
728           (git-set-files-state modified 'uptodate))
729       (git-refresh-files))))
730
731 (defun git-resolve-file ()
732   "Resolve conflicts in marked file(s)."
733   (interactive)
734   (let ((files (git-marked-files-state 'unmerged)))
735     (when files
736       (apply #'git-run-command nil nil "update-index" "--info-only" "--" (git-get-filenames files))
737       (git-set-files-state files 'modified)
738       (git-refresh-files))))
739
740 (defun git-remove-handled ()
741   "Remove handled files from the status list."
742   (interactive)
743   (ewoc-filter git-status
744                (lambda (info)
745                  (not (or (eq (git-fileinfo->state info) 'ignored)
746                           (eq (git-fileinfo->state info) 'uptodate)))))
747   (unless (ewoc-nth git-status 0)  ; refresh header if list is empty
748     (git-refresh-ewoc-hf git-status)))
749
750 (defun git-setup-diff-buffer (buffer)
751   "Setup a buffer for displaying a diff."
752   (with-current-buffer buffer
753     (diff-mode)
754     (goto-char (point-min))
755     (setq buffer-read-only t))
756   (display-buffer buffer)
757   (shrink-window-if-larger-than-buffer))
758
759 (defun git-diff-file ()
760   "Diff the marked file(s) against HEAD."
761   (interactive)
762   (let ((files (git-marked-files)))
763     (git-setup-diff-buffer
764      (apply #'git-run-command-buffer "*git-diff*" "diff-index" "-p" "-M" "HEAD" "--" (git-get-filenames files)))))
765
766 (defun git-diff-unmerged-file (stage)
767   "Diff the marked unmerged file(s) against the specified stage."
768   (let ((files (git-marked-files)))
769     (git-setup-diff-buffer
770      (apply #'git-run-command-buffer "*git-diff*" "diff-files" "-p" stage "--" (git-get-filenames files)))))
771
772 (defun git-diff-file-base ()
773   "Diff the marked unmerged file(s) against the common base file."
774   (interactive)
775   (git-diff-unmerged-file "-1"))
776
777 (defun git-diff-file-mine ()
778   "Diff the marked unmerged file(s) against my pre-merge version."
779   (interactive)
780   (git-diff-unmerged-file "-2"))
781
782 (defun git-diff-file-other ()
783   "Diff the marked unmerged file(s) against the other's pre-merge version."
784   (interactive)
785   (git-diff-unmerged-file "-3"))
786
787 (defun git-diff-file-combined ()
788   "Do a combined diff of the marked unmerged file(s)."
789   (interactive)
790   (git-diff-unmerged-file "-c"))
791
792 (defun git-diff-file-idiff ()
793   "Perform an interactive diff on the current file."
794   (interactive)
795   (error "Interactive diffs not implemented yet."))
796
797 (defun git-log-file ()
798   "Display a log of changes to the marked file(s)."
799   (interactive)
800   (let* ((files (git-marked-files))
801          (coding-system-for-read git-commits-coding-system)
802          (buffer (apply #'git-run-command-buffer "*git-log*" "rev-list" "--pretty" "HEAD" "--" (git-get-filenames files))))
803     (with-current-buffer buffer
804       ; (git-log-mode)  FIXME: implement log mode
805       (goto-char (point-min))
806       (setq buffer-read-only t))
807     (display-buffer buffer)))
808
809 (defun git-log-edit-files ()
810   "Return a list of marked files for use in the log-edit buffer."
811   (with-current-buffer log-edit-parent-buffer
812     (git-get-filenames (git-marked-files-state 'added 'deleted 'modified))))
813
814 (defun git-commit-file ()
815   "Commit the marked file(s), asking for a commit message."
816   (interactive)
817   (unless git-status (error "Not in git-status buffer."))
818   (let ((buffer (get-buffer-create "*git-commit*"))
819         (merge-heads (git-get-merge-heads))
820         (dir default-directory)
821         (sign-off git-append-signed-off-by))
822     (with-current-buffer buffer
823       (when (eq 0 (buffer-size))
824         (cd dir)
825         (erase-buffer)
826         (insert
827          (propertize
828           (format "Author: %s <%s>\n%s"
829                   (git-get-committer-name) (git-get-committer-email)
830                   (if merge-heads
831                       (format "Parent: %s\n%s\n"
832                               (git-rev-parse "HEAD")
833                               (mapconcat (lambda (str) (concat "Parent: " str)) merge-heads "\n"))
834                     ""))
835           'face 'git-header-face)
836          (propertize git-log-msg-separator 'face 'git-separator-face)
837          "\n")
838         (cond ((and merge-heads (file-readable-p ".git/MERGE_MSG"))
839                (insert-file-contents ".git/MERGE_MSG"))
840               (sign-off
841                (insert (format "\n\nSigned-off-by: %s <%s>\n"
842                                (git-get-committer-name) (git-get-committer-email)))))))
843     (let ((log-edit-font-lock-keywords
844            `(("^\\(Author:\\|Date:\\|Parent:\\|Signed-off-by:\\)\\(.*\\)"
845               (1 font-lock-keyword-face)
846               (2 font-lock-function-name-face))
847              (,(concat "^\\(" (regexp-quote git-log-msg-separator) "\\)$")
848               (1 font-lock-comment-face)))))
849       (log-edit #'git-do-commit nil #'git-log-edit-files buffer))))
850
851 (defun git-find-file ()
852   "Visit the current file in its own buffer."
853   (interactive)
854   (unless git-status (error "Not in git-status buffer."))
855   (let ((info (ewoc-data (ewoc-locate git-status))))
856     (find-file (git-fileinfo->name info))
857     (when (eq 'unmerged (git-fileinfo->state info))
858       (smerge-mode))))
859
860 (defun git-find-file-imerge ()
861   "Visit the current file in interactive merge mode."
862   (interactive)
863   (unless git-status (error "Not in git-status buffer."))
864   (let ((info (ewoc-data (ewoc-locate git-status))))
865     (find-file (git-fileinfo->name info))
866     (smerge-ediff)))
867
868 (defun git-view-file ()
869   "View the current file in its own buffer."
870   (interactive)
871   (unless git-status (error "Not in git-status buffer."))
872   (let ((info (ewoc-data (ewoc-locate git-status))))
873     (view-file (git-fileinfo->name info))))
874
875 (defun git-refresh-status ()
876   "Refresh the git status buffer."
877   (interactive)
878   (let* ((status git-status)
879          (pos (ewoc-locate status))
880          (cur-name (and pos (git-fileinfo->name (ewoc-data pos)))))
881     (unless status (error "Not in git-status buffer."))
882     (git-clear-status status)
883     (git-run-command nil nil "update-index" "--info-only" "--refresh")
884     (if (git-empty-db-p)
885         ; we need some special handling for an empty db
886         (with-temp-buffer
887           (git-run-command t nil "ls-files" "-z" "-t" "-c")
888           (git-parse-ls-files status 'added))
889       (with-temp-buffer
890         (git-run-command t nil "diff-index" "-z" "-M" "HEAD")
891         (git-parse-status status)))
892       (with-temp-buffer
893         (git-run-command t nil "ls-files" "-z" "-u")
894         (git-parse-ls-unmerged status))
895       (when (file-readable-p ".git/info/exclude")
896         (with-temp-buffer
897           (git-run-command t nil "ls-files" "-z" "-t" "-o"
898                            "--exclude-from=.git/info/exclude"
899                            (concat "--exclude-per-directory=" git-per-dir-ignore-file))
900           (git-parse-ls-files status 'unknown)))
901     (git-refresh-files)
902     (git-refresh-ewoc-hf status)
903     ; move point to the current file name if any
904     (let ((node (and cur-name (git-find-status-file status cur-name))))
905       (when node (ewoc-goto-node status node)))))
906
907 (defun git-status-quit ()
908   "Quit git-status mode."
909   (interactive)
910   (bury-buffer))
911
912 ;;;; Major Mode
913 ;;;; ------------------------------------------------------------
914
915 (defvar git-status-mode-hook nil
916   "Run after `git-status-mode' is setup.")
917
918 (defvar git-status-mode-map nil
919   "Keymap for git major mode.")
920
921 (defvar git-status nil
922   "List of all files managed by the git-status mode.")
923
924 (unless git-status-mode-map
925   (let ((map (make-keymap))
926         (diff-map (make-sparse-keymap)))
927     (suppress-keymap map)
928     (define-key map " "   'git-next-file)
929     (define-key map "a"   'git-add-file)
930     (define-key map "c"   'git-commit-file)
931     (define-key map "d"    diff-map)
932     (define-key map "="   'git-diff-file)
933     (define-key map "f"   'git-find-file)
934     (define-key map "\r"  'git-find-file)
935     (define-key map "g"   'git-refresh-status)
936     (define-key map "i"   'git-ignore-file)
937     (define-key map "l"   'git-log-file)
938     (define-key map "m"   'git-mark-file)
939     (define-key map "M"   'git-mark-all)
940     (define-key map "n"   'git-next-file)
941     (define-key map "p"   'git-prev-file)
942     (define-key map "q"   'git-status-quit)
943     (define-key map "r"   'git-remove-file)
944     (define-key map "R"   'git-resolve-file)
945     (define-key map "T"   'git-toggle-all-marks)
946     (define-key map "u"   'git-unmark-file)
947     (define-key map "U"   'git-revert-file)
948     (define-key map "v"   'git-view-file)
949     (define-key map "x"   'git-remove-handled)
950     (define-key map "\C-?" 'git-unmark-file-up)
951     (define-key map "\M-\C-?" 'git-unmark-all)
952     ; the diff submap
953     (define-key diff-map "b" 'git-diff-file-base)
954     (define-key diff-map "c" 'git-diff-file-combined)
955     (define-key diff-map "=" 'git-diff-file)
956     (define-key diff-map "e" 'git-diff-file-idiff)
957     (define-key diff-map "E" 'git-find-file-imerge)
958     (define-key diff-map "m" 'git-diff-file-mine)
959     (define-key diff-map "o" 'git-diff-file-other)
960     (setq git-status-mode-map map)))
961
962 ;; git mode should only run in the *git status* buffer
963 (put 'git-status-mode 'mode-class 'special)
964
965 (defun git-status-mode ()
966   "Major mode for interacting with Git.
967 Commands:
968 \\{git-status-mode-map}"
969   (kill-all-local-variables)
970   (buffer-disable-undo)
971   (setq mode-name "git status"
972         major-mode 'git-status-mode
973         goal-column 17
974         buffer-read-only t)
975   (use-local-map git-status-mode-map)
976   (let ((buffer-read-only nil))
977     (erase-buffer)
978   (let ((status (ewoc-create 'git-fileinfo-prettyprint "" "")))
979     (set (make-local-variable 'git-status) status))
980   (set (make-local-variable 'list-buffers-directory) default-directory)
981   (run-hooks 'git-status-mode-hook)))
982
983 (defun git-status (dir)
984   "Entry point into git-status mode."
985   (interactive "DSelect directory: ")
986   (setq dir (git-get-top-dir dir))
987   (if (file-directory-p (concat (file-name-as-directory dir) ".git"))
988       (let ((buffer (create-file-buffer (expand-file-name "*git-status*" dir))))
989         (switch-to-buffer buffer)
990         (cd dir)
991         (git-status-mode)
992         (git-refresh-status)
993         (goto-char (point-min)))
994     (message "%s is not a git working tree." dir)))
995
996 (provide 'git)
997 ;;; git.el ends here