diff --git a/magit.el b/magit.el index d3d23ccb5c..7480cbe202 100644 --- a/magit.el +++ b/magit.el @@ -741,6 +741,7 @@ This is calculated from `magit-highlight-indentation'.") (define-key map (kbd "+") 'magit-diff-larger-hunks) (define-key map (kbd "0") 'magit-diff-default-hunks) (define-key map (kbd "h") 'magit-toggle-diff-refine-hunk) + (define-key map (kbd "M-g") 'magit-goto-diffstats) map)) (defvar magit-commit-mode-map @@ -791,7 +792,11 @@ This is calculated from `magit-highlight-indentation'.") (define-key map (kbd "T") 'magit-change-what-branch-tracks) map)) - +(defvar magit-diffstat-keymap + (let ((map (make-sparse-keymap))) + (define-key map (kbd "e") 'magit-diffstat-ediff) + map)) + (defun magit-bug-report (str) "Ask the user to submit a bug report about the error described in STR." ;; XXX - should propose more information to be included. @@ -1447,6 +1452,14 @@ DEF is the default value." (defvar magit-section-hidden-default nil) +(defvar magit-show-diffstat t + "If non-nil, diff and commit log will display diffstat.") + +(defvar magit-diffstat-cached-sections) +(make-variable-buffer-local 'magit-diffstat-cached-sections) +(put 'magit-diffstat-cached-sections 'permanent-local t) + + (defun magit-new-section (title type) "Create a new section with title TITLE and type TYPE in current buffer. @@ -1719,6 +1732,31 @@ See `magit-insert-section' for meaning of the arguments" (goto-char (magit-section-beginning sec)) (message "No such section")))) + +(defun magit-goto-diff-section-at-file (file) + "Go to the section containing by the pathname, FILE" + (let ((pos (catch 'diff-section-found + (dolist (sec (magit-section-children magit-top-section)) + (when (and (eq (magit-section-type sec) 'diff) + (string-equal (magit-diff-item-file sec) file)) + (throw 'diff-section-found + (magit-section-beginning sec))))))) + (when pos + (goto-char pos)))) + +(defun magit-goto-diffstats () + "Go to the diffstats section if exists" + (interactive) + (let ((pos (catch 'section-found + (dolist (sec (magit-section-children magit-top-section)) + (when (eq (magit-section-type sec) 'diffstats) + (throw 'section-found + (magit-section-beginning sec))))))) + (if pos + (goto-char pos) + (if (called-interactively-p 'interactive) + (message "No diffstats section found"))))) + (defun magit-for-all-sections (func &optional top) "Run FUNC on TOP and recursively on all its children. Default value for TOP is `magit-top-section'" @@ -2732,15 +2770,135 @@ Customize `magit-diff-refine-hunk' to change the default mode." nil))) (defun magit-wash-diffs () + (magit-wash-diffstats) (magit-wash-sequence #'magit-wash-diff-or-other-file)) (defun magit-wash-diff-or-other-file () (or (magit-wash-diff) (magit-wash-other-file))) +(defun magit-diffstat-ediff () + (interactive) + (magit-goto-diff-section-at-file + (magit-diff-item-file (magit-current-section))) + (call-interactively 'magit-ediff)) + +(defun magit-wash-diffstat (&optional guess) + (let ((entry-regexp "^ ?\\(.*?\\)\\( +| +.*\\)$")) + (when (looking-at entry-regexp) + (let ((file (match-string-no-properties 1)) + (remaining (match-string-no-properties 2))) + (delete-region (point) (+ (line-end-position) 1)) + (magit-with-section "diffstat" 'diffstat + ;;(magit-set-section-info 'incomplete) + + ;; diffstat entries will look like + ;; + ;; ' PSEUDO-FILE-NAME | +++---' + ;; + ;; Since PSEUDO-FILE-NAME is not always real pathname, we + ;; don't know the pathname yet. Thus, section info will be + ;; (diffstat FILE incomplete MARKER-BEGIN MARKER-END) for + ;; now, where MARKER-BEGIN points the beginning of the + ;; PSEUDO-FILE-NAME, MARKER-END points the end of the + ;; PSEUDO-FILE-NAME, and FILE will be abbreviated filename. + ;; Later in `magit-wash-diff-or-other-file`, the section + ;; info will be updated. + + ;; Note that FILE is the 2nd element of the section-info; + ;; this is intentional, so that `magit-diff-item-file` can + ;; return the FILE part. + + ;; (list 'diffstat + ;; (or (and (> (length file) 3) + ;; (string-equal (substring file 0 3) "...") + ;; (message "got the invalid file here") + ;; 'truncated) + ;; file))) + + (insert " ") + (let ((f-begin (point-marker)) f-end) + (insert file) + (setq f-end (point-marker)) + + (magit-set-section-info (list 'diffstat + file 'incomplete f-begin f-end)) + (insert remaining) + (magit-put-line-property 'keymap magit-diffstat-keymap) + + (insert "\n") + (add-to-list 'magit-diffstat-cached-sections + magit-top-section)) + + ;; (insert (propertize (concat " " + ;; (propertize file + ;; 'face + ;; 'magit-diff-file-header) + ;; remaining) + ;; 'keymap + ;; magit-diffstat-keymap) + ;; "\n") + ))))) + +(defun magit-wash-diffstats () + (let ((entry-regexp "^ ?\\(.*?\\)\\( +| +.*\\)$") + (title-regexp "^ ?\\([0-9]+ +files? change.*\\)\n+") + (pos (point))) + (save-restriction + (save-match-data + (when (and (looking-at entry-regexp) + (re-search-forward title-regexp nil t)) + (let ((title-line (match-string-no-properties 1)) + (stat-end (point-marker))) + (delete-region (match-beginning 0) (match-end 0)) + (narrow-to-region pos stat-end) + (goto-char (point-min)) + (magit-with-section "diffstats" 'diffstats + (insert title-line) + ;;(magit-put-line-property 'face 'magit-section-title) + (insert "\n") + + (set (make-local-variable 'magit-diffstat-cached-sections) + nil) + + (magit-wash-sequence #'magit-wash-diffstat)) + (setq magit-diffstat-cached-sections + (nreverse magit-diffstat-cached-sections)) + (insert "\n"))))))) + +(defun magit-wash-diffstats-postwork (file &optional section) + (let ((sec (or section + (and (boundp 'magit-diffstat-cached-sections) + (pop magit-diffstat-cached-sections))))) + (when sec + (let* ((info (magit-section-info sec)) + (begin (nth 3 info)) + (end (nth 4 info))) + (put-text-property begin end + 'face 'magit-diff-file-header) + (magit-set-section-info (list 'diffstat file 'completed) + sec))))) + +(defun magit-diffstat-item-kind (diffstat) + (car (magit-section-info diffstat))) + +(defun magit-diffstat-item-file (diffstat) + (let ((file (cadr (magit-section-info diffstat)))) + ;; Git diffstat may shorten long pathname with the prefix "..." + ;; (e.g. ".../long/sub/dir/file" or "...longfilename") + (save-match-data + (if (string-match "\\`\\.\\.\\." file) + nil + file)))) + +(defun magit-diffstat-item-status (diffstat) + "Return 'completed or 'incomplete depending on the processed status" + (caddr (magit-section-info diffstat))) + (defun magit-wash-other-file () (if (looking-at "^? \\(.*\\)$") (let ((file (match-string-no-properties 1))) + (magit-wash-diffstats-postwork file) (delete-region (point) (+ (line-end-position) 1)) (magit-with-section file 'file (magit-set-section-info file) @@ -2808,6 +2966,8 @@ Customize `magit-diff-refine-hunk' to change the default mode." (goto-char (match-beginning 0)) (goto-char (point-max))) (point-marker)))) + (magit-wash-diffstats-postwork file) + (let* ((status (cond ((looking-at "^diff --cc") 'unmerged) @@ -3426,7 +3586,8 @@ insert a line to tell how to insert more of them" (defvar magit-currently-shown-commit nil) (defun magit-wash-commit () - (let ((magit-current-diff-range)) + (let ((magit-current-diff-range) + (merge-commit)) (when (looking-at "^commit \\([0-9a-fA-F]\\{40\\}\\)") (setq magit-current-diff-range (match-string 1)) (add-text-properties (match-beginning 1) (match-end 1) @@ -3435,7 +3596,8 @@ insert a line to tell how to insert more of them" ((search-forward-regexp "^Merge: \\([0-9a-fA-F]+\\) \\([0-9a-fA-F]+\\)$" nil t) (setq magit-current-diff-range (cons (cons (match-string 1) (match-string 2)) - magit-current-diff-range)) + magit-current-diff-range) + merge-commit t) (let ((first (magit-set-section nil 'commit (match-beginning 1) (match-end 1))) (second (magit-set-section nil 'commit (match-beginning 2) (match-end 2)))) (magit-set-section-info (match-string 1) first) @@ -3444,8 +3606,23 @@ insert a line to tell how to insert more of them" (make-commit-button (match-beginning 2) (match-end 2))) (t (setq magit-current-diff-range (cons (concat magit-current-diff-range "^") - magit-current-diff-range)))) - (search-forward-regexp "^$") + magit-current-diff-range) + merge-commit nil))) + + (search-forward-regexp "^$") ; point at the beginning of log msgs + + (when magit-show-diffstat + (let ((pos (point))) + (save-excursion + (forward-char) + (when (search-forward-regexp (if merge-commit "^$" "^---$") + nil t) + (delete-region (match-beginning 0) + (+ (match-end 0) 1)) + (insert "\n") + + (magit-wash-diffstats))))) + (while (and (search-forward-regexp "\\(\\b[0-9a-fA-F]\\{4,40\\}\\b\\)\\|\\(^diff\\)" nil 'noerror) (not (match-string 2))) @@ -3499,6 +3676,7 @@ insert a line to tell how to insert more of them" "--pretty=medium" `(,@(if magit-have-abbrev (list "--no-abbrev-commit")) ,@(if magit-have-decorate (list "--decorate=full")) + ,@(if magit-show-diffstat (list "--stat")) "--cc" "-p" ,commit)))) @@ -5275,10 +5453,13 @@ restore the window state that was saved before ediff was called." range)))) (setq magit-current-range range) (magit-create-buffer-sections - (magit-git-section 'diffbuf - (magit-rev-range-describe range "Changes") - 'magit-wash-diffs - "diff" (magit-diff-U-arg) args "--")))) + (apply #'magit-git-section + 'diffbuf + (magit-rev-range-describe range "Changes") + 'magit-wash-diffs + "diff" (magit-diff-U-arg) + `(,@(if magit-show-diffstat (list "--patch-with-stat")) + ,args "--"))))) (define-derived-mode magit-diff-mode magit-mode "Magit Diff" "Mode for looking at a git diff. @@ -5648,6 +5829,11 @@ With a prefix argument, visit in other window." (dired-jump other-window (file-truename info))) ((diff) (dired-jump other-window (file-truename (magit-diff-item-file item)))) + ((diffstat) + (let ((file (magit-diffstat-item-file item))) + (if file + (dired-jump other-window (file-truename file)) + (error "Can't get the pathname for this file")))) ((hunk) (dired-jump other-window (file-truename (magit-diff-item-file @@ -5672,6 +5858,18 @@ With a prefix argument, visit in other window." (funcall (if other-window 'find-file-other-window 'find-file) file))))) + ((diffstat) + (let ((file (magit-diffstat-item-file item))) + (cond ((null file) + (error "Can't get pathname for this file")) + ((not (file-exists-p file)) + (error "Can't visit deleted file: %s" file)) + ((file-directory-p file) + (magit-status file)) + (t + (funcall + (if other-window 'find-file-other-window 'find-file) + file))))) ((hunk) (let ((file (magit-diff-item-file (magit-hunk-item-diff item))) (line (magit-hunk-item-target-line item)) @@ -5695,6 +5893,8 @@ With a prefix argument, visit in other window." (call-interactively 'magit-visit-file-item)) ((diff) (call-interactively 'magit-visit-file-item)) + ((diffstat) + (call-interactively 'magit-visit-file-item)) ((hunk) (call-interactively 'magit-visit-file-item)) ((commit)