all messages for Emacs-related lists mirrored at yhetil.org
 help / color / mirror / code / Atom feed
From: Sean Whitton <spwhitton@spwhitton.name>
To: Dmitry Gutov <dgutov@yandex.ru>
Cc: 60126@debbugs.gnu.org, juri@linkov.net
Subject: bug#60126: 30.0.50; vc-git-checkin: Offer to unstage conflicting changes
Date: Thu, 22 Dec 2022 20:59:53 -0700	[thread overview]
Message-ID: <878riybwhy.fsf@melete.silentflame.com> (raw)
In-Reply-To: <87h6xnasgn.fsf@melete.silentflame.com> (Sean Whitton's message of "Thu, 22 Dec 2022 17:12:24 -0700")

[-- Attachment #1: Type: text/plain, Size: 752 bytes --]

Hello,

On Thu 22 Dec 2022 at 05:12PM -07, Sean Whitton wrote:

>> For completeness, though, here's a way to implement 'git push --staged' with
>> Git plumbing manually: https://stackoverflow.com/a/72582276/615245
>>
>> And as for a 'git pop --index' substitute, if the stash contains only the
>> index area stuff, it might be as easy as
>>
>>   git diff stash@{0}^..stash@{0} > patch.diff
>>   git apply --cached patch.diff
>>   git stash drop
>
> These references are helpful.  I'll investigate further.

Here is my patch.

It works, except that sometimes the let-binding of process-environment
fails, such that the commands affect the normal index rather than the
temporary index.  Can you see what I'm doing wrong there?

Thanks.

-- 
Sean Whitton

[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-vc-git-checkin-Stash-other-staged-changes.patch --]
[-- Type: text/x-patch, Size: 7741 bytes --]

From acea3dcd080c1cfacbddeb257683438de8a9b325 Mon Sep 17 00:00:00 2001
From: Sean Whitton <spwhitton@spwhitton.name>
Date: Thu, 22 Dec 2022 20:54:08 -0700
Subject: [PATCH] vc-git-checkin: Stash other staged changes

* lisp/vc/vc-git.el (vc-git--stash-staged-changes): New function.
(vc-git-checkin): Use new function to avoid needing to unstage changes
unrelated to the patch we want to commit (bug#60126).
---
 lisp/vc/vc-git.el | 97 ++++++++++++++++++++++++++++++++++++++++-------
 1 file changed, 83 insertions(+), 14 deletions(-)

diff --git a/lisp/vc/vc-git.el b/lisp/vc/vc-git.el
index 0a4e9caa614..86e8f5894df 100644
--- a/lisp/vc/vc-git.el
+++ b/lisp/vc/vc-git.el
@@ -1020,22 +1020,36 @@ vc-git-checkin
           ;; message.  Handle also remote files.
           (if (eq system-type 'windows-nt)
               (let ((default-directory (file-name-directory file1)))
-                (make-nearby-temp-file "git-msg")))))
+                (make-nearby-temp-file "git-msg"))))
+         to-stash)
     (when vc-git-patch-string
       (unless (zerop (vc-git-command nil t nil "diff" "--cached" "--quiet"))
-        ;; Check that all staged changes also exist in the patch.
-        ;; This is needed to allow adding/removing files that are
-        ;; currently staged to the index.  So remove the whole file diff
-        ;; from the patch because commit will take it from the index.
+        ;; Check that what's already staged is compatible with what
+        ;; we want to commit (bug#60126).
+        ;;
+        ;; 1. If the changes to a file in the index are identical to
+        ;;    the changes to that file we want to commit, remove the
+        ;;    changes from our patch, and let the commit take them
+        ;;    from the index.  This is necessary for adding and
+        ;;    removing files to work.
+        ;;
+        ;; 2. If the changes to a file in the index are different to
+        ;;    changes to that file we want to commit, then we have to
+        ;;    unstage the changes or abort.
+        ;;
+        ;; 3. If there are changes to a file in the index but we don't
+        ;;    want to commit any changes to that file, we need to
+        ;;    stash those changes before committing.
         (with-temp-buffer
           (vc-git-command (current-buffer) t nil "diff" "--cached")
           (goto-char (point-min))
-          (let ((pos (point)) file-name file-diff file-beg)
+          (let ((pos (point)) file-name file-header file-diff file-beg)
             (while (not (eobp))
               (when (and (looking-at "^diff --git a/\\(.+\\) b/\\(.+\\)")
                          (string= (match-string 1) (match-string 2)))
                 (setq file-name (match-string 1)))
               (forward-line 1) ; skip current "diff --git" line
+              (setq file-header (buffer-substring pos (point)))
               (search-forward "diff --git" nil 'move)
               (move-beginning-of-line 1)
               (setq file-diff (buffer-substring pos (point)))
@@ -1049,12 +1063,15 @@ vc-git-checkin
                                            (+ file-beg (length file-diff)))))
                      (setq vc-git-patch-string
                            (string-replace file-diff "" vc-git-patch-string)))
-                    ((and file-name
-                          (yes-or-no-p
-                           (format "Unstage already-staged changes to %s?"
-                                   file-name)))
-                     (vc-git-command nil 0 file-name "reset" "-q" "--"))
-                    (t (user-error "Index not empty")))
+                    ((string-match (format "^%s" (regexp-quote file-header))
+                                   vc-git-patch-string)
+                     (if (and file-name
+                              (yes-or-no-p
+                               (format "Unstage already-staged changes to %s?"
+                                       file-name)))
+                         (vc-git-command nil 0 file-name "reset" "-q" "--")
+                       (user-error "Index not empty")))
+                    (t (push file-name to-stash)))
               (setq pos (point))))))
       (unless (string-empty-p vc-git-patch-string)
         (let ((patch-file (make-nearby-temp-file "git-patch")))
@@ -1062,7 +1079,8 @@ vc-git-checkin
             (insert vc-git-patch-string))
           (unwind-protect
               (vc-git-command nil 0 patch-file "apply" "--cached")
-            (delete-file patch-file)))))
+            (delete-file patch-file))))
+      (when to-stash (vc-git--stash-staged-changes files)))
     (cl-flet ((boolean-arg-fn
                (argument)
                (lambda (value) (when (equal value "yes") (list argument)))))
@@ -1088,7 +1106,58 @@ vc-git-checkin
                       args)
                     (unless vc-git-patch-string
                       (if only (list "--only" "--") '("-a"))))))
-    (if (and msg-file (file-exists-p msg-file)) (delete-file msg-file))))
+    (if (and msg-file (file-exists-p msg-file)) (delete-file msg-file))
+    (when to-stash
+      (let ((cached (make-nearby-temp-file "git-cached")))
+        (unwind-protect
+            (progn (with-temp-file cached
+                     (vc-git-command t 0 nil "stash" "show" "-p"))
+                   (vc-git-command nil 0 cached "apply" "--cached"))
+          (delete-file cached))
+        (vc-git-command nil 0 nil "stash" "drop")))))
+
+(defun vc-git--stash-staged-changes (files &optional message)
+  "Stash only the staged changes to FILES with description MESSAGE."
+  ;; This is necessary because even if you pass a list of file names
+  ;; to git-stash(1), it will stash any and all staged changes.
+  (unless (zerop
+           (vc-git-command nil t files "diff" "--cached" "--quiet"))
+    (unless message (setq message "Previously staged changes"))
+    (cl-flet
+        ((git-string (&rest args)
+           (string-trim-right
+            (with-output-to-string
+              (apply #'vc-git-command standard-output 0 nil args)))))
+      (let ((cached (make-nearby-temp-file "git-cached"))
+            tree)
+        ;; Use a temporary index to create a tree object corresponding
+        ;; to the staged changes to FILES.
+        (unwind-protect
+            (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")
+                      (vc-git-command nil 0 cached "apply" "--cached")
+                      (setq tree (git-string "write-tree")))
+                  (delete-file index))))
+          (delete-file cached))
+        ;; Prepare stash commit object, which has a special structure.
+        (let* ((tree-commit (git-string "commit-tree" "-m" message
+                                        "-p" "HEAD" tree))
+               (stash-commit (git-string "commit-tree" "-m" message
+                                         "-p" "HEAD" "-p" tree-commit
+                                         tree)))
+          ;; Push the new stash entry.
+          (vc-git-command nil 0 nil "update-ref" "--create-reflog"
+                          "-m" message "refs/stash" stash-commit)
+          ;; Unstage the changes we've now stashed.
+          (vc-git-command nil 0 files "reset" "--"))))))
 
 (defun vc-git-find-revision (file rev buffer)
   (let* (process-file-side-effects
-- 
2.30.2


  reply	other threads:[~2022-12-23  3:59 UTC|newest]

Thread overview: 26+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2022-12-16 18:32 bug#60126: 30.0.50; vc-git-checkin: Offer to unstage conflicting changes Sean Whitton
2022-12-17 17:06 ` Juri Linkov
2022-12-18  0:20   ` Sean Whitton
2022-12-18  1:08 ` Dmitry Gutov
2022-12-19 22:30   ` Sean Whitton
2022-12-20  0:53     ` Dmitry Gutov
2022-12-20  6:43       ` Sean Whitton
2022-12-20 13:47         ` Dmitry Gutov
2022-12-20 16:47           ` Sean Whitton
2022-12-20 15:13         ` Dmitry Gutov
2022-12-20 17:04           ` Sean Whitton
2022-12-20 23:10             ` Sean Whitton
2022-12-20 23:41               ` Sean Whitton
2022-12-20 23:45               ` Dmitry Gutov
2022-12-23  0:12                 ` Sean Whitton
2022-12-23  3:59                   ` Sean Whitton [this message]
2022-12-23  8:16                     ` Eli Zaretskii
2022-12-24  2:03                       ` Sean Whitton
2022-12-23 23:18                     ` Dmitry Gutov
2022-12-24  2:02                       ` Sean Whitton
2022-12-24 14:50                         ` Dmitry Gutov
2022-12-24 18:22                           ` Sean Whitton
2022-12-24 19:26                             ` Dmitry Gutov
2022-12-24 20:10                               ` Sean Whitton
2022-12-23 22:55                   ` Dmitry Gutov
2022-12-20 17:13           ` Juri Linkov

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=878riybwhy.fsf@melete.silentflame.com \
    --to=spwhitton@spwhitton.name \
    --cc=60126@debbugs.gnu.org \
    --cc=dgutov@yandex.ru \
    --cc=juri@linkov.net \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
Code repositories for project(s) associated with this external index

	https://git.savannah.gnu.org/cgit/emacs.git
	https://git.savannah.gnu.org/cgit/emacs/org-mode.git

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.