DuckCowMooQuack
DuckCowMooQuack

Reputation: 105

Moving what commit a branch was created on

Ok, so here's the deal. I created a branch (master) off of a commit (2f6ea37), and then create another branch (develop) off of master. Changes were made on develop. The problem is, master should actually have been created off of a previous commit (df7992e) instead of (2f6ea37).

How do I change where master was created without loosing the new stuff committed to develop?

Here's how it is now:

branch1 --> <2f6ea37> --> <df7992e>
                                   \ 
                                    master
                                          \
                                           develop --> <new_commit>

Here's how it should be:

branch1 --> <2f6ea37> --> <df7992e>
                     \ 
                      master
                            \
                             develop --> <new_commit>

Upvotes: 0

Views: 44

Answers (1)

torek
torek

Reputation: 488183

TL;DR

You probably just need three git branch -f commands to do what you want:

git branch -f master 2f6ea37
git branch -f branch1 df7992e
git branch -f develop 2f6ea37

But to do this with git branch -f, you must be on some branch name other than any of these three, so you might want one git reset --hard or other command. If develop already has additional commits, you have a bigger problem. See the long explanation below.

Long

First (and this is somewhat minor, but it really does matter), the arrows in your diagram are wrong because they're forwards. :-) Git does everything backwards. So a commit like df7992e will point backwards to its predecessor 2f6ea37.

Branch names like master and develop just contain one raw hash ID, i.e., they point directly to specific commits. So I would draw this as:

2f6ea37   <-- branch1
    \
  df7992e   <-- master
      \
       C <-D <-E   <-- develop

To avoid mucking about with unreadable hash IDs, I'd then just call 2f6ea37 A, and df7992e B:

A   <-- branch1
 \
  B   <-- master
   \
    C--D--E   <-- develop

Now, the thing about branch names in Git is, you can always stuff a new hash ID into any branch name. That branch immediately now just points to the hash ID you gave it.

The way branch names are expected to move, over time, is so that they accumulate new commits:

A--H   <-- branch1
 \
  B--F--G   <-- master
   \
    C--D--E--I   <-- develop

for instance has moved all three branch names in "expected" ways: branch1 now identifies the commit with hash H, which points back to the commit with hash A, which branch1 identified earlier. So that's fine. Similar arguments work for the other two.

You apparently want the name master to identify commit A, and the name branch1 to identify commit B. (We'll worry about develop in the next section!) You can achieve this part by just forcefully shoving the right hash IDs into each name:

A   <-- master
 \
  B   <-- branch1
   \
    C--D--E   <-- develop

But if we compare this to the status a moment ago, the name master has moved in the "wrong" direction. It used to name commit B and now names commit A instead. The name branch1 has moved in the "right" direction (forwards, against the internal backwards-pointing arrows).

Moving a branch name backwards like this confuses other Git repositories (and/or their users) if they have seen your master identifying commit B before. Remember, all Gits everywhere share the raw hash IDs, so if any other Git has commit B, they could have some of their names remembering it. They might have their origin/master pointing to their copy of shared commit B, and as a result, they might have their own master pointing to B too.

If no one else has ever seen this, you're fine. Just use git branch -f and/or git reset --hard to stuff the correct hash IDs into the correct branch names.

The potentially large thorn here

The last issue is the branch name develop. Here, I've drawn it pointing to commit E, with E pointing back to D which points back to C which points back to B.

I believe that, at the moment—based on the way you drew your initial diagram—what you really have looks more like this:

A   <-- branch1
 \
  B   <-- master, develop

That is, commits C-D-E simply don't exist. If so, we just need to move develop to point directly to A too, with the same caveat about moving develop "backwards" that we had about moving master the same way:

A   <-- master, develop
 \
  B   <-- branch1

If you now git checkout develop and make a new commit C, the new C will point back to A:

  C   <-- develop (HEAD)
 /
A   <-- master
 \
  B   <-- branch1

The attached (HEAD) here indicates that the branch you have checked out, at this point, is develop.

But what if you already do have existing commits C-D-E? That is, what if your current diagram is:

A   <-- branch1
 \
  B   <-- master
   \
    C--D--E   <-- develop (HEAD)

The problem here is that existing commit C already points back to existing commit B. Nothing about any commit can ever be changed once the commit is made. So you cannot change C at all. What you will have to do in this case is copy C, and thus also D and E, to new and improved commits. What's new and improved about them is that the new C—let's call it C'—points to A, and uses A as its source base. Similarly, the new D' points back to C' and the new E' points back to D', giving us this:

  C'-D'-E'   <-- develop (HEAD)
 /
A   <-- branch1
 \
  B   <-- master
   \
    C--D--E   [abandoned]

This too moves the name develop in an unexpected way: now we not only backed up over B, we first backed up over, and abandoned, E and then D and then C too. (Existing commit B can still be found by some branch name(s) so it is not abandoned.)

The command that will achieve all of this is git rebase. With your HEAD firmly attached to develop, you would run:

git rebase --onto 2f6ea37 df7992e

which uses raw hash IDs to say Excluding commits B and earlier, copy all commits from the current one backwards, with the new commits going after commit 2f6ea37. Then move the name develop to point to the last such copied commit. The exclusion means that the set of commits to be copied is exactly the right set—C-D-E—so we get just what we drew above.

It's now time to use the two git branch -f commands to swap the other two branch names, while we're still on develop. Or, we could swap them before starting the git rebase.

What if you don't have commits C-D-E to copy, but are on develop?

Suppose the actual picture right now can be drawn this way:

A   <-- branch1
 \
  B   <-- master, develop (HEAD)

You can use git branch -f to swap the two names branch1 and master, without changing any other commits in any way. But now you're left with:

A   <-- master
 \
  B   <-- branch1, develop (HEAD)

Using git branch -f develop will give you an error:

fatal: Cannot force update the current branch.

But you still need to move develop. There are lots of ways to do it:

  • Using git rebase: git rebase --onto master branch1: this excludes commits starting from B backwards, which is all commits, from the copying; copies zero commits to come after commit A; and then moves your current branch name to point to commit A.

  • Using git reset: git reset --hard master: this discards all in-progress work in your index and work-tree, re-setting them to match commit A instead, and moves your current branch name develop to point to commit A. The advantage of this is it's quick and easy and does just what you want. The disadvantage is that it wipes out any in-progress work you forgot to save somewhere. The rebase command will warn you.

  • Using git checkout: git checkout master; git branch -f develop master; git checkout develop. This is exactly as efficient as the rebase and reset commands, but requires typing in three separate commands. It does have the ability to carry uncommitted work in your index and work-tree in some cases (for gory details, see Checkout another branch when there are uncommitted changes on the current branch).

Conclusion

Branch names can be moved pretty easily. To move the branch you're standing on, either get off it (git checkout something else), or use git reset --hard or other sneakier methods. Remember that git reset --hard is destructive of uncommitted work.

You cannot modify any existing commits. If the adjustments you would like to make to the branch names can be done without this, you can simply adjust those branch names. Anyone else—some other Git repository you're supplying with commits, for instance—might get confused by "unusual" branch name movements, so make sure those other Git repositories and their users are prepared for that.

If you need to copy some commits, git rebase will do the job. This may also adjust your branch names in ways that other Gits who are keeping track of your branch names (and your commits) may need to prepare for.

Upvotes: 1

Related Questions