|
|
|
|
@@ -1,11 +1,11 @@
|
|
|
|
|
;;; diff-hl.el --- Highlight uncommitted changes using VC -*- lexical-binding: t -*-
|
|
|
|
|
|
|
|
|
|
;; Copyright (C) 2012-2023 Free Software Foundation, Inc.
|
|
|
|
|
;; Copyright (C) 2012-2024 Free Software Foundation, Inc.
|
|
|
|
|
|
|
|
|
|
;; Author: Dmitry Gutov <dgutov@yandex.ru>
|
|
|
|
|
;; Author: Dmitry Gutov <dmitry@gutov.dev>
|
|
|
|
|
;; URL: https://github.com/dgutov/diff-hl
|
|
|
|
|
;; Keywords: vc, diff
|
|
|
|
|
;; Version: 1.9.2
|
|
|
|
|
;; Version: 1.10.0
|
|
|
|
|
;; Package-Requires: ((cl-lib "0.2") (emacs "25.1"))
|
|
|
|
|
|
|
|
|
|
;; This file is part of GNU Emacs.
|
|
|
|
|
@@ -194,8 +194,30 @@ the NEW revision is not specified (meaning, the diff is against
|
|
|
|
|
the current version of the file)."
|
|
|
|
|
:type 'boolean)
|
|
|
|
|
|
|
|
|
|
(defcustom diff-hl-update-async nil
|
|
|
|
|
"When non-nil, `diff-hl-update' will run asynchronously.
|
|
|
|
|
|
|
|
|
|
This can help prevent Emacs from freezing, especially by a slow version
|
|
|
|
|
control (VC) backend. It's disabled in remote buffers, though, since it
|
|
|
|
|
didn't work reliably in such during testing."
|
|
|
|
|
:type 'boolean)
|
|
|
|
|
|
|
|
|
|
;; Threads are not reliable with remote files, yet.
|
|
|
|
|
(defcustom diff-hl-async-inhibit-functions (list #'diff-hl-with-editor-p
|
|
|
|
|
#'file-remote-p)
|
|
|
|
|
"Functions to call to check whether asychronous method should be disabled.
|
|
|
|
|
|
|
|
|
|
When `diff-hl-update-async' is non-nil, these functions are called in turn
|
|
|
|
|
and passed the value `default-directory'.
|
|
|
|
|
|
|
|
|
|
If any returns non-nil, `diff-hl-update' will run synchronously anyway."
|
|
|
|
|
:type '(repeat :tag "Predicate" function))
|
|
|
|
|
|
|
|
|
|
(defvar diff-hl-reference-revision nil
|
|
|
|
|
"Revision to diff against. nil means the most recent one.")
|
|
|
|
|
"Revision to diff against. nil means the most recent one.
|
|
|
|
|
|
|
|
|
|
It can be a relative expression as well, such as \"HEAD^\" with Git, or
|
|
|
|
|
\"-2\" with Mercurial.")
|
|
|
|
|
|
|
|
|
|
(defun diff-hl-define-bitmaps ()
|
|
|
|
|
(let* ((scale (if (and (boundp 'text-scale-mode-amount)
|
|
|
|
|
@@ -309,6 +331,8 @@ the current version of the file)."
|
|
|
|
|
diff-hl-reference-revision))))
|
|
|
|
|
|
|
|
|
|
(declare-function vc-git-command "vc-git")
|
|
|
|
|
(declare-function vc-git--rev-parse "vc-git")
|
|
|
|
|
(declare-function vc-hg-command "vc-hg")
|
|
|
|
|
|
|
|
|
|
(defun diff-hl-changes-buffer (file backend)
|
|
|
|
|
(diff-hl-with-diff-switches
|
|
|
|
|
@@ -384,6 +408,28 @@ the current version of the file)."
|
|
|
|
|
(nreverse res))))
|
|
|
|
|
|
|
|
|
|
(defun diff-hl-update ()
|
|
|
|
|
"Updates the diff-hl overlay."
|
|
|
|
|
(if (and diff-hl-update-async
|
|
|
|
|
(not
|
|
|
|
|
(run-hook-with-args-until-success 'diff-hl-async-inhibit-functions
|
|
|
|
|
default-directory)))
|
|
|
|
|
;; TODO: debounce if a thread is already running.
|
|
|
|
|
(make-thread 'diff-hl--update-safe "diff-hl--update-safe")
|
|
|
|
|
(diff-hl--update)))
|
|
|
|
|
|
|
|
|
|
(defun diff-hl-with-editor-p (_dir)
|
|
|
|
|
(bound-and-true-p with-editor-mode))
|
|
|
|
|
|
|
|
|
|
(defun diff-hl--update-safe ()
|
|
|
|
|
"Updates the diff-hl overlay. It handles and logs when an error is signaled."
|
|
|
|
|
(condition-case err
|
|
|
|
|
(diff-hl--update)
|
|
|
|
|
(error
|
|
|
|
|
(message "An error occurred in diff-hl--update: %S" err)
|
|
|
|
|
nil)))
|
|
|
|
|
|
|
|
|
|
(defun diff-hl--update ()
|
|
|
|
|
"Updates the diff-hl overlay."
|
|
|
|
|
(let ((changes (diff-hl-changes))
|
|
|
|
|
(current-line 1))
|
|
|
|
|
(diff-hl-remove-overlays)
|
|
|
|
|
@@ -460,13 +506,13 @@ the current version of the file)."
|
|
|
|
|
(run-with-idle-timer 0.01 nil #'diff-hl-after-undo (current-buffer)))))
|
|
|
|
|
|
|
|
|
|
(defun diff-hl-after-undo (buffer)
|
|
|
|
|
(with-current-buffer buffer
|
|
|
|
|
(unless (buffer-modified-p)
|
|
|
|
|
(diff-hl-update))))
|
|
|
|
|
(when (buffer-live-p buffer)
|
|
|
|
|
(with-current-buffer buffer
|
|
|
|
|
(unless (buffer-modified-p)
|
|
|
|
|
(diff-hl-update)))))
|
|
|
|
|
|
|
|
|
|
(defun diff-hl-after-revert ()
|
|
|
|
|
(defvar revert-buffer-preserve-modes)
|
|
|
|
|
(when revert-buffer-preserve-modes
|
|
|
|
|
(when (bound-and-true-p revert-buffer-preserve-modes)
|
|
|
|
|
(diff-hl-update)))
|
|
|
|
|
|
|
|
|
|
(defun diff-hl-diff-goto-hunk-1 (historic)
|
|
|
|
|
@@ -708,6 +754,21 @@ its end position."
|
|
|
|
|
(unless (eq backend 'Git)
|
|
|
|
|
(user-error "Only Git supports staging; this file is controlled by %s" backend))))
|
|
|
|
|
|
|
|
|
|
(defun diff-hl-stage-diff (orig-buffer)
|
|
|
|
|
(let ((patchfile (make-temp-file "diff-hl-stage-patch"))
|
|
|
|
|
success)
|
|
|
|
|
(write-region (point-min) (point-max) patchfile
|
|
|
|
|
nil 'silent)
|
|
|
|
|
(unwind-protect
|
|
|
|
|
(with-current-buffer orig-buffer
|
|
|
|
|
(with-output-to-string
|
|
|
|
|
(vc-git-command standard-output 0
|
|
|
|
|
patchfile
|
|
|
|
|
"apply" "--cached" )
|
|
|
|
|
(setq success t)))
|
|
|
|
|
(delete-file patchfile))
|
|
|
|
|
success))
|
|
|
|
|
|
|
|
|
|
(defun diff-hl-stage-current-hunk ()
|
|
|
|
|
"Stage the hunk at or near point.
|
|
|
|
|
|
|
|
|
|
@@ -741,17 +802,7 @@ Only supported with Git."
|
|
|
|
|
(insert (format "diff a/%s b/%s\n" file-base file-base))
|
|
|
|
|
(insert (format "--- a/%s\n" file-base))
|
|
|
|
|
(insert (format "+++ b/%s\n" file-base)))
|
|
|
|
|
(let ((patchfile (make-temp-file "diff-hl-stage-patch")))
|
|
|
|
|
(write-region (point-min) (point-max) patchfile
|
|
|
|
|
nil 'silent)
|
|
|
|
|
(unwind-protect
|
|
|
|
|
(with-current-buffer orig-buffer
|
|
|
|
|
(with-output-to-string
|
|
|
|
|
(vc-git-command standard-output 0
|
|
|
|
|
patchfile
|
|
|
|
|
"apply" "--cached" ))
|
|
|
|
|
(setq success t))
|
|
|
|
|
(delete-file patchfile))))
|
|
|
|
|
(setq success (diff-hl-stage-diff orig-buffer)))
|
|
|
|
|
(when success
|
|
|
|
|
(if diff-hl-show-staged-changes
|
|
|
|
|
(message (concat "Hunk staged; customize `diff-hl-show-staged-changes'"
|
|
|
|
|
@@ -773,6 +824,85 @@ Only supported with Git."
|
|
|
|
|
(unless diff-hl-show-staged-changes
|
|
|
|
|
(diff-hl-update)))
|
|
|
|
|
|
|
|
|
|
(defun diff-hl-stage-dwim (&optional with-edit)
|
|
|
|
|
"Stage the current hunk or choose the hunks to stage.
|
|
|
|
|
When called with the prefix argument, invokes `diff-hl-stage-some'."
|
|
|
|
|
(interactive "P")
|
|
|
|
|
(if (or with-edit (region-active-p))
|
|
|
|
|
(call-interactively #'diff-hl-stage-some)
|
|
|
|
|
(call-interactively #'diff-hl-stage-current-hunk)))
|
|
|
|
|
|
|
|
|
|
(defvar diff-hl-stage--orig nil)
|
|
|
|
|
|
|
|
|
|
(define-derived-mode diff-hl-stage-diff-mode diff-mode "Stage Diff"
|
|
|
|
|
"Major mode for editing a diff buffer before staging.
|
|
|
|
|
|
|
|
|
|
\\[diff-hl-stage-commit]"
|
|
|
|
|
(setq revert-buffer-function #'ignore))
|
|
|
|
|
|
|
|
|
|
(define-key diff-hl-stage-diff-mode-map (kbd "C-c C-c") #'diff-hl-stage-finish)
|
|
|
|
|
|
|
|
|
|
(defun diff-hl-stage-some (&optional beg end)
|
|
|
|
|
"Stage some or all of the current changes, interactively.
|
|
|
|
|
Pops up a diff buffer that can be edited to choose the changes to stage."
|
|
|
|
|
(interactive "r")
|
|
|
|
|
(diff-hl--ensure-staging-supported)
|
|
|
|
|
(let* ((line-beg (and beg (line-number-at-pos beg t)))
|
|
|
|
|
(line-end (and end (line-number-at-pos end t)))
|
|
|
|
|
(file buffer-file-name)
|
|
|
|
|
(dest-buffer (get-buffer-create "*diff-hl-stage-some*"))
|
|
|
|
|
(orig-buffer (current-buffer))
|
|
|
|
|
;; FIXME: If the file name has double quotes, these need to be quoted.
|
|
|
|
|
(file-base (file-name-nondirectory file)))
|
|
|
|
|
(with-current-buffer dest-buffer
|
|
|
|
|
(let ((inhibit-read-only t))
|
|
|
|
|
(erase-buffer)))
|
|
|
|
|
(diff-hl-diff-buffer-with-reference file dest-buffer nil 3)
|
|
|
|
|
(with-current-buffer dest-buffer
|
|
|
|
|
(let ((inhibit-read-only t))
|
|
|
|
|
(when end
|
|
|
|
|
(with-no-warnings
|
|
|
|
|
(let (diff-auto-refine-mode)
|
|
|
|
|
(diff-hl-diff-skip-to line-end)
|
|
|
|
|
(diff-hl-split-away-changes 3)
|
|
|
|
|
(diff-end-of-hunk)))
|
|
|
|
|
(delete-region (point) (point-max)))
|
|
|
|
|
(if beg
|
|
|
|
|
(with-no-warnings
|
|
|
|
|
(let (diff-auto-refine-mode)
|
|
|
|
|
(diff-hl-diff-skip-to line-beg)
|
|
|
|
|
(diff-hl-split-away-changes 3)
|
|
|
|
|
(diff-beginning-of-hunk)))
|
|
|
|
|
(goto-char (point-min))
|
|
|
|
|
(forward-line 3))
|
|
|
|
|
(delete-region (point-min) (point))
|
|
|
|
|
;; diff-no-select creates a very ugly header; Git rejects it
|
|
|
|
|
(insert (format "diff a/%s b/%s\n" file-base file-base))
|
|
|
|
|
(insert (format "--- a/%s\n" file-base))
|
|
|
|
|
(insert (format "+++ b/%s\n" file-base)))
|
|
|
|
|
(let ((diff-default-read-only t))
|
|
|
|
|
(diff-hl-stage-diff-mode))
|
|
|
|
|
(setq-local diff-hl-stage--orig orig-buffer))
|
|
|
|
|
(pop-to-buffer dest-buffer)
|
|
|
|
|
(message "Press %s and %s to navigate, %s to split, %s to kill hunk, %s to undo, and %s to stage the diff after editing"
|
|
|
|
|
(substitute-command-keys "\\`n'")
|
|
|
|
|
(substitute-command-keys "\\`p'")
|
|
|
|
|
(substitute-command-keys "\\[diff-split-hunk]")
|
|
|
|
|
(substitute-command-keys "\\[diff-hunk-kill]")
|
|
|
|
|
(substitute-command-keys "\\[diff-undo]")
|
|
|
|
|
(substitute-command-keys "\\[diff-hl-stage-finish]"))))
|
|
|
|
|
|
|
|
|
|
(defun diff-hl-stage-finish ()
|
|
|
|
|
(interactive)
|
|
|
|
|
(let ((count 0))
|
|
|
|
|
(when (diff-hl-stage-diff diff-hl-stage--orig)
|
|
|
|
|
(save-excursion
|
|
|
|
|
(goto-char (point-min))
|
|
|
|
|
(while (re-search-forward diff-hunk-header-re-unified nil t)
|
|
|
|
|
(cl-incf count)))
|
|
|
|
|
(message "Staged %d hunks" count)
|
|
|
|
|
(bury-buffer))))
|
|
|
|
|
|
|
|
|
|
(defvar diff-hl-command-map
|
|
|
|
|
(let ((map (make-sparse-keymap)))
|
|
|
|
|
(define-key map "n" 'diff-hl-revert-hunk)
|
|
|
|
|
@@ -781,7 +911,7 @@ Only supported with Git."
|
|
|
|
|
(define-key map "*" 'diff-hl-show-hunk)
|
|
|
|
|
(define-key map "{" 'diff-hl-show-hunk-previous)
|
|
|
|
|
(define-key map "}" 'diff-hl-show-hunk-next)
|
|
|
|
|
(define-key map "S" 'diff-hl-stage-current-hunk)
|
|
|
|
|
(define-key map "S" 'diff-hl-stage-dwim)
|
|
|
|
|
map))
|
|
|
|
|
(fset 'diff-hl-command-map diff-hl-command-map)
|
|
|
|
|
|
|
|
|
|
@@ -827,7 +957,7 @@ The value of this variable is a mode line template as in
|
|
|
|
|
(remove-hook 'after-save-hook 'diff-hl-update t)
|
|
|
|
|
(remove-hook 'after-change-functions 'diff-hl-edit t)
|
|
|
|
|
(remove-hook 'find-file-hook 'diff-hl-update t)
|
|
|
|
|
(remove-hook 'after-revert-hook 'diff-hl-update t)
|
|
|
|
|
(remove-hook 'after-revert-hook 'diff-hl-after-revert t)
|
|
|
|
|
(remove-hook 'magit-revert-buffer-hook 'diff-hl-update t)
|
|
|
|
|
(remove-hook 'magit-not-reverted-hook 'diff-hl-update t)
|
|
|
|
|
(remove-hook 'text-scale-mode-hook 'diff-hl-maybe-redefine-bitmaps t)
|
|
|
|
|
@@ -871,49 +1001,38 @@ The value of this variable is a mode line template as in
|
|
|
|
|
diff-hl-command-map)
|
|
|
|
|
|
|
|
|
|
(declare-function magit-toplevel "magit-git")
|
|
|
|
|
(declare-function magit-unstaged-files "magit-git")
|
|
|
|
|
(declare-function magit-git-items "magit-git")
|
|
|
|
|
|
|
|
|
|
(defvar diff-hl--magit-unstaged-files nil)
|
|
|
|
|
|
|
|
|
|
(defun diff-hl-magit-pre-refresh ()
|
|
|
|
|
(unless (and diff-hl-disable-on-remote
|
|
|
|
|
(file-remote-p default-directory))
|
|
|
|
|
(setq diff-hl--magit-unstaged-files (magit-unstaged-files t))))
|
|
|
|
|
(define-obsolete-function-alias 'diff-hl-magit-pre-refresh 'ignore "1.11.0")
|
|
|
|
|
|
|
|
|
|
(defun diff-hl-magit-post-refresh ()
|
|
|
|
|
(unless (and diff-hl-disable-on-remote
|
|
|
|
|
(file-remote-p default-directory))
|
|
|
|
|
(let* ((topdir (magit-toplevel))
|
|
|
|
|
(modified-files
|
|
|
|
|
(mapcar (lambda (file) (expand-file-name file topdir))
|
|
|
|
|
(delete-consecutive-dups
|
|
|
|
|
(sort
|
|
|
|
|
(nconc (magit-unstaged-files t)
|
|
|
|
|
diff-hl--magit-unstaged-files)
|
|
|
|
|
#'string<))))
|
|
|
|
|
(unmodified-states '(up-to-date ignored unregistered)))
|
|
|
|
|
(setq diff-hl--magit-unstaged-files nil)
|
|
|
|
|
(dolist (buf (buffer-list))
|
|
|
|
|
(when (and (buffer-local-value 'diff-hl-mode buf)
|
|
|
|
|
(not (buffer-modified-p buf))
|
|
|
|
|
;; Solve the "cloned indirect buffer" problem
|
|
|
|
|
;; (diff-hl-mode could be non-nil there, even if
|
|
|
|
|
;; buffer-file-name is nil):
|
|
|
|
|
(buffer-file-name buf)
|
|
|
|
|
(file-in-directory-p (buffer-file-name buf) topdir)
|
|
|
|
|
(file-exists-p (buffer-file-name buf)))
|
|
|
|
|
(with-current-buffer buf
|
|
|
|
|
(let* ((file buffer-file-name)
|
|
|
|
|
(backend (vc-backend file)))
|
|
|
|
|
(when backend
|
|
|
|
|
(cond
|
|
|
|
|
((member file modified-files)
|
|
|
|
|
(when (memq (vc-state file) unmodified-states)
|
|
|
|
|
(vc-state-refresh file backend))
|
|
|
|
|
(diff-hl-update))
|
|
|
|
|
((not (memq (vc-state file backend) unmodified-states))
|
|
|
|
|
(vc-state-refresh file backend)
|
|
|
|
|
(diff-hl-update)))))))))))
|
|
|
|
|
(let* ((topdir (magit-toplevel))
|
|
|
|
|
(modified-files
|
|
|
|
|
(magit-git-items "diff-tree" "-z" "--name-only" "-r" "HEAD~" "HEAD"))
|
|
|
|
|
(unmodified-states '(up-to-date ignored unregistered)))
|
|
|
|
|
(dolist (buf (buffer-list))
|
|
|
|
|
(when (and (buffer-local-value 'diff-hl-mode buf)
|
|
|
|
|
(not (buffer-modified-p buf))
|
|
|
|
|
;; Solve the "cloned indirect buffer" problem
|
|
|
|
|
;; (diff-hl-mode could be non-nil there, even if
|
|
|
|
|
;; buffer-file-name is nil):
|
|
|
|
|
(buffer-file-name buf)
|
|
|
|
|
(file-in-directory-p (buffer-file-name buf) topdir)
|
|
|
|
|
(file-exists-p (buffer-file-name buf)))
|
|
|
|
|
(with-current-buffer buf
|
|
|
|
|
(let* ((file buffer-file-name)
|
|
|
|
|
(backend (vc-backend file)))
|
|
|
|
|
(when backend
|
|
|
|
|
(cond
|
|
|
|
|
((member file modified-files)
|
|
|
|
|
(when (memq (vc-state file) unmodified-states)
|
|
|
|
|
(vc-state-refresh file backend))
|
|
|
|
|
(diff-hl-update))
|
|
|
|
|
((not (memq (vc-state file backend) unmodified-states))
|
|
|
|
|
(vc-state-refresh file backend)
|
|
|
|
|
(diff-hl-update)))))))))))
|
|
|
|
|
|
|
|
|
|
(defun diff-hl-dir-update ()
|
|
|
|
|
(dolist (pair (if (vc-dir-marked-files)
|
|
|
|
|
@@ -988,7 +1107,7 @@ CONTEXT-LINES is the size of the unified diff context, defaults to 0."
|
|
|
|
|
(let* ((dest-buffer (or dest-buffer "*diff-hl-diff-buffer-with-reference*"))
|
|
|
|
|
(backend (or backend (vc-backend file)))
|
|
|
|
|
(temporary-file-directory
|
|
|
|
|
(if (file-directory-p "/dev/shm/")
|
|
|
|
|
(if (and (eq system-type 'gnu/linux) (file-directory-p "/dev/shm/"))
|
|
|
|
|
"/dev/shm/"
|
|
|
|
|
temporary-file-directory))
|
|
|
|
|
(rev
|
|
|
|
|
@@ -1000,7 +1119,7 @@ CONTEXT-LINES is the size of the unified diff context, defaults to 0."
|
|
|
|
|
(diff-hl-git-index-object-name file))
|
|
|
|
|
(diff-hl-create-revision
|
|
|
|
|
file
|
|
|
|
|
(or diff-hl-reference-revision
|
|
|
|
|
(or (diff-hl-resolved-reference-revision backend)
|
|
|
|
|
(diff-hl-working-revision file backend)))))
|
|
|
|
|
(switches (format "-U %d --strip-trailing-cr" (or context-lines 0))))
|
|
|
|
|
(diff-no-select rev (current-buffer) switches 'noasync
|
|
|
|
|
@@ -1011,18 +1130,34 @@ CONTEXT-LINES is the size of the unified diff context, defaults to 0."
|
|
|
|
|
(delete-matching-lines "^Diff finished.*")))
|
|
|
|
|
(get-buffer-create dest-buffer))))
|
|
|
|
|
|
|
|
|
|
(defun diff-hl-resolved-reference-revision (backend)
|
|
|
|
|
(cond
|
|
|
|
|
((null diff-hl-reference-revision)
|
|
|
|
|
nil)
|
|
|
|
|
((eq backend 'Git)
|
|
|
|
|
(vc-git--rev-parse diff-hl-reference-revision))
|
|
|
|
|
((eq backend 'Hg)
|
|
|
|
|
(with-temp-buffer
|
|
|
|
|
(vc-hg-command (current-buffer) 0 nil
|
|
|
|
|
"identify" "-r" diff-hl-reference-revision
|
|
|
|
|
"-i")
|
|
|
|
|
(goto-char (point-min))
|
|
|
|
|
(buffer-substring-no-properties (point) (line-end-position))))
|
|
|
|
|
(t
|
|
|
|
|
diff-hl-reference-revision)))
|
|
|
|
|
|
|
|
|
|
;; TODO: Cache based on .git/index's mtime, maybe.
|
|
|
|
|
(defun diff-hl-git-index-object-name (file)
|
|
|
|
|
(with-temp-buffer
|
|
|
|
|
(vc-git-command (current-buffer) 0 file "ls-files" "-s")
|
|
|
|
|
(and
|
|
|
|
|
(goto-char (point-min))
|
|
|
|
|
(re-search-forward "^[0-9]+ \\([0-9a-f]+\\)")
|
|
|
|
|
(re-search-forward "^[0-9]+ \\([0-9a-f]+\\)" nil t)
|
|
|
|
|
(match-string-no-properties 1))))
|
|
|
|
|
|
|
|
|
|
(defun diff-hl-git-index-revision (file object-name)
|
|
|
|
|
(let ((filename (diff-hl-make-temp-file-name file
|
|
|
|
|
(concat ":" object-name)
|
|
|
|
|
(concat ";" object-name)
|
|
|
|
|
'manual))
|
|
|
|
|
(filebuf (get-file-buffer file)))
|
|
|
|
|
(unless (file-exists-p filename)
|
|
|
|
|
|