git.el: Support for incremental status updates.

When we know which files have been modified, we can now run diff-index
or ls-files with a file list to refresh only the specified files
instead of the whole project.

This also allows proper refreshing of files upon add/delete/resolve,
instead of making assumptions about the new file state.

Signed-off-by: Alexandre Julliard <julliard@winehq.org>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Alexandre Julliard 2007-07-24 12:12:47 +02:00 committed by Junio C Hamano
parent 1130845be8
commit 93c22eeb30

View File

@ -314,8 +314,8 @@ and returns the process output as a string."
(sort-lines nil (point-min) (point-max)) (sort-lines nil (point-min) (point-max))
(save-buffer)) (save-buffer))
(when created (when created
(git-run-command nil nil "update-index" "--info-only" "--add" "--" (file-relative-name ignore-name))) (git-run-command nil nil "update-index" "--add" "--" (file-relative-name ignore-name)))
(git-add-status-file (if created 'added 'modified) (file-relative-name ignore-name)))) (git-update-status-files (list (file-relative-name ignore-name)) 'unknown)))
; propertize definition for XEmacs, stolen from erc-compat ; propertize definition for XEmacs, stolen from erc-compat
(eval-when-compile (eval-when-compile
@ -523,23 +523,39 @@ and returns the process output as a string."
" " (git-escape-file-name (git-fileinfo->name info)) " " (git-escape-file-name (git-fileinfo->name info))
(git-rename-as-string info)))) (git-rename-as-string info))))
(defun git-parse-status (status) (defun git-insert-fileinfo (status info &optional refresh)
"Parse the output of git-diff-index in the current buffer." "Insert INFO in the status buffer, optionally refreshing an existing one."
(goto-char (point-min)) (let ((node (and refresh
(while (re-search-forward (git-find-status-file status (git-fileinfo->name info)))))
":\\([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" (setf (git-fileinfo->needs-refresh info) t)
nil t 1) (when node ;preserve the marked flag
(let ((old-perm (string-to-number (match-string 1) 8)) (setf (git-fileinfo->marked info) (git-fileinfo->marked (ewoc-data node))))
(new-perm (string-to-number (match-string 2) 8)) (if node (ewoc-set-data node info) (ewoc-enter-last status info))))
(state (or (match-string 4) (match-string 6)))
(name (or (match-string 5) (match-string 7))) (defun git-run-diff-index (status files)
(new-name (match-string 8))) "Run git-diff-index on FILES and parse the results into STATUS.
(if new-name ; copy or rename Return the list of files that haven't been handled."
(if (eq ?C (string-to-char state)) (let ((refresh files))
(ewoc-enter-last status (git-create-fileinfo 'added new-name old-perm new-perm 'copy name)) (with-temp-buffer
(ewoc-enter-last status (git-create-fileinfo 'deleted name 0 0 'rename new-name)) (apply #'git-run-command t nil "diff-index" "-z" "-M" "HEAD" "--" files)
(ewoc-enter-last status (git-create-fileinfo 'added new-name old-perm new-perm 'rename name))) (goto-char (point-min))
(ewoc-enter-last status (git-create-fileinfo (git-state-code state) name old-perm new-perm)))))) (while (re-search-forward
":\\([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"
nil t 1)
(let ((old-perm (string-to-number (match-string 1) 8))
(new-perm (string-to-number (match-string 2) 8))
(state (or (match-string 4) (match-string 6)))
(name (or (match-string 5) (match-string 7)))
(new-name (match-string 8)))
(if new-name ; copy or rename
(if (eq ?C (string-to-char state))
(git-insert-fileinfo status (git-create-fileinfo 'added new-name old-perm new-perm 'copy name) refresh)
(git-insert-fileinfo status (git-create-fileinfo 'deleted name 0 0 'rename new-name) refresh)
(git-insert-fileinfo status (git-create-fileinfo 'added new-name old-perm new-perm 'rename name)) refresh)
(git-insert-fileinfo status (git-create-fileinfo (git-state-code state) name old-perm new-perm) refresh))
(setq files (delete name files))
(when new-name (setq files (delete new-name files)))))))
files)
(defun git-find-status-file (status file) (defun git-find-status-file (status file)
"Find a given file in the status ewoc and return its node." "Find a given file in the status ewoc and return its node."
@ -548,32 +564,59 @@ and returns the process output as a string."
(setq node (ewoc-next status node))) (setq node (ewoc-next status node)))
node)) node))
(defun git-parse-ls-files (status default-state &optional skip-existing) (defun git-run-ls-files (status files default-state &rest options)
"Parse the output of git-ls-files in the current buffer." "Run git-ls-files on FILES and parse the results into STATUS.
(goto-char (point-min)) Return the list of files that haven't been handled."
(let (infolist) (let ((refresh files))
(while (re-search-forward "\\([HMRCK?]\\) \\([^\0]*\\)\0" nil t 1) (with-temp-buffer
(let ((state (match-string 1)) (apply #'git-run-command t nil "ls-files" "-z" "-t" (append options (list "--") files))
(name (match-string 2))) (goto-char (point-min))
(unless (and skip-existing (git-find-status-file status name)) (while (re-search-forward "\\([HMRCK?]\\) \\([^\0]*\\)\0" nil t 1)
(push (git-create-fileinfo (or (git-state-code state) default-state) name) infolist)))) (let ((state (match-string 1))
(dolist (info (nreverse infolist)) (name (match-string 2)))
(ewoc-enter-last status info)))) (git-insert-fileinfo status (git-create-fileinfo (or (git-state-code state) default-state) name) refresh)
(setq files (delete name files))))))
files)
(defun git-parse-ls-unmerged (status) (defun git-run-ls-unmerged (status files)
"Parse the output of git-ls-files -u in the current buffer." "Run git-ls-files -u on FILES and parse the results into STATUS."
(goto-char (point-min)) (with-temp-buffer
(let (files) (apply #'git-run-command t nil "ls-files" "-z" "-u" "--" files)
(while (re-search-forward "[0-7]\\{6\\} [0-9a-f]\\{40\\} [123]\t\\([^\0]+\\)\0" nil t) (goto-char (point-min))
(let ((node (git-find-status-file status (match-string 1)))) (let (unmerged-files)
(when node (push (ewoc-data node) files)))) (while (re-search-forward "[0-7]\\{6\\} [0-9a-f]\\{40\\} [123]\t\\([^\0]+\\)\0" nil t)
(git-set-files-state files 'unmerged))) (let ((node (git-find-status-file status (match-string 1))))
(when node (push (ewoc-data node) unmerged-files))))
(git-set-files-state unmerged-files 'unmerged))))
(defun git-add-status-file (state name) (defun git-update-status-files (files &optional default-state)
"Add a new file to the status list (if not existing already) and return its node." "Update the status of FILES from the index."
(unless git-status (error "Not in git-status buffer.")) (unless git-status (error "Not in git-status buffer."))
(or (git-find-status-file git-status name) (let* ((status git-status)
(ewoc-enter-last git-status (git-create-fileinfo state name)))) (remaining-files
(if (git-empty-db-p) ; we need some special handling for an empty db
(git-run-ls-files status files 'added "-c")
(git-run-diff-index status files))))
(git-run-ls-unmerged status files)
(when (and (or (not files) remaining-files)
(file-readable-p ".git/info/exclude"))
(setq remaining-files (git-run-ls-files status remaining-files
'unknown "-o" "--exclude-from=.git/info/exclude"
(concat "--exclude-per-directory=" git-per-dir-ignore-file))))
; mark remaining files with the default state (or remove them if nil)
(when remaining-files
(if default-state
(ewoc-map (lambda (info)
(when (member (git-fileinfo->name info) remaining-files)
(git-set-files-state (list info) default-state))
nil)
status)
(ewoc-filter status
(lambda (info files)
(not (member (git-fileinfo->name info) files)))
remaining-files)))
(git-refresh-files)
(git-refresh-ewoc-hf status)))
(defun git-marked-files () (defun git-marked-files ()
"Return a list of all marked files, or if none a list containing just the file at cursor position." "Return a list of all marked files, or if none a list containing just the file at cursor position."
@ -789,54 +832,34 @@ and returns the process output as a string."
(defun git-add-file () (defun git-add-file ()
"Add marked file(s) to the index cache." "Add marked file(s) to the index cache."
(interactive) (interactive)
(let ((files (git-marked-files-state 'unknown))) (let ((files (git-get-filenames (git-marked-files-state 'unknown))))
(unless files (unless files
(push (ewoc-data (push (file-relative-name (read-file-name "File to add: " nil nil t)) files))
(git-add-status-file 'added (file-relative-name (apply #'git-run-command nil nil "update-index" "--add" "--" files)
(read-file-name "File to add: " nil nil t)))) (git-update-status-files files 'uptodate)))
files))
(apply #'git-run-command nil nil "update-index" "--info-only" "--add" "--" (git-get-filenames files))
(git-set-files-state files 'added)
(git-refresh-files)))
(defun git-ignore-file () (defun git-ignore-file ()
"Add marked file(s) to the ignore list." "Add marked file(s) to the ignore list."
(interactive) (interactive)
(let ((files (git-marked-files-state 'unknown))) (let ((files (git-get-filenames (git-marked-files-state 'unknown))))
(unless files (unless files
(push (ewoc-data (push (file-relative-name (read-file-name "File to ignore: " nil nil t)) files))
(git-add-status-file 'unknown (file-relative-name (dolist (f files) (git-append-to-ignore f))
(read-file-name "File to ignore: " nil nil t)))) (git-update-status-files files 'ignored)))
files))
(dolist (info files) (git-append-to-ignore (git-fileinfo->name info)))
(git-set-files-state files 'ignored)
(git-refresh-files)))
(defun git-remove-file () (defun git-remove-file ()
"Remove the marked file(s)." "Remove the marked file(s)."
(interactive) (interactive)
(let ((files (git-marked-files-state 'added 'modified 'unknown 'uptodate))) (let ((files (git-get-filenames (git-marked-files-state 'added 'modified 'unknown 'uptodate))))
(unless files (unless files
(push (ewoc-data (push (file-relative-name (read-file-name "File to remove: " nil nil t)) files))
(git-add-status-file 'unknown (file-relative-name
(read-file-name "File to remove: " nil nil t))))
files))
(if (yes-or-no-p (if (yes-or-no-p
(format "Remove %d file%s? " (length files) (if (> (length files) 1) "s" ""))) (format "Remove %d file%s? " (length files) (if (> (length files) 1) "s" "")))
(progn (progn
(dolist (info files) (dolist (name files)
(let ((name (git-fileinfo->name info))) (when (file-exists-p name) (delete-file name)))
(when (file-exists-p name) (delete-file name)))) (apply #'git-run-command nil nil "update-index" "--remove" "--" files)
(apply #'git-run-command nil nil "update-index" "--info-only" "--remove" "--" (git-get-filenames files)) (git-update-status-files files nil))
; remove unknown files from the list, set the others to deleted
(ewoc-filter git-status
(lambda (info files)
(not (and (memq info files) (eq (git-fileinfo->state info) 'unknown))))
files)
(git-set-files-state files 'deleted)
(git-refresh-files)
(unless (ewoc-nth git-status 0) ; refresh header if list is empty
(git-refresh-ewoc-hf git-status)))
(message "Aborting")))) (message "Aborting"))))
(defun git-revert-file () (defun git-revert-file ()
@ -849,26 +872,23 @@ and returns the process output as a string."
(format "Revert %d file%s? " (length files) (if (> (length files) 1) "s" "")))) (format "Revert %d file%s? " (length files) (if (> (length files) 1) "s" ""))))
(dolist (info files) (dolist (info files)
(case (git-fileinfo->state info) (case (git-fileinfo->state info)
('added (push info added)) ('added (push (git-fileinfo->name info) added))
('deleted (push info modified)) ('deleted (push (git-fileinfo->name info) modified))
('unmerged (push info modified)) ('unmerged (push (git-fileinfo->name info) modified))
('modified (push info modified)))) ('modified (push (git-fileinfo->name info) modified))))
(when added (when added
(apply #'git-run-command nil nil "update-index" "--force-remove" "--" (git-get-filenames added)) (apply #'git-run-command nil nil "update-index" "--force-remove" "--" added))
(git-set-files-state added 'unknown))
(when modified (when modified
(apply #'git-run-command nil nil "checkout" "HEAD" (git-get-filenames modified)) (apply #'git-run-command nil nil "checkout" "HEAD" modified))
(git-set-files-state modified 'uptodate)) (git-update-status-files (append added modified) 'uptodate))))
(git-refresh-files))))
(defun git-resolve-file () (defun git-resolve-file ()
"Resolve conflicts in marked file(s)." "Resolve conflicts in marked file(s)."
(interactive) (interactive)
(let ((files (git-marked-files-state 'unmerged))) (let ((files (git-get-filenames (git-marked-files-state 'unmerged))))
(when files (when files
(apply #'git-run-command nil nil "update-index" "--" (git-get-filenames files)) (apply #'git-run-command nil nil "update-index" "--" files)
(git-set-files-state files 'modified) (git-update-status-files files 'uptodate))))
(git-refresh-files))))
(defun git-remove-handled () (defun git-remove-handled ()
"Remove handled files from the status list." "Remove handled files from the status list."
@ -1071,27 +1091,9 @@ and returns the process output as a string."
(pos (ewoc-locate status)) (pos (ewoc-locate status))
(cur-name (and pos (git-fileinfo->name (ewoc-data pos))))) (cur-name (and pos (git-fileinfo->name (ewoc-data pos)))))
(unless status (error "Not in git-status buffer.")) (unless status (error "Not in git-status buffer."))
(git-run-command nil nil "update-index" "--refresh")
(git-clear-status status) (git-clear-status status)
(git-run-command nil nil "update-index" "--info-only" "--refresh") (git-update-status-files nil)
(if (git-empty-db-p)
; we need some special handling for an empty db
(with-temp-buffer
(git-run-command t nil "ls-files" "-z" "-t" "-c")
(git-parse-ls-files status 'added))
(with-temp-buffer
(git-run-command t nil "diff-index" "-z" "-M" "HEAD")
(git-parse-status status)))
(with-temp-buffer
(git-run-command t nil "ls-files" "-z" "-u")
(git-parse-ls-unmerged status))
(when (file-readable-p ".git/info/exclude")
(with-temp-buffer
(git-run-command t nil "ls-files" "-z" "-t" "-o"
"--exclude-from=.git/info/exclude"
(concat "--exclude-per-directory=" git-per-dir-ignore-file))
(git-parse-ls-files status 'unknown)))
(git-refresh-files)
(git-refresh-ewoc-hf status)
; move point to the current file name if any ; move point to the current file name if any
(let ((node (and cur-name (git-find-status-file status cur-name)))) (let ((node (and cur-name (git-find-status-file status cur-name))))
(when node (ewoc-goto-node status node))))) (when node (ewoc-goto-node status node)))))