Hacker News new | past | comments | ask | show | jobs | submit login
How to undo almost anything with Git (2015) (github.blog)
372 points by ljiljana on Dec 23, 2019 | hide | past | favorite | 79 comments



Also worth noting: you can abort and undo many kinds of in-progress operations with “git <operation> —abort”. Works for merge, rebase, and a few other things, and can seriously save your bacon when you do a “git merge <badbranch>” and are suddenly confronted with a bazillion merge conflicts.

Also, if you’re in a panic because something went screwy, check “git status” and read every line carefully. status tells you a lot more than you might expect - what branch you’re on, if you’re even on a branch, what merge/rebase/commit operation you’re in the middle of (if any), and even how to go forward with or back out of the current operation.

Finally, commit regularly and often! reflog and rebase mean that you can always maintain a clean history if you want, while committing makes sure that your changes are properly tracked and saved by git so you can rewind when needed. Once you get comfortable with it, git lets you really practice fearless experimentation, which unlocked a whole new level of productivity for me.


> check “git status” and read every line carefully.

Seconded, but I'd go even further. Run and read git status before and after running any git command until you get comfortable enough with git to where you can predict the output without running it.

Additionally you can go get git prompt[1] installed so you always know your git status at a glance. Saves a lot of typing in any case.

As a last suggestion I'd say go read git-scm.com[2]. It has the excellently written man pages for looking up what a command does and which flags it takes. It also has an amazingly written free ebook explaining git top-to-bottom. After reading that you'll be a git wizard compared to your peers.

[1] https://github.com/git/git/blob/master/contrib/completion/gi... [2] https://git-scm.com/


I totally second reading the book front to back. It is not that long and fairly well written, and can save so much time and frustration in the long run. Giving the link is the first thing I do when helping someone with git.

Unfortunately so many people put that aside and never read it, and then come back to me a few weeks later because they messed everything up again, and still do not understand what I mean with "index".

Git is very powerful but a bit awkward, so I consider knowing the underlying concepts necessary to use it properly.


I don't even think git is all that awkward. It's incredibly elegant but that also makes it somewhat orthogonal compared to how you're used to think about stuff.

I dunno, git is probably one of the few instances where I feel like RTFM is a valid remark.


Weirdly there is no "git stash pop --abort" to roll back when a merge conflict occurs during stash pop.


You can also use git-retain-history with reflog to sidestep unexpected git operations.

https://git-man-page-generator.lokaltog.net/#4b16e11f044dd60...


The thing that made git click for me was to understand the data structures that make git work.

There's beauty and elegance in the implementation details of git. You can do and undo with confidence once you can translate the changes you want to make to git object transformations.

To get an overview of the concepts behind git I recommend this article by one of the GitHub founders: http://tom.preston-werner.com/2009/05/19/the-git-parable.htm...

To understand the data structures I suggest: https://codewords.recurse.com/issues/two/git-from-the-inside...


Here also my 'git from scratch' TODO for the new year list : https://wyag.thb.lt/


This looks great!


> The thing that made git click for me was to understand the data structures that make git work.

I cannot agree with this more! The data structure makes git git, there are some questionable CLI design choices but they all melt away once you get the data structure.

In relation to trying to manipulate commits and "undoing things", everything will be significantly clearer the faster you can move away from thinking of undo like a one dimensional word processor function... Instead start to think of git as an immutable graph of commit objects you are able to traverse, reference and use in anyway - at that point "undo" seems like a completely inadequate description.


"understand the data structures that make git work" - insert relevant xkcd.


I'm actually somewhat surprised that this wasn't mentioned in the comments yet: https://ohshitgit.com/ Which is a page that covers common mistakes using git and quick ways to fix your mistakes. I've found myself making a mistake mentioned on this page with regularity and always refer to the site to help myself fix it quickly.

