OdieO
OdieO

Reputation: 6994

Git resetting a branch

enter image description here

So, I made some changes and committed them in the second commit - "wslanguagelabel - color changes". These changes are now on master. I then started a new branch - replies.

However, I want to get rid of the changes in the second commit shown - "wslanguagelabel - color changes" - so that master now points to "blocking - works", and then keep working on the replies branch.

Here's my question: If I hard reset master to "blocking - works", will this cause problems when I eventually merge replies? This is where I don't understand git well enough, and what exactly goes on under the hood. I'm worried that replies will still have changes from the second commit, which I want to get rid of, but I'm not sure if my thinking there is correct.


BLAZ's answer below is correct for the simple case of removing the commit from the branch.

MU gives a more complete answer in the responses to my comments.

Upvotes: 0

Views: 397

Answers (3)

torek
torek

Reputation: 487755

I think this will help: think of the green labels, replies, master, and trishtext, as just that: labels, on green sticky-notes or similar.

These green sticky-note labels are pasted onto the circles, which are the actual commits.

You can peel off a label and stick it on any other commit circle, at any time.

If a commit circle has no label, and is not connected directly to another labeled commit "above" it, it becomes "abandoned". Eventually abandoned commits are garbage-collected.1 This gives you a chance to re-label them if you goof up, but it's best to think of them as gone, as they're kind of hard to find (git reflog helps find them, also git log -g, but it gets messy).

In all cases, the commits themselves—not the labels—make up the "commit graph". Each commit permanently records its parent commits (two or more if a merge, but usually just one parent commit). To see a commit sequence, git starts with a label like master or replies. That names one specific commit. Git looks at that commit and tells you something about it. Then, it moves back to that commit's parent(s), moving "down the line" from c1173c4 to af211ba, and tells you something about that commit, and so on.


Doing a rebase (as some have suggested) copies a series of commits. Here's a similar sequence shown in text form (not as pretty, no colors, etc., but otherwise the same idea as the snippet you showed):

* 1f33fa6 (master) D
* 6cb155d C
* 170af7a B
| * 4396360 (branch) checkpoint: got to state X
| * ba1bdf2 checkpoint more work
| * ace0be9 begin work on feature
|/  
* c55985d A: first commit

I can now git checkout branch—this gets me commit 4396360 as my working tree—and then git rebase master. What this does is copy the changes I made in the three "side branch" commits (basically, run git diff or git show on each one to compare it with its parent, to see what I changed each time) and attempt to make the same changes/commits on top of 1f33fa6.

If this succeeds, the result is:

  * xxxxxxx (branch) checkpoint: got to state X
  * wwwwwww checkpoint more work
  * vvvvvvv begin work on feature
 /
* 1f33fa6 (master) D
* 6cb155d C
* 170af7a B
| * 4396360 [abandoned] checkpoint: got to state X
| * ba1bdf2 checkpoint more work
| * ace0be9 begin work on feature
|/  
* c55985d A: first commit

I drew the new ones in shifted-over like this to make them mirror the old ones; "git log" wouldn't shift them over, and they'd have real actual SHA-1 values as well, but this is to give you the idea. I also kept the old commits, but marked them "abandoned", because they would be: after copying all the commits, git moves the label, branch, to mark the newest of the new ones, which means the old ones no longer have a label.

If I use git rebase -i, I can drop one or more of the original commits, and git will do its best to copy only the changes I keep. The three old commits will still be there, but with no label, they are quite hard to find, and the new commits will omit the one(s) I dropped.


If you simply move labels about—this is (half of) what git reset does2—then you get no new copies of old commits, and no commits get changed. It's just that the labels move.

You were asking about how this affect a future git merge; the answer to that is: "it depends".

What git merge does is sort of simple to describe, although the details get quite difficult sometimes. A regular merge has two possibilities:

  • "fast forward", or
  • "true merge".

Git starts by looking at the current branch, and the commit that you give it. Let's look at your current situation:

* c1173ca (replies) working
* af211ba (master, trishtext) wslanguagelabel - color changes
* d38eaad blocking - works

Let's say you git checkout master. This puts you on branch master, which is also commit af211ba. The contents of that commit go into your working tree. Now say you use the commit git merge replies. Git must resolve replies to a commit ID (c1173ca, in this case). It then looks at the history of your current branch, master, and the history of the named commit, c1173ca.

The parent of c1173ca is af211ba. That's your current commit! "Wow, this is an easy merge," says git; "all I have to do is slide the label forward one commit, to c1173ca, and check out that version and I'll be all done!" The result is this:

* c1173ca (master, replies) working
* af211ba (trishtext) wslanguagelabel - color changes
* d38eaad blocking - works

That's a "fast forward" merge. Git says: the history of the thing you're merging-in connects straight back to the history of the current branch, so I can just slide the branch label forward across all those commits—in this case, the one commit—and I'm done.

Suppose you use git reset to "move the label back one step", though, so that you start here:

* c1173ca (replies) working
* af211ba (trishtext) wslanguagelabel - color changes
* d38eaad (master) blocking - works

If you now git checkout master and git merge replies, git has to go through the same process. The history starting from c1173ca goes to af211ba, and then af211ba has d38eaad as its parent, and wow, hey, it's another easy case! Git can once again do a fast-forward merge, sliding the label master forward (or upward) two nodes.

A "real merge" occurs when you have a situation where sliding the labels around won't do the trick. For instance, in the graph I showed, git merge branch can't slide master around to point to commit 4396360. If it did, commits 170af7a, 6cb155d, and 1f33fa6 would be abandoned. Instead, it has to do a real merge, producing a common working tree and making a two-parent commit (I drew this by hand, actual git log --oneline --graph etc output may differ slightly):

* (master) Merge branch 'branch'
|\
* | 1f33fa6 D
* | 6cb155d C
* | 170af7a B
| * 4396360 (branch) checkpoint: got to state X
| * ba1bdf2 checkpoint more work
| * ace0be9 begin work on feature
|/  
* c55985d A: first commit

Whether you get into such a situation depends on what commits you make. (You can force git to make a real merge, a two-parent merge commit, even when fast-forward is possible, with git merge --no-ff, but if you need that you probably know why, already. :-) )


1Whenever you peel off a label, it leaves a bit of "residue" as it were (in the "reflog"), a sort of ghost-image saying "I was once labeled replies" (or whatever the label was). It's after the reflog entries expire that the commit node really goes away.

2The other half is manipulation of the "index" or staging-area, and the third half—git reset has three halves :-)—is how git reset --hard throws away the current work tree and replaces it based on the commit you're resetting-to. Only --hard does that; the other git reset modes mean "move the current branch label and/or fuss with the index/staging-area."

Upvotes: 1

Anshul Goyal
Anshul Goyal

Reputation: 76837

Instead of checking out a branch on the latest commit, and then resetting HEAD, you should checkout the branch replies at that specific commit itself, like below

git checkout -b replies <SHA_of_commit>

This way, your new branch will be at exactly the commit you need to be, and you won't have to do any hard resets on your master branch. The merge should also work seemlessly fine, unless you change the same set of code in the replies branch as you do in master branch in subsequent commits.

Upvotes: 1

Blaz Bratanic
Blaz Bratanic

Reputation: 2279

Branch replies will retain the changes from the second commit. You could selectively remove the second commit from its history with:

git checkout replies
git rebase -i HEAD~2

where you delete the line with the second commit.

Upvotes: 1

Related Questions