izaak_pyzaak
izaak_pyzaak

Reputation: 960

Checkout to old commit, modify code, then push detached HEAD to origin/master

I have checkout out to a previous commit on my master branch (leaving me in a detached HEAD state) and modified the code. Now I would like to push this version to origin/master, such that this version now becomes the newest commit. Visually, here is what I've done

This is how I started:

commits: A -> B -> C -> D (HEAD)

I git checkout to C:

commits: A -> B -> C (detached HEAD) -> D

Modified the code at C, leading to C':

commits: A -> B -> C' (detached HEAD) -> D

I then added and commited my modifications. Now I want to make C' most current commit on origin/master, such that it is committed ahead of D:

commits: A -> B -> C'-> D -> C' (HEAD)

I dont particularly care if the C' preceding D reverts to C or stays as C'.

I'm unclear what commands I need to issue in order to achieve this, anybody know?

UPDATE: I am also not bothered about keeping D. Therefore, perhaps it is possible to delete D, which would make C' the most current?

UPDATE: I ended up git rebaseing to commit C, thereby deleting D, and then re-adding the modifications to get C', then pushing C'. I'm sure there's a better way to do this...

Upvotes: 0

Views: 293

Answers (2)

Mark Adelsberger
Mark Adelsberger

Reputation: 45659

Well, your commit graphs aren't exactly accurate (and could use a little clarification). The reason to go into this, is it will help understand the solution when we get to that point. So:

This is how I started:

commits: A -> B -> C -> D (HEAD)

Now, if you're going to draw arrows between commits, they should point "backwards" in time, becuase git stores parent pointers but not child pointers. (I just draw lines instead of arrows.)

I assume the master branch (and the origin/master ref) are at D. Also since you're not showing detached HEAD state, I assume you have master checked out. I would normally draw that something like

commits: A -- B -- C -- D <--(master)(origin/master)
                                 ^(HEAD)

This shows where the refs are pointing, and reflects that HEAD is a symbolic ref pointed at master.

I git checkout to C:

So now we'd have

commits: A -- B -- C -- D <--(master)(origin/master)
                   ^(HEAD)

(This shows an advantage of drawing HEAD this way; just move it so it points to a commit, and we know we're in detached state.)

Modified the code at C, leading to C':

And this is where the above diagrams become inaccurate. This new commit does not replace C as you've drawn, but rather is a new child of C. So that's more like

commits: A -- B -- C -- D <--(master)(origin/master)
                    \
                     C'
                     ^(HEAD)

(which shows an advantage of drawing lines instead of arrows; easier to depict diverging lines of commits). But even that's not quite right, because by convention C' would mean "a new/rewritten commit that applies the same changes to its parent as C applies to Cs parent"; so we should call it something else.

commits: A -- B -- C -- D <--(master)(origin/master)
                    \
                     E
                     ^(HEAD)

And now we can address what you're trying to do.

Now, while you say in updates that you don't care about keeping D, you may not have considered all the reasons to keep it. If you were saying "I really want to throw D out of the history, and here's why..." that would be one thing, but if it's just "I don't care one way or the other", then you should consider keeping it. And here's why:

To remove D is a history rewrite. Once a branch has been pushed, doing a history rewrite on that branch can cause problems, particularly if the repo is shared with other users. See the git rebase docs under "recovering from upstream rebase".

Now, if you understand the issues with that - i.e. if you get that you need to coordinate with anyone else who has a copy of the ref, and that failing to do so could result in your rewrite being accidentally undone - and still want to discard D, then you can do it like this:

Rewrite Approach

Starting from where your original question left off, you'd move the master branch to your newly-created commit.

git branch -f master
git checkout master

which would give you

commits: A -- B -- C -- D <--(origin/master)
                    \
                     E <--(master)
                              ^(HEAD)

(In fact, the easier thing would've been to reset master to HEAD^ way back at the start, instead of checking out into detached HEAD state; assuming, that is, you knew you were going to do a rewrite at that point.)

Then you can push the rewrite of master, but you'll have to "force" the push. This is the "red flag" that you're going to cause an upstream rebase

git push --force-with-lease

In the event anyone else has added more commits to origin/master, this will fail. This is because completing the rewrite would risk losing their work, and at a minimum additional steps should be taken to address that. If you still want to override that safety check, you can say

git push -f

Be aware that neither this, nor any other, method actually deletes the commit D. It removes D from master history, which means it will probably eventually be deleted by gc.

commits: A -- B -- C -- D
                    \
                     E <--(master)(origin/master)
                              ^(HEAD)

No Rewrite Approach

If, on the other hand, you decide that a rewrite is more trouble than it's worth, you'd do something like this:

Again picking up where the original question left off, you probably want to preserve the changes you made so you don't have to start over.

git branch temp
git checkout master

Now revert the changes that were made in D

git revert HEAD

yielding

                          ~D <--(master)
                         /         ^(HEAD)
commits: A -- B -- C -- D <--(origin/master)
                    \         
                     E <--(temp)

The content (TREE) at ~D will match the content at C, so now you can just say

git rebase master temp
git checkout master
git merge --ff-only temp
git branch -d temp

So finally we have

                          ~D -- E' <--(master)
                         /               ^(HEAD)
commits: A -- B -- C -- D <--(origin/master)
                    \         
                     E

The original E commit is no longer of interest; the reversal of D (~D) and the addition of the changes from E (E') are on master and can be pushed normally.

Upvotes: 2

junkangli
junkangli

Reputation: 1202

If you simply want to rewrite your git history with your modified code at C', you can create a new branch:

git checkout -b <name_of_new_branch>

Then force push your new branch to the remote:

git push origin <your_branch_name> -f

Upvotes: 0

Related Questions