(It looks like they have a swearing-safe version now at https://dangitgit.com/)


If you have to use the command line, those "gosh darn it git" sites have some good recipes. Because if the problem is "I accidentally committed to the wrong branch!" who is going to remember all this off the top of their head:

  # undo the last commit, but leave the changes available
  git reset HEAD~ --soft
  git stash
  # move to the correct branch
  git checkout name-of-the-correct-branch
  git stash pop
  git add . # or add individual files
  git commit -m "your message here";
  # now your changes are on the correct branch
Or you can use a powerful GUI like SmartGit. Then you don't have to memorize anything or look up a recipe. For the situation above, you simply drag your branch markers to where you want them in the log. Done!

Similarly, for situations where you would look up hashes in the reflog, just click the Recyclable Commits checkbox, and everything in the reflog shows up as ordinary commits in the same commit tree as everything else. You can even see diffs between the reflog commits and your regular commits without having to do any temporary checkouts.

I know many developers like the command line and don't want to consider using a GUI. But I encourage you to give SmartGit a try. It works in conjunction with the command line, so you you're not locked into the GUI, you can use either one whenever you want.


If you're trying to memorize that list of commands without understanding what is happening under the hood you have a snowflakes chance in hell. If on the other hand you've read (and understood!) the git reset manpage it's hardly magic.

Git reset changes the ref HEAD is pointing at. So in this case HEAD is pointing to the branch you've accidentally committed to. Running `git reset HEAD- --soft` simply moves the branch to point to one commit previous. The soft flag tells git reset to not touch any actual files while doing this, so your changes stay untouched. This is really the most complex part. After this all that's happening is re-committing the same changes to the correct branch. The git stash is even optional if the changes don't conflict with any changes done by the checkout.

You're right, memorizing a list of commands like that is a fools errand. What's not a fools errand is understanding the underlying data structure of git and how various commands act on it. Which is all documented in the official docs by the way, using a writing style that's rather readable in my opinion. Switching to a GUI wholesale is throwing out the baby with the bathwater. Using a GUI tool still requires knowing how git works to be effective.

Of course you might still argue that it's too much typing, which is probably true. Good thing git aliases got invented so even though I can recall the entire command list you mentioned from memory I'd still in practice simply run "git uncommit" since I've had that alias defined for about forever now.

This comment isn't aimed at anyone in particular and especially not at the parent comment. The parent comment just hit my grumpy button I think ;-)


There are other ways to do that, too. For example, Git stash is my blind spot and I don't really use it.

A conceptually simple way (for me) is to: checkout "good" branch, cherry-pick the "wrong" commit, then checkout the "bad" branch and remove the "wrong" commit ("git reset --hard HEAD^").

Mixing and matching simple commands mechanistically means everything can in fact be remembered.

YMMV.


You might want to try using rebase for this use case too, Saves a couple checkout steps. Just make sure to specify the right commit range ;)


Everything apart from the initial reset command in that ‘recipe’ is stuff that a command line git user would be doing all the time anyway. There’s not really anything to remember here, other than that ‘git reset’ lets me undo commits that I haven’t pushed.


> It works in conjunction with the command line

Do you know of git GUIs that explicitly maintain a bijection between the GUI and the underlying command history? It'd be cool to use the GUI and see the command history, or use the CLI and see updates in the GUI.


I'm not sure if this is exactly what you're looking for, but maybe close? SmartGit has an Output window that shows the underlying Git commands that it uses when you drag things around or use its commands. And if you make changes in the command line and then switch back to the SmartGit window, it updates to match.


Git-fork does this.


> use the CLI and see updates in the GUI

Which GUI tool does not?


[flagged]


> I know you're trying to sell your SyntEvo(R) [sic] SmartGit(TM)

That is a remarkably uncharitable comment.

If you have any evidence that I am a shill for syntevo, and not just a happy customer, you should state it.

Otherwise you owe me an apology.


I'm also a non-shill advocating SmartGit at any opportunity. Is it so incomprehensible for FOSS users that there are commercial products that are so great that they have a dedicated fan base s opposed to half baked open source crap?


