
Git Undo - dominicrodger
http://megakemp.com/2016/08/25/git-undo/
======
rjbwork
I freely admit to being an Hg fan, but that this stuff is accepted as common
practice kinda blows my mind. What's so wrong with keeping an accurate picture
of history that people do all kinds of manipulation to their history to keep
from the VCS system from accurately reflecting history of development?

~~~
chriswarbo
There seems to be a split among git users between those who think history
should show what actually happened (i.e. it's left alone), and those who think
history should tell a story about the changes (i.e. once you've finished
something, turn it into a coherent set of commits like "stub out X", "add
tests for X", "make first X test pass", etc.).

I agree with you, that history should be left alone; mostly I think of the
YAGNI argument that its futile to think that you have a better idea of what
future developers want to see, compared to those future developers themselves.

My repo histories are riddled with stuff like "finished X", "stubbed out Y",
"fix typo in X", but at least nothing has been hidden from future devs who
might be digging around for their purposes, regardless of whatever elegant
story I might come up with.

~~~
angry-hacker
But what good does it have for future devs if the history is

\-- Added this thing \-- Fixed typo \-- Capitalized the letter

Etc.

~~~
chriswarbo
One important reason is to avoid wasting time on gilding lilies.

Another reason is that the git information (e.g. from git blame) tells us when
the code was written and in what order, rather than some post-hoc
rearrangement.

For example, we might notice that code X is doing some tricky work which
elsewhere is done by a helper function Y. We look at the git info and see that
X was added after Y, so we try to figure out what special edge-cases X is
trying to deal with that Y wasn't suitable for. Little do we realise that X
was actually written before Y existed, but the commits got rearranged.

That kind of archeology is difficult to predict in advance (mostly because, if
we realised all of the issues with our code beforehand, we'd fix them
immediately!).

Future devs are just as capable at traversing repos and collapsing diffs as
you or I, so there's no need to lie to them. In fact, they might have access
to much smarter tools and IDEs than we do.

~~~
shados
Personally, I only care about when the code hit master. Because that's when it
could potentially have broken shit for everyone.

That I committed it locally is pretty irrelevant: I could just as well NOT
have committed it, made a backup of the files on the side, copied them back
in...from the perspective of the rest of my team, my local history is an
implementation detail.

If the only thing I do is manipulate my local history, then open a PR and
merge, master's history will actually show something much closer to the truth:
That on X date I added something to master.

That I spent 6 weeks and 300 commits locally to do it (kids, don't do this at
home!), literally doesn't matter to anyone.

~~~
TurningCanadian
> Personally, I only care about when the code hit master.

So just look for the merge commit on the master branch that brought it in.

By having 300 separate commits (which you were doing anyway) it helps us know
what your thought process was on the day that a given line changed. Maybe you
were refactoring function X to do Y. If you don't mention that you were
accounting for changes happening in someone else's branch, then we know we
have to look closer at that code. Without the individual commit, all we know
is that giant-project-x was accomplished with this commit, and the change to
that line may or may not have the necessary update.

~~~
sofal
By having a history of every single key you typed to create this comment, it
would help me know what your thought process was when you typed it up. Maybe
you got pissed off and wrote a swear word or two and then backspaced. Maybe
you worded something awkwardly and then refactored your sentence. Without all
of your keystroke history, all we know is that a comment was made by you, and
your opinion may or may not have taken into account certain arguments made by
others in the same thread around the time that the comment was published.

------
panic
I wonder how much time and money has been wasted trying to operate Git's
confusing UI. The repository format itself seems fine, but I'm surprised we're
not all using a better frontend by now.

~~~
Cthulhu_
One can argue semantics whether you call it a UI or a CLI, but hey. There's a
number of GUI clients out there that depending on your criteria could be
considered better. They're usually not as powerful as the CLI client though,
and if they are, features like accessing the reflog are hard to find and use.
There's also a few alternative CLIs out there, I've just done a quick googling
and came across [http://www.saintsjd.com/2012/01/a-better-ui-for-
git/](http://www.saintsjd.com/2012/01/a-better-ui-for-git/) and
[http://www.kennethreitz.org/essays/legit-the-sexy-git-
cli](http://www.kennethreitz.org/essays/legit-the-sexy-git-cli).

Personally I prefer the CLI, it's the only tool that I can rely on to do what
I tell it to do and to know what's happening. But it takes time and effort to
get used to it.

~~~
blakeyrat
A CLI is a user interface.

The problem with Git (well, one of many many problems with Git) is that it
conflates its user interface with machine interfaces-- which means tools that
have to work with Git (like those GUI clients) _have_ to use the CLI to do so.
They don't have a more powerful option, like an officially-supported API or a
shared library they could call into. This is terrible software design.

~~~
niftich
libgit2 [1]

In fact, there exist many alternative 'frontends' to Git. There are even
protocol translators like Hg-Git [2], and many importers that typically use
the fast-import format to ingest Git-impl primitives [3]

[1] [https://libgit2.github.com/](https://libgit2.github.com/)

[2] [http://hg-git.github.io/](http://hg-git.github.io/)

[3] [https://github.com/frej/fast-export](https://github.com/frej/fast-export)

~~~
blakeyrat
libgit2 is not official, not in-sync with the main Git tool development, and
doesn't support many of the features Git supports. So no, it isn't a solution
to the problem.

Separating the UI and machine interface is something that should have been
done from day one. In fact, I've been told Git's codebase actually _already
does that_ (it just doesn't expose the machine interface to the outside
world.) Human beings are not machines. They have entirely different needs.

------
derefr
Most git command-lines are lengthy enough to give me time to consider them, so
I don't often feel the need to "take back" a git invocation. What I do often
screw up, though, is the (almost hypnotic) tapping of y/n when doing a `git
{add, reset, checkout} -p` to prepare and clean up a commit.

Ideally, with all of the -p commands, git wouldn't actually apply any of the
changes I specified until it was about to quit (i.e. either when I advance
past the end of the set of potentially-affected hunks, or I manually type
'q'), and then would prompt me for whether {set of operations I specified} is
what I wanted to do. This would leave the -p operations the the flexibility to
expose an 'u'ndo.

~~~
arghimonmobile
You'd probably really like Magit mode for Emacs then. What you're describing
with interactive add becomes trivial since you can stage parts of the diff by
selecting them in a region. I can hardly live without Magit these days. Bonus
is that it even works on remote hosts via tramp. </emacs plug>

~~~
roblabla
<vim plug>With the vim-gitgutter plugin installed, you can dynamically
add/remove/undo hunks without even leaving the editor[0]. I use this instead
of `git add -p` nowadays</vim plug>

[0]: [https://github.com/airblade/vim-gitgutter#getting-
started](https://github.com/airblade/vim-gitgutter#getting-started)

~~~
hacker42
<spacemacs plug />

------
richardwhiuk
I worry about naming a function undo that doesn't necessarily undo what the
user expects. Undo has a strong user expectation, and I'm not convinced this
matches that.

~~~
Mchl
Git already has `git branch` which doesn't in fact do any kind of branching
but creates a label which follows commits when it's checked out.

~~~
panic
Git also has 'git revert' which creates a new commit, 'git reset' which
actually reverts (among other things), 'git checkout' which switches
branches... the fact that all the other commands are confusing doesn't mean
this new one has to be.

------
p4bl0
I have this alias in my ~/.gitconfig:

    
    
        cancel = reset --soft HEAD^
    

I don't want an alias to hard reset, it seems to dangerous and a good way to
lose some work. However a soft reset like this allow me to cancel the last
commit and add an omitted file, or remove one from the commit, or simply to
correct the commit message easily.

~~~
phaemon
Actually, if that's all you want, you can do:

    
    
      git add <file>
      # or "git rm --cached <file>" to remove
      git commit --amend
    

and it will replace with a new commit that has what you want. It's like a mini
rebase -i

~~~
p4bl0
True, but I prefer to be able to see my staged changes as a whole to be sure
that I did everything as I wanted.

------
dan00
Nice idea!

The only non intuitive thing might be, that calling e.g. 'git undo' twice
doesn't undo the last two changes, but the first undos the last change and the
second one undos the undo.

~~~
m_mueller
Could it be improved to have undo filter out its own reflog entries and
instead have a 'redo' to undo those?

~~~
dan00
I don't think that 'git undo' can tell with certainty which entries in the
reflog are from it.

It would be a crude heuristic that's going to break.

~~~
m_mueller
Is there maybe a way to annotate the commit message of HEAD without
influencing the reflog?

~~~
dan00
You can't change the commit message of a commit without changing the commit,
which in most cases is a bad idea, if you don't know pretty much exactly what
your doing.

Replacing one heuristic with another won't make this a stable operation.

A lot of people had already the idea to encode relevant information inside of
documentation and it was always a bad idea in the long run.

~~~
m_mueller
I get that, I just wondered whether it's technically possible (with supported
git operations, not hacking down at FS / byte level).

~~~
Dylan16807
If you want to undo without reflog entries, just snip the most recent line off
the reflog.

If you want to put annotations on commits, use tags with message bodies.

So yes, there are ways to accomplish what you want.

------
CyberShadow
Be careful with hard resets - they throw away working tree changes.
Ironically, it's one of the few git operations you actually can't undo.
(Usually I prefix such scripts with "git stash save" due to this.)

------
garaetjjte
It should automatically skip reflogs created by undo itself, so git undo; git
undo be equivalent to git undo 2, and for undoing undo there should be
seperate git redo.

~~~
ekzy
I agree with that. So you could undo step by step without thinking about the
number to put next to your undo command. It seems a bit trickier to implement,
though

~~~
garaetjjte
Only a bit of awk magic:

~/git-undo.awk:

    
    
      BEGIN { jmp = 0 }
      {
        match($2, "{([0-9]+)}", c);
        if (c[1] == jmp)
        {
          jmp++;
          if ($3 == "reset:")
          {
            match($6, "{([0-9]+)}", x);
            jmp += x[1];
          }
          else
            i--;
          if (i == 0)
          {
            print jmp;
            exit;
          }
        }
      }
    
      git config --global alias.undo '!f() { git reset --hard $(git rev-parse --abbrev-ref HEAD)@{$(git reflog | awk -v i=${1-1} -f ~/git-undo.awk)}; }; f'
    

Redo is also possible, but i don't have time now to do it.

------
ricardobeat
Warrants a big warning that using `reset --hard` will irreversibly wipe out
any uncommitted changes.

~~~
sleepychu
Came here for this. git undo won't restore the changes wiped out by reset
--hard? They aren't stored on the reflog?

~~~
panic
If it hasn't been committed, it won't be in the reflog. Working copies and
stashed changes are especially vulnerable to being overwritten by accident. It
would be great if git had a way to undo these operations too.

~~~
OJFord
You could alias reset to instead commit all and then reset.

~~~
panic
But what if I'm just trying to unstage something? I don't think shell aliases
can solve this in the general case.

------
eliasdorneles
I've also wrote about undoing things in Git and other productivity tips here:
[http://eliasdorneles.github.io/2016/06/19/on-getting-
product...](http://eliasdorneles.github.io/2016/06/19/on-getting-productive-
with-git.html)

------
Confiks
I always make sure to keep a `git log` in my terminal scrollback buffer, so I
can easily `git reset --hard` back to some revision. And if that fails there's
indeed always the reflog to fall back to.

------
dilap
if you're on a mac, the excellent & open-source GitUp graphical git client has
undo built right in. it also makes it easy to slice-&-dice your commit graph.

------
supersan
I wish there was a Dropbox for developers where you never had to worry once
about commits or history or tags. Maybe just a big green "Release" button and
that's all there is to it. Surely it won't be ideal for writing the Linux
operating system but for most of the cases that would be more than enough to
get the job done, keep eveyone in sync and yes a lot less confusing allowing
you to focus on things that really matter.

~~~
lmm
So what happens when two developers edit the same file, or make changes to
different files that in combination break the system?

You need commits, you need the ability to merge. If you don't want to force
all commits to happen online and everyone to resolve conflicts immediately
then you need branches. You want tags if you're going to have releases
(otherwise how do you refer to them?). At that point you basically have git.

All the complicated features were added because someone thought they needed
them (there are certainly a few git features where I think that someone was
wrong, but not many).

------
nihilisticape
The alias is only useful in the simplest use case. Where you just want to undo
the latest one change. Reusing undo to revert the undo is counterintuitive.

If you want to know how many steps back you need to undo, you still need to
check reflog. This means you're better of just resetting manually to the
change you want.

------
2T1Qka0rEiPr
I find reflog one of the neatest git features, and regularly use it to get out
of jail / help cherry-pick across branches. This seems a bit magical, but I
think there are some pretty neat lessons within for lots of users which still
makes this a great read.

------
tempodox
I find it wise to give git a command for reflogging. Flogging it only once
would definitely not be enough by a long shot. It might afford the tortured
git user some release, although it won't change git any.

------
mnx
This seems dangerous to get used to. I always thought the reflog was supposed
to be last-resort. Isn't it?

~~~
cjbprime
That doesn't sound right. There's nothing secret or internal about the reflog.
"reset --hard" is more of a last resort, but only because it wipes uncommitted
changes.

------
ChoHag
I have this. It's called cp(1).

~~~
tempodox
Granted, you can shoot yourself in the foot with cp(1), but not nearly as much
as git(1) lets you.

------
forrestthewoods
Every Git thread makes me so happy I use Perforce.

------
Grangar
What's wrong with git revert?

------
amelius
Why not just use a filesystem which supports snapshots? It would allow you to
go back without even invoking git.

~~~
throwanem
Why not just boil the ocean?

~~~
Dylan16807
Using a better filesystem isn't that hard. Sure, on windows you have to set it
up as a network drive, but that's a few minutes of effort.

