1

The scenario:

  1. Stage file changes
  2. Do some more changes on the file
  3. Stage file changes

Now I regret I staged the last changes and wish to go back to the way the file was staged at first. In essence, I want to rollback the last stage operation.

Note, that nothing was committed, all the changes were staged only.

Is it possible?

mark
  • 59,016
  • 79
  • 296
  • 580
  • It doesn't seem like git records that operation anywhere, other than as cumulative changes in the staging area for a certain file. There's no record of such an action in the reflog, and as such, i don't think it's possible to get back to pre-3 in your list. But, i think it would be possible to come up with some layer on top of `git-add` that records an actual commit of your changes, so you can reset that and re-construct the index as you like. – zrrbite Dec 03 '21 at 23:42
  • 1
    It's definitely *possible* in most cases since Git hangs on to "dead" objects for at least 14 days by default, and usually quite a bit longer. But retrieving the right object is a bit of a pain. See [jthill's answer](https://stackoverflow.com/a/70222648/1256452) for a method. – torek Dec 04 '21 at 03:12

2 Answers2

2

It's possible. It's not even hard. Adding something to the repo adds it to the repo, and Git doesn't clean out anything that's been useful recently unless people get very explicit about telling it to do that, and nobody does.

My first attempt would be to see if git reset --patch -- the.file will do it, though. That lets you reset change hunks in the indexed copy to what they were in the committed copy you checked out; if you're after un-staging something that didn't overwrite lines you staged before and want back now that'll do you just fine.

Also, any good Git plugin, and any half-decent programmer's editor will have one, can do this right in your editor interface, I know of fugitive for vim and magit for emacs, they let you edit and smoosh hunks around in staged content as if they was just another file, and pull stuff out of committed content to wherever you want.

But if your new changes touched what you had in the lines you want back, then the content you want back isn't in your work tree or the indexed or committed content.

For that, and always, there's the option of a little light spelunking in the repo.

Added content stays in .git/objects/??, type-tagged and compressed some, until Git does a repack, and as I recall Git doesn't by default pack up unreferenced items at all, it leaves them lying around loose until they've been unreferenced for at least several weeks, then cleans them out on the next trash run after that.

find .git/objects/?? -type f -exec ls -t {} + | awk -F/ '{print $3$4}'

will list all the loose-object ids in your repo, which will all result from things you did locally, most recent first. To whittle it down to stuff you care about,

find .git/objects/?? -type f -exec ls -t {} + \
| awk -F/ '{print $3$4}' | git cat-file --batch-check \
| awk '$2=="blob" && n++<50'

will show you the id, type and size of the last 50 blobs you added. Tops on that list will almost certainly be the oops, the next blob about the same size is very likely to be what you added before that, git show likely-looking ids until you find what you want. Then,

git update-index --cacheinfo 100644,$thatid,that.file

(or 100755 if it's an executable file) to make the index entry point at that content instead of your oops.

jthill
  • 55,082
  • 5
  • 77
  • 137
  • This is nice trick. From your `find` list, it's possible to `git show ` to double check which blob is the right one to update the index with. – zrrbite Dec 04 '21 at 11:01
  • Also, since there doesn't seem to be a tree/commit object associated with these index blobs, it's difficult to pinpoint which file they were meant for. That's probably also why there's no built in way to undo the 2nd (or more) `git add` to a file. – zrrbite Dec 04 '21 at 11:37
  • Hey, this is engineering, not sales. Tricks are for sales. This is tool use :-) – jthill Dec 04 '21 at 13:15
  • "This is nice tool usage" - Doesn't have quite the same ring to it! :) But i get your point. – zrrbite Dec 04 '21 at 23:27
  • Nah, you're right, s/tool usage/kludge/ and wear it proud. – jthill Dec 04 '21 at 23:38
  • A possible improvement to the above could be to filter out the blob reported by `git ls-files --stage` for `that.file` – zrrbite Dec 04 '21 at 23:45
  • I did have `(git rev-parse :that.file; find etc.)` originally in place of just the find, to make sure the very first blob listed was the indexed copy, but I took it out because it seemed like good exercise-for-the-reader material and it kinda cluttered the main point. – jthill Dec 05 '21 at 00:05
1

When you stage a file, it is written to the index. When you stage it again, it is written to the index, overwriting the earlier version. That's it. It's gone.

See What's the difference between HEAD, working tree and index, in Git? for an explanation of the index and how it fits in with the rest of the Git world.

atsaloli
  • 111
  • 6