It has always bugged me that

    git commit --amend 
Doesn't automatically resign (or fail to resign if the key isn't available) a signed git commit message. Anytime there is a non-verified git commit message in my history you can be sure it's because I was a dummy on the original message.


Wait, seriously? I sign all my commits as habit and I `--amend` regularly. If this means my signature histories are broken…


  git config commit.gpgsign true


This is great! Thanks! I'll use the global setting:

    git config --global commit.gpgsign true


I already have this set, which is how I sign all my commits. If that ensures `git commit --amend` re-signs, then I'm happy.


AFAIK, it does.


This flowchart[1] has been useful to me a few times when I've been lost on how to fix something. I usually keep it as a bookmark, but I don't have it saved on this computer, so I just did a google image search and was able to fine it fairly quickly. A google image search for "git solution workflow" or "git fix flowchart" finds it right away, in case this tickles your brain in the future and you want to find it again, and vaguely remember it's easy to find through image search.

Edit: Updated the link to go to the site referenced in the image. Might as well send you to who developed it so they get some credit.

1: http://justinhileman.info/article/git-pretty/


Why does the rebase command here include the --force flag? I don't think it's necessary.


Git is one of those once-a-decade technologies that's compicated, user un-friendly, takes months to master and is absolutely worth it.


Git shouldn't take months to master if you can understand it conceptually rather than by use-cases. If instead of trying to learn commands to affect the working directory, staged files, or history, you learn how git organizes historical tree of commit hashes, then base what each operation does, it becomes much clearer. It's similar to trying to learn strings of shell pipe commands to get things done rather than realizing that there's pieces of work that each command can do. One tip is to use (even very temporary) branches rather than `stash`es unless it's absolutely short lived. And if you ever get lost, `reflog` is your friend.

For me, I had trouble first using `git` after experience with `cvs` and `subversion`. At some point, everything clicked because I inferred its internal model.


git’s problem to newcomers - even people without any prior experience with diff-paradigm systems like SVN and TFS - is the idea of a commit representing a snapshot of a file system is difficult to comprehend because it feels so crazy and impractical (especially as CS101 makes a huge deal about computational complexity). The fact that git internally is actually quite efficient is buried in heavy articles about git’s plumbing.

Oh, and the fact that a ‘git checkout’ is an unrelated concept to ‘svn checkout’ - and how SVN branches are “spatial” compared to git’s “parallel-universes” model is also a source of confusion.

If I could go back in time to 2005 (in addition to getting in super early on bitcoin) I’d beg Linus to not reuse SVN terminology. Personally I’d go with “git switch” instead of checkout, and “timeline” or “line” for short instead of “branch”. The fact that git branches are not 1-connected graph routes is enough reason not to call them “branches”.

