summaryrefslogtreecommitdiff
path: root/lisp/vc/vc-git.el
diff options
context:
space:
mode:
Diffstat (limited to 'lisp/vc/vc-git.el')
-rw-r--r--lisp/vc/vc-git.el217
1 files changed, 153 insertions, 64 deletions
diff --git a/lisp/vc/vc-git.el b/lisp/vc/vc-git.el
index 610cbde7a49..9715aea1fdc 100644
--- a/lisp/vc/vc-git.el
+++ b/lisp/vc/vc-git.el
@@ -74,6 +74,9 @@
;; - steal-lock (file &optional revision) NOT NEEDED
;; HISTORY FUNCTIONS
;; * print-log (files buffer &optional shortlog start-revision limit) OK
+;; * log-outgoing (buffer remote-location) OK
+;; * log-incoming (buffer remote-location) OK
+;; - log-search (buffer pattern) OK
;; - log-view-mode () OK
;; - show-log-entry (revision) OK
;; - comment-history (file) ??
@@ -101,9 +104,9 @@
(eval-when-compile
(require 'cl-lib)
+ (require 'subr-x) ; for string-trim-right
(require 'vc)
- (require 'vc-dir)
- (require 'grep))
+ (require 'vc-dir))
(defgroup vc-git nil
"VC Git backend."
@@ -180,9 +183,22 @@ Should be consistent with the Git config value i18n.logOutputEncoding."
:type '(coding-system :tag "Coding system to decode Git log output")
:version "25.1")
+(defcustom vc-git-grep-template "git --no-pager grep -n <C> -e <R> -- <F>"
+ "The default command to run for \\[vc-git-grep].
+The following place holders should be present in the string:
+ <C> - place to put the options like -i.
+ <F> - file names and wildcards to search.
+ <R> - the regular expression searched for."
+ :type 'string
+ :version "27.1")
+
;; History of Git commands.
(defvar vc-git-history nil)
+;; Clear up the cache to force vc-call to check again and discover
+;; new functions when we reload this file.
+(put 'Git 'vc-functions nil)
+
;;; BACKEND PROPERTIES
(defun vc-git-revision-granularity () 'repository)
@@ -242,7 +258,7 @@ Should be consistent with the Git config value i18n.logOutputEncoding."
;; Git for Windows appends ".windows.N" to the
;; numerical version reported by Git.
(string-match
- "git version \\([0-9.]+\\)\\(\.windows.[0-9]+\\)?$"
+ "git version \\([0-9.]+\\)\\(\\.windows\\.[0-9]+\\)?$"
version-string))
(match-string 1 version-string)
"0")))))
@@ -278,7 +294,7 @@ in the order given by 'git status'."
;; 2. When a file A is renamed to B in the index and then back to A
;; in the working tree.
;; In both of these instances, `unregistered' is a reasonable response.
- (`("D " "??") 'unregistered)
+ ('("D " "??") 'unregistered)
;; In other cases, let us return `edited'.
(_ 'edited)))
@@ -364,8 +380,8 @@ in the order given by 'git status'."
(defun vc-git-file-type-as-string (old-perm new-perm)
"Return a string describing the file type based on its permissions."
- (let* ((old-type (lsh (or old-perm 0) -9))
- (new-type (lsh (or new-perm 0) -9))
+ (let* ((old-type (ash (or old-perm 0) -9))
+ (new-type (ash (or new-perm 0) -9))
(str (pcase new-type
(?\100 ;; File.
(pcase old-type
@@ -475,9 +491,9 @@ or an empty string if none."
(files (vc-git-dir-status-state->files git-state)))
(goto-char (point-min))
(pcase (vc-git-dir-status-state->stage git-state)
- (`update-index
+ ('update-index
(setq next-stage (if (vc-git--empty-db-p) 'ls-files-added 'diff-index)))
- (`ls-files-added
+ ('ls-files-added
(setq next-stage 'ls-files-unknown)
(while (re-search-forward "\\([0-7]\\{6\\}\\) [0-9a-f]\\{40\\} 0\t\\([^\0]+\\)\0" nil t)
(let ((new-perm (string-to-number (match-string 1) 8))
@@ -485,7 +501,7 @@ or an empty string if none."
(vc-git-dir-status-update-file
git-state name 'added
(vc-git-create-extra-fileinfo 0 new-perm)))))
- (`ls-files-up-to-date
+ ('ls-files-up-to-date
(setq next-stage 'ls-files-unknown)
(while (re-search-forward "\\([0-7]\\{6\\}\\) [0-9a-f]\\{40\\} \\([0-3]\\)\t\\([^\0]+\\)\0" nil t)
(let ((perm (string-to-number (match-string 1) 8))
@@ -496,7 +512,7 @@ or an empty string if none."
'up-to-date
'conflict)
(vc-git-create-extra-fileinfo perm perm)))))
- (`ls-files-conflict
+ ('ls-files-conflict
(setq next-stage 'ls-files-unknown)
;; It's enough to look for "3" to notice a conflict.
(while (re-search-forward "\\([0-7]\\{6\\}\\) [0-9a-f]\\{40\\} 3\t\\([^\0]+\\)\0" nil t)
@@ -505,16 +521,16 @@ or an empty string if none."
(vc-git-dir-status-update-file
git-state name 'conflict
(vc-git-create-extra-fileinfo perm perm)))))
- (`ls-files-unknown
+ ('ls-files-unknown
(when files (setq next-stage 'ls-files-ignored))
(while (re-search-forward "\\([^\0]*?\\)\0" nil t 1)
(vc-git-dir-status-update-file git-state (match-string 1) 'unregistered
(vc-git-create-extra-fileinfo 0 0))))
- (`ls-files-ignored
+ ('ls-files-ignored
(while (re-search-forward "\\([^\0]*?\\)\0" nil t 1)
(vc-git-dir-status-update-file git-state (match-string 1) 'ignored
(vc-git-create-extra-fileinfo 0 0))))
- (`diff-index
+ ('diff-index
(setq next-stage (if files 'ls-files-up-to-date 'ls-files-conflict))
(while (re-search-forward
":\\([0-7]\\{6\\}\\) \\([0-7]\\{6\\}\\) [0-9a-f]\\{40\\} [0-9a-f]\\{40\\} \\(\\([ADMUT]\\)\0\\([^\0]+\\)\\|\\([CR]\\)[0-9]*\0\\([^\0]+\\)\0\\([^\0]+\\)\\)\0"
@@ -566,30 +582,30 @@ or an empty string if none."
(let ((files (vc-git-dir-status-state->files git-state)))
(erase-buffer)
(pcase (vc-git-dir-status-state->stage git-state)
- (`update-index
+ ('update-index
(if files
(vc-git-command (current-buffer) 'async files "add" "--refresh" "--")
(vc-git-command (current-buffer) 'async nil
"update-index" "--refresh")))
- (`ls-files-added
+ ('ls-files-added
(vc-git-command (current-buffer) 'async files
"ls-files" "-z" "-c" "-s" "--"))
- (`ls-files-up-to-date
+ ('ls-files-up-to-date
(vc-git-command (current-buffer) 'async files
"ls-files" "-z" "-c" "-s" "--"))
- (`ls-files-conflict
+ ('ls-files-conflict
(vc-git-command (current-buffer) 'async files
"ls-files" "-z" "-u" "--"))
- (`ls-files-unknown
+ ('ls-files-unknown
(vc-git-command (current-buffer) 'async files
"ls-files" "-z" "-o" "--directory"
"--no-empty-directory" "--exclude-standard" "--"))
- (`ls-files-ignored
+ ('ls-files-ignored
(vc-git-command (current-buffer) 'async files
"ls-files" "-z" "-o" "-i" "--directory"
"--no-empty-directory" "--exclude-standard" "--"))
;; --relative added in Git 1.5.5.
- (`diff-index
+ ('diff-index
(vc-git-command (current-buffer) 'async files
"diff-index" "--relative" "-z" "-M" "HEAD" "--")))
(vc-run-delayed
@@ -738,7 +754,7 @@ The car of the list is the current branch."
(declare-function log-edit-mode "log-edit" ())
(declare-function log-edit-toggle-header "log-edit" (header value))
(declare-function log-edit-extract-headers "log-edit" (headers string))
-(declare-function log-edit-set-header "log-edit" (header value &optional toggle))
+(declare-function log-edit--toggle-amend "log-edit" (last-msg-fn))
(defun vc-git-log-edit-toggle-signoff ()
"Toggle whether to add the \"Signed-off-by\" line at the end of
@@ -746,31 +762,26 @@ the commit message."
(interactive)
(log-edit-toggle-header "Sign-Off" "yes"))
+(defun vc-git-log-edit-toggle-no-verify ()
+ "Toggle whether to bypass the pre-commit and commit-msg hooks."
+ (interactive)
+ (log-edit-toggle-header "No-Verify" "yes"))
+
(defun vc-git-log-edit-toggle-amend ()
"Toggle whether this will amend the previous commit.
If toggling on, also insert its message into the buffer."
(interactive)
- (when (log-edit-toggle-header "Amend" "yes")
- (goto-char (point-max))
- (unless (bolp) (insert "\n"))
- (insert (with-output-to-string
- (vc-git-command
- standard-output 1 nil
- "log" "--max-count=1" "--pretty=format:%B" "HEAD")))
- (save-excursion
- (rfc822-goto-eoh)
- (forward-line 1)
- (let ((pt (point)))
- (and (zerop (forward-line 1))
- (looking-at "\n\\|\\'")
- (let ((summary (buffer-substring-no-properties pt (1- (point)))))
- (skip-chars-forward " \n")
- (delete-region pt (point))
- (log-edit-set-header "Summary" summary)))))))
+ (log-edit--toggle-amend
+ (lambda ()
+ (with-output-to-string
+ (vc-git-command
+ standard-output 1 nil
+ "log" "--max-count=1" "--pretty=format:%B" "HEAD")))))
(defvar vc-git-log-edit-mode-map
(let ((map (make-sparse-keymap "Git-Log-Edit")))
(define-key map "\C-c\C-s" 'vc-git-log-edit-toggle-signoff)
+ (define-key map "\C-c\C-n" 'vc-git-log-edit-toggle-no-verify)
(define-key map "\C-c\C-e" 'vc-git-log-edit-toggle-amend)
map))
@@ -814,6 +825,7 @@ It is based on `log-edit-mode', and has Git-specific extensions.")
`(("Author" . "--author")
("Date" . "--date")
("Amend" . ,(boolean-arg-fn "--amend"))
+ ("No-Verify" . ,(boolean-arg-fn "--no-verify"))
("Sign-Off" . ,(boolean-arg-fn "--signoff")))
comment)))
(when msg-file
@@ -863,6 +875,8 @@ It is based on `log-edit-mode', and has Git-specific extensions.")
;; To be called via vc-pull from vc.el, which requires vc-dispatcher.
(declare-function vc-compilation-mode "vc-dispatcher" (backend))
+(defvar compilation-directory)
+(defvar compilation-arguments)
(defun vc-git--pushpull (command prompt extra-args)
"Run COMMAND (a string; either push or pull) on the current Git branch.
@@ -997,7 +1011,8 @@ This prompts for a branch to merge from."
If SHORTLOG is non-nil, use a short format based on `vc-git-root-log-format'.
\(This requires at least Git version 1.5.6, for the --graph option.)
If START-REVISION is non-nil, it is the newest revision to show.
-If LIMIT is non-nil, show no more than this many entries."
+If LIMIT is a number, show no more than this many entries.
+If LIMIT is a revision string, use it as an end-revision."
(let ((coding-system-for-read
(or coding-system-for-read vc-git-log-output-coding-system)))
;; `vc-do-command' creates the buffer, but we need it before running
@@ -1025,12 +1040,17 @@ If LIMIT is non-nil, show no more than this many entries."
,(format "--pretty=tformat:%s"
(car vc-git-root-log-format))
"--abbrev-commit"))
- (when limit (list "-n" (format "%s" limit)))
- (when start-revision (list start-revision))
+ (when (numberp limit)
+ (list "-n" (format "%s" limit)))
+ (when start-revision
+ (if (and limit (not (numberp limit)))
+ (list (concat start-revision ".." (if (equal limit "")
+ "HEAD"
+ limit)))
+ (list start-revision)))
'("--")))))))
(defun vc-git-log-outgoing (buffer remote-location)
- (interactive)
(vc-setup-buffer buffer)
(vc-git-command
buffer 'async nil
@@ -1044,7 +1064,6 @@ If LIMIT is non-nil, show no more than this many entries."
"..HEAD")))
(defun vc-git-log-incoming (buffer remote-location)
- (interactive)
(vc-setup-buffer buffer)
(vc-git-command nil 0 nil "fetch")
(vc-git-command
@@ -1057,6 +1076,31 @@ If LIMIT is non-nil, show no more than this many entries."
"@{upstream}"
remote-location))))
+(defun vc-git-log-search (buffer pattern)
+ "Search the log of changes for PATTERN and output results into BUFFER.
+
+PATTERN is a basic regular expression by default in Git.
+
+Display all entries that match log messages in long format.
+With a prefix argument, ask for a command to run that will output
+log entries."
+ (let ((args `("log" "--no-color" "-i"
+ ,(format "--grep=%s" (or pattern "")))))
+ (when current-prefix-arg
+ (setq args (cdr (split-string
+ (read-shell-command
+ "Search log with command: "
+ (format "%s %s" vc-git-program
+ (mapconcat 'identity args " "))
+ 'vc-git-history)
+ " " t))))
+ (vc-setup-buffer buffer)
+ (apply 'vc-git-command buffer 'async nil args)))
+
+(defun vc-git-mergebase (rev1 &optional rev2)
+ (unless rev2 (setq rev2 "HEAD"))
+ (string-trim-right (vc-git--run-command-string nil "merge-base" rev1 rev2)))
+
(defvar log-view-message-re)
(defvar log-view-file-re)
(defvar log-view-font-lock-keywords)
@@ -1066,19 +1110,19 @@ If LIMIT is non-nil, show no more than this many entries."
(define-derived-mode vc-git-log-view-mode log-view-mode "Git-Log-View"
(require 'add-log) ;; We need the faces add-log.
;; Don't have file markers, so use impossible regexp.
- (set (make-local-variable 'log-view-file-re) "\\`a\\`")
+ (set (make-local-variable 'log-view-file-re) regexp-unmatchable)
(set (make-local-variable 'log-view-per-file-logs) nil)
(set (make-local-variable 'log-view-message-re)
- (if (not (eq vc-log-view-type 'long))
+ (if (not (memq vc-log-view-type '(long log-search)))
(cadr vc-git-root-log-format)
"^commit *\\([0-9a-z]+\\)"))
;; Allow expanding short log entries.
- (when (memq vc-log-view-type '(short log-outgoing log-incoming))
+ (when (memq vc-log-view-type '(short log-outgoing log-incoming mergebase))
(setq truncate-lines t)
(set (make-local-variable 'log-view-expanded-log-entry-function)
'vc-git-expanded-log-entry))
(set (make-local-variable 'log-view-font-lock-keywords)
- (if (not (eq vc-log-view-type 'long))
+ (if (not (memq vc-log-view-type '(long log-search)))
(list (cons (nth 1 vc-git-root-log-format)
(nth 2 vc-git-root-log-format)))
(append
@@ -1176,7 +1220,7 @@ This requires git 1.8.4 or later, for the \"-L\" option of \"git log\"."
(defvar vc-git--log-view-long-font-lock-keywords nil)
(defvar font-lock-keywords)
(defvar vc-git-region-history-font-lock-keywords
- `((vc-git-region-history-font-lock)))
+ '((vc-git-region-history-font-lock)))
(defun vc-git-region-history-font-lock (limit)
(let ((in-diff (save-excursion
@@ -1373,6 +1417,9 @@ This requires git 1.8.4 or later, for the \"-L\" option of \"git log\"."
(define-key map [git-grep]
'(menu-item "Git grep..." vc-git-grep
:help "Run the `git grep' command"))
+ (define-key map [git-ds]
+ '(menu-item "Delete Stash..." vc-git-stash-delete
+ :help "Delete a stash"))
(define-key map [git-sn]
'(menu-item "Stash a Snapshot" vc-git-stash-snapshot
:help "Stash the current state of the tree and keep the current state"))
@@ -1397,6 +1444,7 @@ This requires git 1.8.4 or later, for the \"-L\" option of \"git log\"."
(declare-function grep-read-files "grep" (regexp))
(declare-function grep-expand-template "grep"
(template &optional regexp files dir excl))
+(defvar compilation-environment)
;; Derived from `lgrep'.
(defun vc-git-grep (regexp &optional files dir)
@@ -1423,8 +1471,7 @@ This command shares argument histories with \\[rgrep] and \\[grep]."
(cond
((equal current-prefix-arg '(16))
(list (read-from-minibuffer "Run: " "git grep"
- nil nil 'grep-history)
- nil))
+ nil nil 'grep-history)))
(t (let* ((regexp (grep-read-regexp))
(files
(mapconcat #'shell-quote-argument
@@ -1434,13 +1481,15 @@ This command shares argument histories with \\[rgrep] and \\[grep]."
(list regexp files dir))))))
(require 'grep)
(when (and (stringp regexp) (> (length regexp) 0))
+ (unless (and dir (file-accessible-directory-p dir))
+ (setq dir default-directory))
(let ((command regexp))
(if (null files)
(if (string= command "git grep")
(setq command nil))
(setq dir (file-name-as-directory (expand-file-name dir)))
(setq command
- (grep-expand-template "git --no-pager grep -n -e <R> -- <F>"
+ (grep-expand-template vc-git-grep-template
regexp files))
(when command
(if (equal current-prefix-arg '(4))
@@ -1457,17 +1506,36 @@ This command shares argument histories with \\[rgrep] and \\[grep]."
(if (eq next-error-last-buffer (current-buffer))
(setq default-directory dir))))))
+(autoload 'vc-dir-marked-files "vc-dir")
+
(defun vc-git-stash (name)
"Create a stash."
(interactive "sStash name: ")
(let ((root (vc-git-root default-directory)))
(when root
- (vc-git--call nil "stash" "save" name)
+ (apply #'vc-git--call nil "stash" "push" "-m" name
+ (when (derived-mode-p 'vc-dir-mode)
+ (vc-dir-marked-files)))
(vc-resynch-buffer root t t))))
+(defvar vc-git-stash-read-history nil
+ "History for `vc-git-stash-read'.")
+
+(defun vc-git-stash-read (prompt)
+ "Read a Git stash. PROMPT is a string to prompt with."
+ (let ((stash (completing-read
+ prompt
+ (split-string
+ (or (vc-git--run-command-string nil "stash" "list") "") "\n")
+ nil :require-match nil 'vc-git-stash-read-history)))
+ (if (string-equal stash "")
+ (user-error "Not a stash")
+ (string-match "^stash@{[[:digit:]]+}" stash)
+ (match-string 0 stash))))
+
(defun vc-git-stash-show (name)
"Show the contents of stash NAME."
- (interactive "sStash name: ")
+ (interactive (list (vc-git-stash-read "Show stash: ")))
(vc-setup-buffer "*vc-git-stash*")
(vc-git-command "*vc-git-stash*" 'async nil "stash" "show" "-p" name)
(set-buffer "*vc-git-stash*")
@@ -1477,24 +1545,27 @@ This command shares argument histories with \\[rgrep] and \\[grep]."
(defun vc-git-stash-apply (name)
"Apply stash NAME."
- (interactive "sApply stash: ")
+ (interactive (list (vc-git-stash-read "Apply stash: ")))
(vc-git-command "*vc-git-stash*" 0 nil "stash" "apply" "-q" name)
(vc-resynch-buffer (vc-git-root default-directory) t t))
(defun vc-git-stash-pop (name)
"Pop stash NAME."
- (interactive "sPop stash: ")
+ (interactive (list (vc-git-stash-read "Pop stash: ")))
(vc-git-command "*vc-git-stash*" 0 nil "stash" "pop" "-q" name)
(vc-resynch-buffer (vc-git-root default-directory) t t))
+(defun vc-git-stash-delete (name)
+ "Delete stash NAME."
+ (interactive (list (vc-git-stash-read "Delete stash: ")))
+ (vc-git-command "*vc-git-stash*" 0 nil "stash" "drop" "-q" name)
+ (vc-resynch-buffer (vc-git-root default-directory) t t))
+
(defun vc-git-stash-snapshot ()
"Create a stash with the current tree state."
(interactive)
(vc-git--call nil "stash" "save"
- (let ((ct (current-time)))
- (concat
- (format-time-string "Snapshot on %Y-%m-%d" ct)
- (format-time-string " at %H:%M" ct))))
+ (format-time-string "Snapshot on %Y-%m-%d at %H:%M"))
(vc-git-command "*vc-git-stash*" 0 nil "stash" "apply" "-q" "stash@{0}")
(vc-resynch-buffer (vc-git-root default-directory) t t))
@@ -1518,6 +1589,7 @@ This command shares argument histories with \\[rgrep] and \\[grep]."
(declare-function vc-dir-refresh "vc-dir" ())
(defun vc-git-stash-delete-at-point ()
+ "Delete the stash at point."
(interactive)
(let ((stash (vc-git-stash-get-at-point (point))))
(when (y-or-n-p (format "Remove stash %s ? " stash))
@@ -1525,16 +1597,19 @@ This command shares argument histories with \\[rgrep] and \\[grep]."
(vc-dir-refresh))))
(defun vc-git-stash-show-at-point ()
+ "Show the stash at point."
(interactive)
(vc-git-stash-show (format "stash@%s" (vc-git-stash-get-at-point (point)))))
(defun vc-git-stash-apply-at-point ()
+ "Apply the stash at point."
(interactive)
(let (vc-dir-buffers) ; Small optimization.
(vc-git-stash-apply (format "stash@%s" (vc-git-stash-get-at-point (point)))))
(vc-dir-refresh))
(defun vc-git-stash-pop-at-point ()
+ "Pop the stash at point."
(interactive)
(let (vc-dir-buffers) ; Likewise.
(vc-git-stash-pop (format "stash@%s" (vc-git-stash-get-at-point (point)))))
@@ -1555,7 +1630,14 @@ The difference to vc-do-command is that this function always invokes
(or coding-system-for-read vc-git-log-output-coding-system))
(coding-system-for-write
(or coding-system-for-write vc-git-commits-coding-system))
- (process-environment (cons "GIT_DIR" process-environment)))
+ (process-environment
+ (append
+ `("GIT_DIR"
+ ;; Avoid repository locking during background operations
+ ;; (bug#21559).
+ ,@(when revert-buffer-in-progress-p
+ '("GIT_OPTIONAL_LOCKS=0")))
+ process-environment)))
(apply 'vc-do-command (or buffer "*vc*") okstatus vc-git-program
;; https://debbugs.gnu.org/16897
(unless (and (not (cdr-safe file-or-list))
@@ -1575,15 +1657,22 @@ The difference to vc-do-command is that this function always invokes
(defun vc-git--call (buffer command &rest args)
;; We don't need to care the arguments. If there is a file name, it
;; is always a relative one. This works also for remote
- ;; directories. We enable `inhibit-null-byte-detection', otherwise
+ ;; directories. We enable `inhibit-nul-byte-detection', otherwise
;; Tramp's eol conversion might be confused.
- (let ((inhibit-null-byte-detection t)
+ (let ((inhibit-nul-byte-detection t)
(coding-system-for-read
(or coding-system-for-read vc-git-log-output-coding-system))
(coding-system-for-write
(or coding-system-for-write vc-git-commits-coding-system))
- (process-environment (cons "PAGER=" process-environment)))
- (push "GIT_DIR" process-environment)
+ (process-environment
+ (append
+ `("GIT_DIR"
+ "PAGER="
+ ;; Avoid repository locking during background operations
+ ;; (bug#21559).
+ ,@(when revert-buffer-in-progress-p
+ '("GIT_OPTIONAL_LOCKS=0")))
+ process-environment)))
(apply 'process-file vc-git-program nil buffer nil command args)))
(defun vc-git--out-ok (command &rest args)