{"thread":{"id":"61358","subject":"Stashing just index..working-copy rather than HEAD..working-copy?","startedAt":"2024-04-24T13:50:50Z","lastAt":"2024-04-27T20:17:50Z","messageCount":4,"participants":["Tim Chase","Chris Torek","Junio C Hamano"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"493401","messageId":"ZikMqXeDnOqK_wlq@thechases.com","threadId":"61358","inReplyTo":null,"subject":"Stashing just index..working-copy rather than HEAD..working-copy?","fromName":"Tim Chase","fromEmail":"git@tim.thechases.com","sentAt":"2024-04-24T13:44:09Z","receivedAt":"2024-04-24T13:50:50Z","isPatch":false,"sender":{"key":"git@tim.thechases.com","avatar":null},"body":"A while back[1] I'd encountered a situation and sparred my way\naround it, but was hoping there was a better solution.\n\nI'd done a\n\n  $ git add -p\n\nto selectively add things that I wanted in the next commit.\nSo I wanted to stash the changes that appeared in\n\n  $ git diff\n\nand test just the changes I was about to commit so I did a\n\n  $ git stash\n\nHowever, that reset my index and stashed everything HEAD..working-copy.\nOkay, my fault.  There's a --keep-index that isn't default, so I\ncarefully re-staged my commit with another\n\n  $ git add -p\n\nand did\n\n  $ git stash --keep-index\n\nto keep the index.  Great.  My index was still good.  But when I went to\n\n  $ git stash pop\n\nas described in `git help stash` under the \"Testing partial commits\"\nit generated conflicts because it had still stashed HEAD..working-copy\n(as confirmed with a `git stash show -p`) rather than index..working-copy\nand some of those popped changes were already in the working-copy/index.\n\nTo work around it, I re-staged my index yet again:\n\n  $ git add -p\n\nand then did\n\n  $ git diff > temp.diff\n  $ git reset --staged\n\ndid my testing, and then re-applied the temp.diff patch to the\nworking-copy to get back to where I'd been.  Conflict-free as\nexpected.\n\nAs a slight improvement, /u/splettnet suggested actually committing\na dummy-commit:\n\n  $ git add -p\n  $ git commit --allow-empty-message\n  $ git stash\n\nat which point I could build/run/test and then resetting to uncommit:\n\n  $ git stash pop\n  $ git reset --soft HEAD~1\n\nwhich I've been using since.  However, I was wondering if there was\na better way to instruct git-stash to stash index..working-copy\ninstead of HEAD..working-copy (and leave the index alone in the\nprocess) in the first place.\n\nThanks,\n\n-tkc\n\n[1]\nhttps://www.reddit.com/r/git/comments/vchu83/stashing_only_unstaged_changes/\n\n\n\n\n\n"},{"id":"493447","messageId":"CAPx1GvcxyDDQmCssMjEnt6JoV6qPc5ZUpgPLX3mpUC_4PNYA1w@mail.gmail.com","threadId":"61358","inReplyTo":"ZikMqXeDnOqK_wlq@thechases.com","subject":"Re: Stashing just index..working-copy rather than HEAD..working-copy?","fromName":"Chris Torek","fromEmail":"chris.torek@gmail.com","sentAt":"2024-04-24T22:17:41Z","receivedAt":"2024-04-24T22:17:55Z","isPatch":false,"sender":{"key":"chris.torek@gmail.com","avatar":"https://avatars.githubusercontent.com/u/16826774?v=4"},"body":"On Wed, Apr 24, 2024 at 6:51 AM Tim Chase <git@tim.thechases.com> wrote:\n> ... However, I was wondering if there was\n> a better way to instruct git-stash to stash index..working-copy\n> instead of HEAD..working-copy (and leave the index alone in the\n> process) in the first place.\n\nLet me start by just providing a simple BUT A BIT DANGEROUS\nrecipe:\n\n    git stash --keep-index\n    [test, and assuming good, proceed with]\n    git reset --hard\n    git stash pop --index\n\nwhich will accomplish what you intended originally. But now, let\nme go on to tell you what you really need to know here, and some\nof the pitfalls you might encounter.\n\nLet's start with revisiting the subject line here:\n\n> Stashing just index..working-copy rather than HEAD..working-copy?\n\nThis implies that you're thinking about Git as storing diffs.\nThis is not the case!  Git stores *snapshots*.\n\nNow, as it happens, storing diffs vs storing snapshots ends up\nequivalent in a way.  But that's a bit like saying that writing\na number (say 15 for instance), then a delta (say 7), is the same\nas writing the number and then the sum (15 and then 22).  They\nare obviously *different*; it's just that if you apply the right\nprocess *in between each step*, you get the same *answer*.\n\nGit \"likes\" to show you differences because that's how humans like\nto think.  We don't want \"I had this full snapshot of everything,\nthen later, I had this other full snapshot of everything\" but\nrather: \"I had a snapshot, but I changed a bit of it.  Let me see\nwhat I changed.\"\n\nNow, the way `git stash` works is that it saves not one but\n*two* snapshots, both as commits, but with neither one being\n\"on\" any *branch*.  Git can do this because in Git it's the\ncommits, not the branch names, that actually matter -- branch\nnames are pretty much irrelevant, except of course to those\npesky humans. :-)\n\nThe two commits that `git stash` saves are:\n\n 1. the complete contents of the index; and\n 2. the complete contents of the working tree that you'd\n    have gotten *in* the index if you had run `git add -u`,\n    more or less.\n\n(There is in fact an optional *third* commit, from `git stash -a`\nor `git stash -u`, but let's just ignore that here.  If you ask\nfor this, it makes things trickier.)  Let's call commit #1 here\nthe \"I\" (for Index) commit, and commit #2 the \"W\" (for Work-tree)\ncommit.\n\nEvery commit, in Git, has a parent commit, or a list of parent\ncommits.  The parent of the \"I\" commit is the `HEAD` commit, and\nfor various internal reasons, the \"W\" commit has two parents,\nboth `HEAD` and the new \"I\" commit.  So Git can always find the\noriginal `HEAD` commit from the stash commits, and can find\nthe \"I\" commit from the \"W\" commit.\n\nHaving made the two commits, `git stash` normally then runs\nthe equivalent of `git reset --hard`, which puts both the index\nand the working tree back to the state saved in the `HEAD`\ncommit.  When you run `git stash --keep-index`, Git modifies\nthis to do the equivalent of \"reset to whatever's in the index\"\n(rather than \"reset to whatever's in the HEAD commit\").\n\nThat's why `git stash --keep-index` lets you test what's in\nthe index.  This is an obvious practical use for `git stash\n--keep-index`.\n\nThe problem with this comes in later: both `git stash apply` and\n`git stash pop` run into it.  They run into it whether you use\n`--index` or not.  **Here's the root of the problem: `git stash`\nmade two commits, not one.**\n\nAgain, `git stash` made two commits.  You can't put two commits\ninto one place!  Whoever invented `git stash` chose to solve this\nproblem in a kind of strange way.\n\nLet's start with `git stash apply`.  Whoever first wrote the stash\ncode was thinking about `git apply` here.  How does `git apply`\nwork?  Well, it takes, as its input, a diff.  We get a diff by\ncomparing *two things*.  So `git stash apply` compares two things:\nthe commit that you had as `HEAD` when you ran `git stash`, and\nthe commit that `git stash` saved as \"W\".\n\n`git stash apply` therefore runs:\n\n    git diff [various options if needed] <W's HEAD-parent> <W>\n\nwhich gets it a diff that it can then, in effect, feed to\n`git apply`.  The apply code then tries to apply that diff to\nyour *current working tree*.\n\nIf your current working tree matches W's HEAD-parent, this\napplication proceeds smoothly, and you're all set.  But what\nif, for whatever reason, your current working tree *doesn't*\nmatch W's HEAD-parent?  What if instead if matches W's I-parent,\naka the \"I\" commit?  In that case, some lines try to apply\ntwice and/or cause a conflict -- and that's exactly what you\nhave been running into.\n\nIf `git stash` had a way to do:\n\n    git diff [options] <W's I-parent> <W>\n\nand apply that, *that* would be what you would want here.  But\nalas, it lacks any such option.\n\nWhat `git stash` *does* have is `git stash apply --index`.  This\ntells Git to run *two* `git diff`s:\n\n    git diff [options] <original HEAD-parent> <I>\n    git diff [options] <I> <W>\n\nGit then tries to apply the first diff to both the index and the\nworking tree (a la `git apply --index`), and then apply the second\ndiff to the working tree only (`git apply` without options).\n\nIf your working tree matches the original `HEAD`, you get just\nwhat you want: the index is restored to the way it was when you\nran `git stash --keep-index`, and then the working tree is also\nrestored to the way it was at that time.\n\n**The biggest pitfall here is that you might forget `--index`.**\n\nIf you use `git stash pop`, this can be pretty terrible!\n\nThe W-and-I commit pair that `git stash` makes is, as mentioned\nearlier, on *no* branch.  This means Git can't find it directly\nby a branch name.  The way Git finds these commits is through a\nspecial name, `refs/stash`, that's not a *branch* name at all.\n\nThe `git stash apply` command means *apply a stash*.  By default,\nit applies the topmost stash in the stash-stack.  It then *leaves\nthat stash around* so you can still access it by the same name.\n\nThe `git stash pop` command essentially means: *run `git stash\napply`, then if it says it worked, run `git stash drop`.*  It's\nthe `drop` command that discards the name for the stash.  Once\nthe *name* is gone, the only way you can get to the two stash\ncommits is to find the big ugly hash ID for the W commit.\n\n(Finding the W commit gets you all three -- then-HEAD, I, and W\n-- via the two parents in the W commit.  Finding the I commit is\nnot as useful as it gets you just the then-HEAD as its parent.\nThat's why the special `refs/stash` name stores just the W commit\nhash ID: that's all you need.)\n\nNow, if you use the \"DANGEROUS\" recipe, suppose you run:\n\n    git stash --keep-index\n    [test and find that it's all good]\n    git reset --hard\n    git stash pop       [OOPS FORGOT TO USE --index]\n\nThe `git reset --hard` puts everything back to the `HEAD` commit\nstate, losing the carefully-`git add`-ed parts that you just\ntested and intend to commit.  Then `git stash pop` applies *only\nthe W commit diffs*, which is not awful on its own but doesn't\nsave the carefully-staged stuff as staged.  Then it drops *both\nstash commits*.  You now have to re-create the carefully-`add`-ed\nparts.\n\nIf you catch the mistake right away, you'll usually have the hash\nID of the dropped stash handy in your Terminal window or wherever,\nand be able to snag it, which can save a lot of work.  But if not,\nwell, that's why I call this \"dangerous\".\n\nTo reduce the danger, you can simply avoid `git stash pop`. Run\n`git stash apply` instead, remembering or maybe forgetting the\n`--index`.  Then check your work and if you goofed it up and\nforgot `--index`, you can `git reset --hard` and `git apply\n--index` this time, because the topmost stash is still the topmost\nstash.\n\nTo help remember all of the above, let's revisit the subject\nline once more:\n\n> Stashing just index..working-copy rather than HEAD..working-copy?\n\n`git stash` *already saves everything you want*.  It's actually\nthe *application* step that goes awry here.\n\n   *  *  *\n\nWith all that said, I'd like to make one last suggestion, which\nI think is a lot simpler: *stop using `git stash`*.  Just make\na commit!  If you want to test it, consider making a new branch\nfirst:\n\n    [do a bunch of careful `git add`s or whatever]\n    [realize \"I need to test this\"]\n    git switch -c test-my-index\n    git commit -m message1\n    git switch -c save-additional-work\n    git add -u\n    git commit -m message2\n\nYou can now check out the \"test-my-index\" branch, as a branch, and\ntest it and if it doesn't work, keep fixing it up until it *does*\nwork.  Once it's ready to go, smash it all down to a single commit\nwith `git rebase` if needed, maybe fix up the commit message(s),\nand then you have it ready to go into the original branch as a\nsingle good commit.\n\nMeanwhile, the \"save-additional-work\" branch is there for you to\nget the working-tree changes back whenever you want them.  Not\nonly that, but that branch has the original to-be-tested index\nchanges as its parent commit, and then the commit-before-that as\nits parent's parent, so you can easily see what you were thinking.\n\nChris\n"},{"id":"493451","messageId":"xmqq1q6uwd5y.fsf@gitster.g","threadId":"61358","inReplyTo":"CAPx1GvcxyDDQmCssMjEnt6JoV6qPc5ZUpgPLX3mpUC_4PNYA1w@mail.gmail.com","subject":"Re: Stashing just index..working-copy rather than HEAD..working-copy?","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-04-24T23:31:37Z","receivedAt":"2024-04-24T23:31:46Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Chris Torek <chris.torek@gmail.com> writes:\n\n> With all that said, I'd like to make one last suggestion, which\n> I think is a lot simpler: *stop using `git stash`*.  Just make\n> a commit!\n\n;-)\n\nIf I recall correctly, the original design of \"git stash\" was \"I\nsave everything in the working tree, so that I can start working on\nan urgent request immediately, and then later restore everything\",\nand there was no \"--index\" option for application, even though the\nstash entries were the W commit that is a merge of the I (index)\ncommit and the B (base) commit.  The \"apply/pop --index\" was a mere\nafterthought that does not work very well and made things more\nconfusing.  It wasn't meant to be used in anything complex, for\nwhich a separate branch with real commits were the way to go.\n\nThere were some reasons (like, working tree side post-commit hooks\nthat are not well written to distinguish temporary commits from real\nones and send out notifications outside) that some folks wanted to\navoid making a commit on a temporary branch and to them, having a\nbit more complex \"stash\" may have been a way for them to avoid\ntriggering those poorly designed workflow around post-commit hooks.\nBut with modern Git in this age with workflows and disciplines\nbetter understood, I agree that we should encourage use of more\ntemporary branches with real commits.  If there are reasons to cause\ndevelopers fear of commitment (e.g., \"my $CORP environment forces me\nto show every commit I make to CI server, which slows me down and\nwastes resources if I make many tentative commits only for\nsnapshot\"), they should be solved in a way that users do not have to\nfear commitments.\n\nThanks.\n\n\n\n"},{"id":"493552","messageId":"Zi1daCgMI3KkC9yA@thechases.com","threadId":"61358","inReplyTo":"CAPx1GvcxyDDQmCssMjEnt6JoV6qPc5ZUpgPLX3mpUC_4PNYA1w@mail.gmail.com","subject":"Re: Stashing just index..working-copy rather than HEAD..working-copy?","fromName":"Tim Chase","fromEmail":"git@tim.thechases.com","sentAt":"2024-04-27T20:17:44Z","receivedAt":"2024-04-27T20:17:50Z","isPatch":false,"sender":{"key":"git@tim.thechases.com","avatar":null},"body":"On 2024-04-24 15:17, Chris Torek wrote:\n> Let's start with revisiting the subject line here:\n> \n> > Stashing just index..working-copy rather than HEAD..working-copy?\n> \n> This implies that you're thinking about Git as storing diffs.\n\nHaving looked at the `gitk --all` display (or similar git-log with\ngraph visualization) I can see the pair of stashed commits you\ndescribe,\n\n  $ git init dummy\n  $ cd dummy\n  $ seq 10 > a.txt\n  $ git add a.txt\n  $ ed a.txt\n  5s/$/ production change/\n  6s/$/ pending change/\n  wq\n  $ git add -p\n  e\n  (modify the diff so that only line #5 is modified and line 6 remains untouched)\n  $ git stash -k\n\nThis feels like what I expect (the diff that my `git add -p` showed):\n\n  $ git diff --cached\n  diff --git a/a.txt b/a.txt\n  index 0ff3bbb..94fa4fc 100644\n  --- a/a.txt\n  +++ b/a.txt\n  @@ -2,7 +2,7 @@\n   2\n   3\n   4\n  -5\n  +5 production change\n   6\n   7\n   8\n  \n> Now, the way `git stash` works is that it saves not one but\n> *two* snapshots, both as commits, but with neither one being\n> \"on\" any *branch*.\n\nRight.  So looking at my test repo\n\n  $ git log --oneline --graph --all\n  *   aad5479 (refs/stash) WIP on main: 7f38a19 Initial checkin\n  |\\  \n  | * e8e0979 index on main: 7f38a19 Initial checkin\n  |/  \n  * 7f38a19 (HEAD -> main) Initial checkin\n\nthe stash looks right when I diff the two snapshots that the\nlogs produce\n\n  $ git diff e8e0..aad5\n  diff --git a/a.txt b/a.txt\n  index 94fa4fc..c9203f6 100644\n  --- a/a.txt\n  +++ b/a.txt\n  @@ -3,7 +3,7 @@\n   3\n   4\n   5 production change\n  -6\n  +6 pending change\n   7\n   8\n   9\n\nAFAICT, that's diffing the I and W commits you detail.\n\n> That's why `git stash --keep-index` lets you test what's in\n> the index.  This is an obvious practical use for `git stash\n> --keep-index`.\n\nRight, so we're on the same page through here.\n\nIf I apply that diff against the current state of things (production\nchanges in the Index & WC but not committed officially yet), it\nworks without conflict.\n\n> The problem with this comes in later: both `git stash apply` and\n> `git stash pop` run into it.  They run into it whether you use\n> `--index` or not.  **Here's the root of the problem: `git stash`\n> made two commits, not one.**\n\nAlmost...as shown above, the diff of the \"I\" and \"W\" commits\n*does* produce the correct diff that applies cleanly.  So the root\nof the problem is diffing the \"wrong\" (for values of my expectations)\npair of commits to generate this diff to apply.\n\n> How does `git apply` work?  Well, it takes, as its input, a\n> diff.  We get a diff by comparing *two things*.  So `git stash\n> apply` compares two things: the commit that you had as `HEAD`\n> when you ran `git stash`, and the commit that `git stash` saved\n> as \"W\".\n> \n> `git stash apply` therefore runs:\n> \n>     git diff [various options if needed] <W's HEAD-parent> <W>\n\nAnd here's where it feels confusing/wrong to me -- it's choosing\nto diff W^..W instead of I..W to obtain that diff-to-apply.\n\nAs noted in `git help stash` in the `pop` docs\n\n  The working directory must match the index.\n\nwhich it does.\n\n  $ git diff # compare WC with index\n\nreturns no difference.  However the working directory doesn't match\nW^1.  Choosing to apply W^..W is what introduces the conflicts.\n\nMaybe those docs should read something like\n\n  The working directory must match the HEAD at the time of stashing\n\nor something like that?\n\n> If your current working tree matches W's HEAD-parent, this\n> application proceeds smoothly, and you're all set.  But what\n> if, for whatever reason, your current working tree *doesn't*\n> match W's HEAD-parent?  What if instead if matches W's I-parent,\n> aka the \"I\" commit?  In that case, some lines try to apply\n> twice and/or cause a conflict -- and that's exactly what you\n> have been running into.\n\nExactly :-)\n\n> If `git stash` had a way to do:\n> \n>     git diff [options] <W's I-parent> <W>\n> \n> and apply that, *that* would be what you would want here.  But\n> alas, it lacks any such option.\n> \n> What `git stash` *does* have is `git stash apply --index`.  This\n> tells Git to run *two* `git diff`s:\n> \n>     git diff [options] <original HEAD-parent> <I>\n>     git diff [options] <I> <W>\n> \n> Git then tries to apply the first diff to both the index and the\n> working tree (a la `git apply --index`), and then apply the second\n> diff to the working tree only (`git apply` without options).\n\nIf I understand you correctly, it sounds like `git stash {apply,pop}\n--index` does a bit of a `reset` of the index & WC back to the\npre-stashed state, then *recreates* the index based on that first\ndiff, and recreates the WC based on both diffs.\n\n> If your working tree matches the original `HEAD`, you get just\n> what you want: the index is restored to the way it was when you\n> ran `git stash --keep-index`, and then the working tree is also\n> restored to the way it was at that time.\n\nRight.\n\n> **The biggest pitfall here is that you might forget `--index`.**\n\nFair (and worth my considering an alias or something)\n\n> If you use `git stash pop`, this can be pretty terrible!\n\nAnd what brought me to posting :-)\n\n> Now, if you use the \"DANGEROUS\" recipe, suppose you run:\n> \n>     git stash --keep-index\n>     [test and find that it's all good]\n>     git reset --hard\n>     git stash pop       [OOPS FORGOT TO USE --index]\n\nThis takes no imagination on my part having done exactly that\nomission of --index :-)\n\n> `git stash` *already saves everything you want*.  It's actually\n> the *application* step that goes awry here.\n\nRight.\n\n> With all that said, I'd like to make one last suggestion, which\n> I think is a lot simpler: *stop using `git stash`*.  Just make\n> a commit!\n\nAnd given the limitations I'm seeing on how stash pop/apply behave,\nI think that's the conclusion I'm coming to as well (and sorta what\n/u/splettnet kinda suggested).\n\nBy doing an actual commit:\n\n  $ git add -p # complex teasing out of the commit\n  $ git commit -m \"message if all tests succeed\"\n  $ git stash\n\nI can test it, and if it works, the commit is already in the repo.\nThen a\n\n  $ git stash pop\n\ndoes what I expect, and if the testing failed, I can\n\n  $ git reset --mixed HEAD~\n\nto get back to where I was.\n\nThat suffices for me.\n\nThanks for the detailed write-up!\n\n-tkc\n\n\n\n\n\n"}]}