I’d also convince him to include a “gitd” mode that would automatically fetch from upstream and automatically add commits when it detects file-move/rename changes. (I understand why git doesn’t have an exact “rename” op, but tooling today still isn’t good enough to detect rename-then-edit or rename-then-split operations without an intermediate rename-only commit. My dream “gitd” would also make auto-commits every 30 seconds just to give me a powerful filesystem undo feature. If you had hours of work that resulted in new files before you staged them and you accidentally did a hard reset then you’re sol - and this has happened to me already :/


IMHO, the main idea of git is immutability. See the following comparison of git and synergy in a migration study. https://github.com/24eme/eurocontrol_synergy2git/blob/master... The git description is far shorter, but also more complete.


In practice, git isn't immutable - people rebase their commits after pushing, or go back in time to remove a file that shouldn't have been there and reindex, and so on.


These actions are not modification, but creation of an alternate reality (commit tree). This gives trust that if we are not happy with this alternate reality, we have some time to revert to the original one (untill garbage collector remove it).


It makes complete sense for programmers learning git to also learn some relevant underpinnings of what git is doing (not necessarily how it achieves it so efficiently). That is a much quicker route than having users guess at similarities with things they've used or imagine while using it and being surprised with it veers off expectations.


When I used to use NetBeans the main feature I loved was the history feature.

Basically every save to every file was stored and diffable in a kind of self contained repo.

I've missed this feature in every other editor since.


IntelliJ IDEA has that feature (local history)


Can confirm.


NetBeans is a wonderful IDE for this reason.


It only takes a couple of hours to start being productive while using it though. All the commands you need to know are add, commit, push, pull, branch and checkout. Perhaps add log and reset to that list if you make errors.


Why is it worth it? I've been happier with other vcs.


Curious, what do you currently use for version control?


I'm not that poster, but Mercurial is just as powerful and much easier to use.


That's what everyone says, but for me, coming from a rebase heavy git workflow, I've found Mercurial far more difficult to learn.

For example, with Mercurial, there's at least four different ways to do a rebase-ish thing: transplant, graft, rebase, and rebase (w/ evolve enabled). It's not obvious which a newbie should pick (rebase+evolve... I think?). Likewise, Mercurial has purge and strip which both delete commits in different ways. Git has multiple ways to do the same thing, but at least it's simple and consistent when you lift the hood.

Undoing any sort of rebase-ish operation in Mercurial also seems difficult and janky. It seems to take multiple steps, and involves unbundling some sort of patch file stored underneath your home directory. Whereas in git, you just update a pointer: `git reset --hard $BRANCH@{1}`. Git's reflog is such a fantastic safety net. Doing any sort of history rewriting in Mercurial feels very dangerous, in comparison.


For example, with Mercurial, there's at least four different ways to do a rebase-ish thing: transplant, graft, rebase, and rebase (w/ evolve enabled). It's not obvious which a newbie should pick (rebase+evolve... I think?).

You're cherrypicking--no pun intended.

By default, Mercurial doesn't do any of these operations; you have to activate extensions.

If I were a newbie coming from Git and I wanted a similar workflow using Mercurial, I would probably start with rebase.

But these days, the Evolve extension is the way to go--using it is a lot easier than anything I've seen or experienced using Git: https://www.mercurial-scm.org/doc/evolution/user-guide.html.

I find it kinda ironic when in this message thread, people are praising a chart like this--http://justinhileman.info/article/git-pretty/. It feels like the DVCS version of Helsinki Syndrome…

By design, Mercurial makes it much harder to shoot yourself in the foot.

In contrast, there's an entire cottage industry (https://ohshitgit.com and the like) to help when you--inevitably--get into a bad situation with Git.


> By default, Mercurial doesn't do any of these operations; you have to activate extensions.

graft is actually built-in, but that's beside the point. Coming from a rebase heavy workflow in git, things like updating a ref or abandoning a commit feel like fundamental operations, and when moving to Mercurial it wasn't obvious which of the built-ins or bundled extensions I needed to be reading about to do these things.

I did eventually find Evolve, as you suggest, but it's not one of the bundled extensions, and it's not something you'll find in the official tutorial, or "The Definitive Guide", or even in most of the SO answers explaining how to do git-like things.

> By design, Mercurial makes it much harder to shoot yourself in the foot.

This is true, but git makes it so easy to recover from those mistakes.


Fortunately "transplant" is just an old extension, so you can just forget entirely about it.

"Graft" copies; "Rebase" moves.

Alternatively just forget about graft also, and use "rebase --keep" to copy.

"Rebase with evolve" is conceptually still rebase.

Mercurial has strip (removes changesets from repository, by default stores a backup). Mercurial does not have purge.

Evolve has ~purge~ prune (marks changesets as obsolete).

Evolve is similar to a git reflog.

Evolve stores more contextual information than the reflog, so Mercurial+Evolve it is safer and easier to undo things than in Git+reflog. Mercurial also pushes some of this contextual information, so even collaborative history rewriting is possible in a safe and easy way, unlike git.


You surely don't know much about Mercurial.

"purge" has NOTHING to do with commits. "purge" only removes file not tracked by Mercurial.

"graft" replaced "transplant".

"graft" copies commits while "rebase" moves commits.

These commands all have very clear purposes, unlike those in Git.

It's just that you haven't really bothered to spend a few minutes on learning them.


purge is a third-party extension; it removes untracked files as you wrote.

But ~purge~ prune is also a command of the evolve extension, that is conceptually similar to strip, as GP wrote.


evolve doesn't have a purge command. It has a prune command.

purge is not a third party extension. It is distributed along with Mercurial.


You are correct, thanks.


I don’t do any of the things you describe.


This page is from 2015 yet hasn't once came up on my searches to do a bunch of these things. Thanks for resharing.



Managed to miss this first time, this is cool.

Also Merry Christmas and Happy New Year.


Except you can't undo git reset --hard unless of course you build a safety net. https://gist.github.com/chx/3a694c2a077451e3d446f85546bb9278


For several months now I've been wondering how to get rid of a 700 MB data file that was accidentally committed to our shared git repo. Now everyone has it, and a clean pull takes forever. Appreciate any thoughts.


You should be able to run a command that cleans the git tree of the file in all commits. I’ve had to do it because of committed passwords in files.

https://stackoverflow.com/questions/35115585/remove-file-fro...


When using `git filter-branch` it's much faster (up to 100x) to use the `--index-filter` option rather than `--tree-filter` as it only updates the git history and not the working directory. Docs and examples at https://git-scm.com/docs/git-filter-branch


Like others comment it's possible to scrub it from the repo entirely but this will change every commit hash (I'm guessing back to the commit where the file first appeared). It's probably something you need to coordinate carefully among all the teams. Have no open branches, scrub it, force push that then people can resume work.


