commit 29c44e61c595ce44301badd8251737fd725fe1d0 Author: Paul Nelson Date: Sun May 3 11:15:19 2026 +0200 Fix VC Git patch check-in for sparse checkouts * lisp/vc/vc-git.el (vc-git--with-temp-index): New macro. Pass the temporary index through 'file-local-name' when setting GIT_INDEX_FILE. (vc-git--stash-staged-changes): Use it. This fixes a Tramp bug: GIT_INDEX_FILE was previously set to the full remote name of the temporary index, which the remote Git process cannot use. (vc-git--checkin): Prepare git apply --3way with a temporary GIT_INDEX_FILE, so that sparse checkout errors from out-of-cone untracked files do not leave the user's index partially modified. (Bug#80951) * test/lisp/vc/vc-git-tests.el: Require log-edit. (vc-git-test-checkin-patch-sparse-checkout): New test. diff --git a/lisp/vc/vc-git.el b/lisp/vc/vc-git.el index 312abcbb975..83e624e618c 100644 --- a/lisp/vc/vc-git.el +++ b/lisp/vc/vc-git.el @@ -1296,6 +1296,21 @@ It is based on `log-edit-mode', and has Git-specific extensions." (file-local-name ,temp))) (delete-file ,temp)))) +(defmacro vc-git--with-temp-index (&rest body) + "Execute BODY with a temporary Git index file. +After executing, delete the temporary index. The index starts empty; +callers may populate it with, e.g., \\='git read-tree HEAD\\='." + (declare (indent 0) (debug t)) + (cl-with-gensyms (index) + `(let ((,index (make-nearby-temp-file "git-index"))) + (unwind-protect + ;; Use `file-local-name' to strip the TRAMP prefix + ;; from the index. + (with-environment-variables + (("GIT_INDEX_FILE" (file-local-name ,index))) + ,@body) + (delete-file ,index))))) + (defalias 'vc-git-async-checkins #'always) (defalias 'vc-git-working-revision-symbol (cl-constantly "HEAD")) @@ -1418,35 +1433,35 @@ It is an error to supply both or neither." ;; ;; 'git apply --3way --ours' is the way Git provides to ;; achieve this. This requires that the index match the - ;; working tree and also implies the --index option, which - ;; means applying the changes to the index in addition to - ;; the working tree. These are both okay here because - ;; before doing this we know the index is empty (we just - ;; committed) and so we can just make use of it and reset - ;; afterwards. + ;; working tree and also implies the --index option. Use a + ;; temporary index for that. (when (and patch-string (not (string-empty-p patch-string))) - (vc-git-command nil 0 nil "add" "--all") - (with-temp-buffer - (vc-git--with-apply-temp (patch t 1 "--3way") - (with-temp-file patch - (insert patch-string))) - ;; We could delete the following if we could also pass - ;; --ours to git-apply, but that is only available in - ;; recent versions of Git. --3way is much older. - (cl-loop - initially (goto-char (point-min)) - ;; git-apply doesn't apply Git's usual quotation and - ;; escape rules for printing file names so we can do - ;; this simple regexp processing. - ;; (Passing -z does not affect the relevant output.) - while (re-search-forward "^U " nil t) - collect (buffer-substring-no-properties (point) - (pos-eol)) - into paths - finally (when paths - (vc-git-command nil 0 paths - "checkout" "--ours")))) - (vc-git-command nil 0 nil "reset")) + (vc-git--with-temp-index + (vc-git-command nil 0 nil "read-tree" "HEAD") + ;; This index is scratch for 'git apply --3way', + ;; so ignore nonzero exit status (e.g., due to + ;; out-of-cone files in a sparse checkout). + (vc-git-command nil t nil "add" "--all") + (with-temp-buffer + (vc-git--with-apply-temp (patch t 1 "--3way") + (with-temp-file patch + (insert patch-string))) + ;; We could delete the following if we could also pass + ;; --ours to git-apply, but that is only available in + ;; recent versions of Git. --3way is much older. + (cl-loop + initially (goto-char (point-min)) + ;; git-apply doesn't apply Git's usual quotation and + ;; escape rules for printing file names so we can do + ;; this simple regexp processing. + ;; (Passing -z does not affect the relevant output.) + while (re-search-forward "^U " nil t) + collect (buffer-substring-no-properties (point) + (pos-eol)) + into paths + finally (when paths + (vc-git-command nil 0 paths + "checkout" "--ours")))))) (when to-stash (vc-git--with-apply-temp (cached) (with-temp-file cached @@ -1589,19 +1604,11 @@ REV is ignored." (progn (with-temp-file cached (vc-git-command t 0 files "diff" "--cached" "--")) - (let* ((index (make-nearby-temp-file "git-index")) - (process-environment - (cons (format "GIT_INDEX_FILE=%s" index) - process-environment))) - (unwind-protect - (progn - (vc-git-command nil 0 nil "read-tree" "HEAD") - ;; See `vc-git--with-apply-temp' - ;; regarding use of `file-local-name'. - (vc-git-command nil 0 nil "apply" "--cached" - (file-local-name cached)) - (setq tree (git-string "write-tree"))) - (delete-file index)))) + (vc-git--with-temp-index + (vc-git-command nil 0 nil "read-tree" "HEAD") + (vc-git-command nil 0 nil "apply" "--cached" + (file-local-name cached)) + (setq tree (git-string "write-tree")))) (delete-file cached)) ;; Prepare stash commit object, which has a special structure. (let* ((tree-commit (git-string "commit-tree" "-m" message diff --git a/test/lisp/vc/vc-git-tests.el b/test/lisp/vc/vc-git-tests.el index 96fa3d65c05..6a6de5707f2 100644 --- a/test/lisp/vc/vc-git-tests.el +++ b/test/lisp/vc/vc-git-tests.el @@ -25,6 +25,7 @@ ;;; Code: (require 'ert-x) +(require 'log-edit) (require 'vc) (require 'vc-dir) (require 'vc-git) @@ -232,7 +233,6 @@ is absent." (ert-deftest vc-git-test-checkin-patch-staged-diff () "Checking in a patch that matches staged changes should not error." (skip-unless (executable-find vc-git-program)) - (require 'log-edit) (vc-git-test--with-repo repo (vc-git-test--start-branch) (write-region "Hello\n" nil "README") @@ -242,4 +242,41 @@ is absent." (should (equal (string-trim (vc-git-test--run "log" "-1" "--pretty=%s")) "Second")))) +(ert-deftest vc-git-test-checkin-patch-sparse-checkout () + "Check in a single-file patch in a sparse checkout. +The patch should be committed and the user's index left untouched, even +when an untracked file lies outside the sparse-checkout cone. +Regression test for bug#80951." + (skip-unless (executable-find vc-git-program)) + ;; `sparse-checkout' was introduced in Git 2.25. + (skip-unless (version<= "2.25" (vc-git--program-version))) + (vc-git-test--with-repo repo + (write-region "1\n" nil "tracked.txt") + (write-region "a\n" nil "other.txt") + (vc-git-test--run "add" "tracked.txt" "other.txt") + (vc-git-test--run "commit" "-m" "Initial") + ;; Restrict the sparse checkout to the two committed files, so that + ;; any other path is "outside the cone". + (vc-git-test--run "sparse-checkout" "init" "--no-cone") + (vc-git-test--run "sparse-checkout" "set" "/tracked.txt" "/other.txt") + ;; scratch.txt is untracked and outside the cone: this is what makes + ;; `git add --all' exit non-zero. + (write-region "do not add me\n" nil "scratch.txt") + ;; Modify both in-cone files, but only check in tracked.txt. + (write-region "2\n" nil "tracked.txt") + (write-region "b\n" nil "other.txt") + (let ((patch (vc-git-test--run "diff" "--" "tracked.txt")) + vc-async-checkin) + ;; Before the fix, this signaled an error from `git add --all'. + (vc-git--checkin "Update tracked" nil patch)) + ;; Check that the selected patch was committed. + (should (equal (string-trim (vc-git-test--run "log" "-1" "--pretty=%s")) + "Update tracked")) + ;; Check that nothing was left staged in the user's index. + (should (zerop (vc-git-command nil t nil "diff" "--cached" "--quiet"))) + ;; Check that other.txt remains modified in the worktree, while + ;; scratch.txt is untracked. + (should (equal (vc-git-test--run "status" "--short") + " M other.txt\n?? scratch.txt\n")))) + ;;; vc-git-tests.el ends here commit b49da1b7e0e639092edff2959720b4df4b86fcc2 Author: Sean Whitton Date: Fri Jun 19 16:01:44 2026 +0100 vc-git, vc-hg log outgoing: Avoid unnecessary mergebase calls * lisp/vc/vc.el (vc-standard-log-outgoing): New function. (vc-default-log-outgoing): * lisp/vc/vc-git.el (vc-git-log-outgoing): * lisp/vc/vc-hg.el (vc-hg-log-outgoing): Use it. diff --git a/lisp/vc/vc-git.el b/lisp/vc/vc-git.el index e68945cda8d..312abcbb975 100644 --- a/lisp/vc/vc-git.el +++ b/lisp/vc/vc-git.el @@ -909,6 +909,11 @@ them one-by-one, accepting the first that has an upstream.)" (when-let* ((upstream (branch-upstream target))) (throw 'ret upstream)))))))))) +(declare-function vc-standard-log-outgoing "vc") + +(defun vc-git-log-outgoing (buffer upstream-location) + (vc-standard-log-outgoing 'Git buffer upstream-location 'skip-mergebase)) + (defun vc-git-dir--branch-headers () "Return headers for branch-related information." (let ((branch (vc-git-working-branch)) diff --git a/lisp/vc/vc-hg.el b/lisp/vc/vc-hg.el index 0fa22d97fad..c00723af102 100644 --- a/lisp/vc/vc-hg.el +++ b/lisp/vc/vc-hg.el @@ -2015,6 +2015,11 @@ This is based on the following assumptions: (ii) there is only one remote head for the current branch." (cdr (assq 'branch (vc-hg--working-branch)))) +(declare-function vc-standard-log-outgoing "vc") + +(defun vc-hg-log-outgoing (buffer upstream-location) + (vc-standard-log-outgoing 'Hg buffer upstream-location 'skip-mergebase)) + (provide 'vc-hg) ;;; vc-hg.el ends here diff --git a/lisp/vc/vc.el b/lisp/vc/vc.el index ddac5724f63..46715d01693 100644 --- a/lisp/vc/vc.el +++ b/lisp/vc/vc.el @@ -460,17 +460,21 @@ ;; revision shown, rather than the working revision, which is normally ;; the case). Not all backends support this. ;; -;; - log-outgoing (buffer upstream-location) (DEPRECATED) +;; - log-outgoing (buffer upstream-location) (SOFT DEPRECATED) ;; ;; Insert in BUFFER the revision log for the changes that will be ;; sent when performing a push operation to UPSTREAM-LOCATION. -;; Deprecated: implement incoming-revision and mergebase instead. +;; Deprecated: implement incoming-revision and mergebase instead, +;; unless what `vc-default-log-outgoing' does with those is too slow +;; for this backend. ;; -;; - log-incoming (buffer upstream-location) (DEPRECATED) +;; - log-incoming (buffer upstream-location) (SOFT DEPRECATED) ;; ;; Insert in BUFFER the revision log for the changes that will be ;; received when performing a pull operation from UPSTREAM-LOCATION. -;; Deprecated: implement incoming-revision and mergebase instead. +;; Deprecated: implement incoming-revision and mergebase instead, +;; unless what `vc-default-log-incoming' does with those is too slow +;; for this backend. ;; ;; * incoming-revision (&optional upstream-location refresh) ;; @@ -4710,11 +4714,25 @@ can be a remote branch name." "31.1") (defun vc-default-log-outgoing (backend buffer upstream-location) + (vc-standard-log-outgoing backend buffer upstream-location nil)) + +(defun vc-standard-log-outgoing + (backend buffer upstream-location &optional skip-mergebase) + "VC `log-outgoing' in terms of `incoming-revision' and `mergebase'. +BACKEND is the VC backend, BUFFER is the buffer to log to, +UPSTREAM-LOCATION is the place to which the changes are outgoing. +Optional argument SKIP-MERGEBASE, if non-nil, skips calling `mergebase' +and instead passes `incoming-revision' directly as the log limit. +For some backends this is equivalent, and saves running one external +command. Whether this equivalence holds depends on the details of the +`print-log' implementation for BACKEND when `vc-log-view-types' contains +`log-outgoing'." (let ((incoming (vc--incoming-revision backend upstream-location)) (default-directory (vc-root-dir backend))) (vc-call-backend backend 'print-log (list default-directory) buffer t "" - (vc-call-backend backend 'mergebase incoming)))) + (if skip-mergebase incoming + (vc-call-backend backend 'mergebase incoming))))) ;;;###autoload (defun vc-log-search (pattern) commit 08f02cbae08ccd945fc09d48c3ee02dce628bee8 Author: Sean Whitton Date: Fri Jun 19 15:50:32 2026 +0100 VC-Dir shows key binding hints * lisp/vc/vc-dir.el (vc-dir-key-binding-hint-label): New defcustom. (vc-dir-headers): Show key binding hints (bug#81190). * etc/NEWS: Document the change. diff --git a/etc/NEWS b/etc/NEWS index 1308c3eda28..672e8b62971 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -109,6 +109,11 @@ This means it won't get in your way even if it's slow for your repository. As such, the 'vc-dir-show-outgoing-count' option is now obsolete. +--- +*** VC-Dir now shows key binding hints. +To hide these, you can customize the new user option +'vc-dir-show-key-binding-hints' to nil. + ** Outline mode *** New command 'outline-show-entry-and-parents'. diff --git a/lisp/vc/vc-dir.el b/lisp/vc/vc-dir.el index a7c6dc9eb01..151cb676838 100644 --- a/lisp/vc/vc-dir.el +++ b/lisp/vc/vc-dir.el @@ -64,6 +64,11 @@ See `run-hooks'." :group 'vc :version "28.1") +(defface vc-dir-key-binding-hint-label '((t :inherit font-lock-builtin-face)) + "Face for key binding labels in VC-Dir buffers." + :group 'vc + :version "32.1") + (defface vc-dir-header-urgent-value '((t :inherit font-lock-warning-face)) "Face for urgent header values in VC-Dir buffers." :group 'vc @@ -1494,6 +1499,12 @@ Each element is a pair (HEADER . FUN) where VC backend `dir-extra-headers' implementations may push additional elements to this list.") +(defcustom vc-dir-show-key-binding-hints t + "Whether to show key binding hints in VC-Dir buffers." + :type 'boolean + :group 'vc + :version "32.1") + (defun vc-dir-headers (backend dir) "Display the headers in the *VC-Dir* buffer. It calls the `dir-extra-headers' backend method to display backend @@ -1511,7 +1522,35 @@ specific headers." (concat (propertize (format "%-11s: " header) 'face 'vc-dir-header) "\n")) - vc-dir-async-header-values))) + vc-dir-async-header-values) + (and + vc-dir-show-key-binding-hints + (concat + "\n" + (substitute-command-keys + (concat + "\\" + (propertize "Act " 'font-lock-face 'vc-dir-key-binding-hint-label) + " " + "(\\[vc-revert]) Revert, " + "(\\[vc-dir-delete-file]) Delete, " + "(\\[vc-dir-ignore]) Ignore, " + "(\\[vc-next-action]/\\[vc-dir-root-next-action]) Commit/commit all, " + "(\\[vc-push]) Push" + "\n" + (propertize "Marks" 'font-lock-face 'vc-dir-key-binding-hint-label) + " " + "(\\[vc-dir-mark]) Mark, " + "(\\[vc-dir-unmark]) Unmark, " + "(\\[vc-dir-unmark-all-files]) Unmark same state/dir, " + "(\\[universal-argument] \\[vc-dir-unmark-all-files]) Unmark all" + "\n" + (propertize "View " 'font-lock-face 'vc-dir-key-binding-hint-label) + " " + "(\\[vc-diff]) Diff, " + "(\\[revert-buffer]) Refresh, " + "(\\[vc-dir-hide-up-to-date]) Hide up-to-date")) + "\n")))) (defun vc-dir--set-header (def-dir) (ewoc-set-hf vc-ewoc (vc-dir-headers vc-dir-backend def-dir) "") commit 4470996198d1cad003c174aee478c65cb439a7af Author: Sean Whitton Date: Fri Jun 19 11:08:33 2026 +0100 * lisp/vc/vc-hg.el (vc-hg-previous-revision): Use 'hg log'. 'hg id' is much slower than 'hg log'. diff --git a/lisp/vc/vc-hg.el b/lisp/vc/vc-hg.el index 7a073d0b20b..0fa22d97fad 100644 --- a/lisp/vc/vc-hg.el +++ b/lisp/vc/vc-hg.el @@ -1158,11 +1158,11 @@ hg binary." (t ;; We can't simply decrement by 1, because that revision might ;; be e.g. on a different branch (bug#22032). - (with-temp-buffer - (and (zerop (vc-hg-command t nil nil "id" "-n" - "-r" (concat rev "~1"))) - ;; Trim the trailing newline. - (buffer-substring (point-min) (1- (point-max)))))))) + (with-output-to-string + (vc-hg-command standard-output 0 nil "log" + "-r" (format "revset(%s~1)" rev) + "--template" (if vc-use-short-revision + "{node|short}" "{node}")))))) (defun vc-hg-next-revision (_file rev) (let ((newrev (1+ (string-to-number rev)))