Suppose I do
$ git checkout master
$ touch foo.py
$ git commit -m "oops" foo.py
$ git checkout -b new_branch
$ touch bar.py
$ git commit -m "changes" bar.py
Now when I try to push back changes on new_branch, I get
Local branch 'master' is ahead of remote branch 'origin/master'
How do I reset master while not losing my changes (foo.py, bar.py) on new_branch?
I read the git reset page, and it looked like it might involve --keep, but I couldn't tell.
This can be very confusing initially, and what you needed was a proper introduction to how Git implements branches; but at this point we'll use the retrofit method. :-) The trick to understanding all this is that Git's commits are permanent and unchanging, but its branches—or more precisely, branch names—are temporary, and in fact mostly irrelevant.
There are three things that matter when you are building a new commit (they are HEAD
, the index, and the work-tree), but once you have the commit built and committed, it's quite permanent and it is very hard to get Git to lose it entirely. It's easy enough to accidentally misplace it, though, so let's try to avoid that. :-)
If we ignore the branch names entirely, we can draw a graph of the commits that exist in your repository. Given what you have done—making two new commits—let's draw them like this, where round o
s represent commits, and A
and B
are your two new commits:
...--o--o--o
\
A
\
B
We could draw them all on one line, but I want to leave room to write labels in on the right. Commit A
is your "oops", and B
is your "changes".
The main noteworthy thing about this graph drawing is that each commit points to (stores the hash ID of) its predecessor commit. This means that commit B
points back to commit A
. Commit A
points back to the next-most-recent commit, which points back still further, and so on.
Now we add the labels—the branch names. The last boring commit o
still probably has a label origin/master
. Commit A
has the label master
, and commit B
has the label new_branch
, so let's draw these in:
...--o--o--o <-- origin/master
\
A <-- master
\
B <-- new_branch (HEAD)
This is what branch names are and do for you: they are pointers to commits; they remember each commit's big ugly hash ID for you.
When you are on some branch and make a new commit, the branch name comes along for the ride. The special name HEAD
remembers which branch you're on, so that Git knows which name to move to the new commit. (We needed this temporarily at least, when master
and new_branch
briefly both pointed to commit A
.)
What you want to do now is move master
back to point to the last of the boring o
commits. To do so, you can use git reset
, which lets you move a name in an arbitrary way:
git checkout master
git reset --hard origin/master
This assumes (see final section below) that origin/master
really does point to the last of the boring o
commits. The git reset --hard
says: Wipe out my current index and work-tree, and move my current branch—according to HEAD
—so that it points to the commit I name here. We must git checkout master
first, so that HEAD
names master
. Then the git reset
does this:
...--o--o--o <-- master (HEAD), origin/master
\
A
\
B <-- new_branch
So now your two new commits A
and B
are found only via new_branch
and not via master
.
(There are several more things that branch names do for you. In particular, they protect commits from being removed by Git's "garbage collector". If a commit has any name by which we can find it, it's protected. If it has no name, it's no longer protected. There are some semi-hidden names that protect everything for a while—at least 30 days by default—to make sure commits don't get trashed accidentally, but finding them through these reflog names is annoying, so we try not to rely on it so much.
The branch names are also used for git push
, so there they matter a fair bit.)
(I mention "trashing" the index and work-tree above, which isn't completely true. The git reset
command, used the way we're using it here, has three jobs it can do:
It always does the first, optionally adds the second job, and then optionally adds the third. The --hard
mode does all three, the --mixed
mode does the first two, and the --soft
mode does only one. To understand them all properly, we would have to look closely at the definitions of index and work-tree and I'll leave that for other SO questions/answers. The key item here, though, is that you don't want to git reset --hard
until you have everything saved away in commits.)
origin/master
(or it points back too far)?We assumed, above, that there was a handy label—a so-called remote-tracking branch—identifying the commit we want to git reset
our master
to point-to. If we don't have this label, or it points to an even earlier commit, we must locate that commit some other way.
There are a lot of ways to do this. The most straightforward is to go by its hash ID. If you run git log
while on some branch, you see each commit that is "on" or "contained in" that branch, normally in Git's usual backwards-going order. For instance, we might git log
and see commit B
, with its big ugly hash ID, then commit A
with its hash ID, and then that boring commit o
with its big ugly hash ID.
We can use the raw hash ID:
git checkout master && git reset --hard <hash-id>
if we don't have the label. If we do have the label, we should probably just use it.
You might remember to get help from "A DOG" with git log
:
git log --all --decorate --oneline --graph
The all makes Git show all branches and all remote-tracking branches (and everything else: tags, the "stash", notes, etc). The decorate option attaches the label names to commits. The oneline option makes the output show just one line for each commit, and the graph option makes Git try to draw the commit graph.