Not reading. Nothing is ever as good as this flow chart: http://sethrobertson.github.io/GitFixUm/fixup.html


> You started a new branch feature based on master, but master was pretty far behind origin/master

I’m bad at git. So... wait what now?

When is Master not Origin/Master? I don’t understand what is going on there. Explain?


It means your local master is behind remote origin/master. (i.e. you needed to git pull)


OHH... ok! That makes sense. My last two projects I’ve been the only one committing so this pretty much can’t happen. Thanks.


It's missing git revert merges

  git revert -m 1 88113a64a21bf8a51409ee2a1321442fd08db705


PSA: You can safely refer to those long "commit-ish" identifiers using just the first 7 or 8 chars (eg `88113a64`) without real risk of collision. Let alone in examples / gists!


Git will even warn you if the hash prefix you provide is ambiguous!

I had a collision the other day on a mediums-sized when using a 6 character hash and was surprised. Git let me know and told me which objects collided.


Wow, that's super unlikely. How many commits did the repo have?

According to [1] using 3 letters you'd need at least 72 commits to have at least 50% chance to observe a clash. Using 6 letters you'd need at least 3977 commits, lol.

[1] https://en.wikipedia.org/wiki/Birthday_problem#The_generaliz...


I’ve been using git for going on ten years. You don’t think I can pull off just one commit/day?

I use six character hashes, I don’t recall that I’ve had a collision yet. But I guess I’m “due”. :-)


How is that unlikely? 4k commits is nothing. Any project with a dozen developers is going to reach that easily.


But can it also undo the undo?

Anyway, shouldn't git just come with a "undo" command?


Yes


git-extras has a tool called `git undo`. It's just a fancy wrapper around `git reset --soft HEAD` though.


Well you never know what these wrappers and aliases do, and ultimately need to understand what goes on under the hood.

Intellij has a "shelve" command, I decided to stick with "git stash". Magit has the "i", that does "ignore" but also cleans up already committed files, something like "git rm -r --cached ."


I use GitUp.


TLDR: Reflog. That is all.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: