Create new branch from specific commit onwards

Refresh

April 2019

Views

1.8k time

1

I want to create a new branch, starting from a certain commit. However, I want all the commits that were commited afterwards to be part of that branch too.

Lets assume this is branch-D with all the commits.

> branch-D
commit-A ---- commit-B ---- commit-C ---- commit-D

I want to remove everything from commit-B to a new branch, lets name it branch-C, and only keep commit-A on branch-D

> branch-D
commit-A

> branch-C
commit-B ---- commit-C ---- commit-D

I haven't pushed anything to a remote repository yet. Is there a way to do this?

4 answers

0

You can use:

git checkout hashOfCommitA

then

git checkout -b NewBranchName

now You have new branch with commit A. Then You can go back to commit D

git checkout nameOfFirstBranch

and then revert commit A from this branch

git revert hashOfCommitA

now You have to branches on one of them there is only commit A while on the other are commits b,c and d.

-1

The most straight forward way to do this would be to create 2 new branches off the commit prior to commit-A. Then cherry-pick commit-A to 1 branch and cherry-pick commits B,C,D to 2nd branch.

3

The peculiar thing about Git is that branch names don't affect what's in your commit history, except that they let you—and anyone else, which is very important—find the commits. The secret is that in Git, a commit is, potentially, on many branches at the same time.

Importantly, though, no commit, once it is made, can ever be changed. It can be copied to a (new, slightly different) commit, but it can't be changed.

Also, while any branch name can be forced to point to any commit, there's a "normal direction" for branch-name motion: normally, they only advance. Advancing, as we'll see in a moment, means that whatever commit they used to point to, is still "on" that branch. So once you've given your commits to someone else—some other Git—and asked them to call those commits branch-D, it's hard to get the other Git to retract that. Hence:

I haven't pushed anything to a remote repository yet. Is there a way to do this?

Yes, and there may be a very easy way: the "haven't pushed anything yet" means that you're the only one who has these commits, so however you find them, that's how everyone finds them, because you're "everyone". :-) There's no need to get anyone else to change anything.

As long as they're already laid out the way you want, you just need to rearrange the way in which you find these commits.

Let's draw your existing series of commits the "Git Way":

... <--A <--B <--C <--D   <-- branch-D

In Git, each commit is uniquely identified by its hash ID. These things are big and ugly and apparently-random (though they're actually completely deterministic), so we hardly ever use them, or use an abbreviated form like d1c9d3a. Instead of that, let's just call the last commit D.

Inside commit D, there's another hash ID, identifying D's parent commit. Let's say it's c033bae, but let's just call that "commit C". So we say that D points to C.

Similarly, C points back to B, which points back to A, which points back to ... well, perhaps the tip commit of master—you didn't say, but let's assume that for now:

...--o--o   <-- master
         \
          A--B--C--D   <-- branch-D

This is a more compact way to draw them. Commits always, necessarily, point backwards, so we don't really need the internal arrows—we know they always go backwards. Branch names, however, like master and branch-D ... well, we can make those point anywhere. What we want to do is to make them point to the "tip commit" of a branch.

Git finds commits by starting from the one that the branch-name points to: D, for branch-D, or the final o on the first row for master. Then it looks at the parent of the current commit, to move backwards. Then it looks at the parent's parent, and so on. Hence the two o commits are on both master and branch-D: we can find them by starting from master, or we can find them by starting from branch-D and working backwards four steps.

This means that the picture we want looks, maybe, like this:

...--o--o   <-- master
         \
          A   <-- branch-D
           \
            B--C--D   <-- branch-C

Here, the commits that are on master are also on both branches, and commit A, which now is the tip of branch-D, is still on branch-C as well. It's just not the tip of branch-C any more.


On the other hand, perhaps the picture we want looks like this:

          ?--?--?   <-- branch-C
         /
...--o--o   <-- master
         \
          A   <-- branch-D
           \
            B--C--D   <-- ???

That is, we need to answer a question. The name branch-C will point to some particular commit. When we go three steps back, should we arrive at commit A? Or should we arrive instead at the last commit on master?


If the first picture is the right one, the answer is easy: make the new name, branch-C, pointing to commit D; then force the existing name branch-D to move back to commit A. To do this:

git branch branch-C branch-D  # copy the hash ID from branch-D to new branch-C

and then, depending on which branch is checked out right now, either:

git reset --hard <hash-of-A> # move current branch; re-set index and work-tree

or:

git branch -f branch-D <hash-of-A> # force branch-D to point to A

Note that before using git reset --hard, it's a very good idea to make sure that you have nothing modified that you want to save. While commits are pretty much permanent (you can get them back, typically for at least 30 days, even if you kick them off all branch names), the index and work-tree that git reset --hard clobbers are not.

If you want that second picture, though—if you want the parent of commit B to be something other than commit A—you'll have to copy commit B to a new, different commit, that's "like B, but...". The differences between the original B and the copy will include the changed parent hash ID.

For that, you will need to use git cherry-pick or an equivalent (such as git rebase, which is basically an en-masse cherry-pick operation that copies lots of commits). To do that, you could:

git checkout -b branch-C master

giving:

...--o--o   <-- branch-C (HEAD), master
         \
          A--B--C--D   <-- branch-D

and then run three git cherry-pick commands to copy B, C, and D; or simpler—this uses the <commit>~<number> notation:

 git cherry-pick branch-D~3..branch-D

to copy all three at once. This produces:

          B'-C'-D'   <-- branch-C (HEAD)
         /
...--o--o   <-- master
         \
          A--B--C--D   <-- branch-D

At this point, it's safe to force branch-D to point to commit A:

git branch -f branch-D branch-D~3

You can do this by hash IDs, as in the earlier examples, too:

git branch -f branch-D <hash-of-A>

All we're doing with branch-D~3 is telling Git: count back three parent steps, one parent at a time. So we start at D, count back one step to C, count back another step to B, and count back a third step to A.

1

Update - I misread your diagrams and had mixed up the branch names as a result. Also as torek points out, a "more correct" reading of your diagrams includes some ambiguities. I'll keep my answer mostly as is (but with branch names straightened out) as I believe it conveys the main principles; and I'll at a bit about using rebase if you need to further edit branchC history; but for a more detailed answer, see torek's response.


First let me get to the solution I think you're looking for; but I suggest reading beyond that, because there's conceptual confusion in this question that would be worth clearing up.

So you have now

A -- B -- C -- D <--(branchD)

I think you want to end up with only A on branchD, and a new branch for B, C, and D. So step one is, with branchD (or D) checked out, create the new branch

git branch branchC

Then move the branchD branch back to A.

git reset --hard HEAD~3

(I used HEAD~3 because in this example, that would be one name for A. It depends on the number of commits being taken out of master's history. It would always be ok to use the commit ID (SHA) of A in place of HEAD~3.)

After these steps, you have

A <--(branchD)
 \
  B -- C -- D <--(branchC)

which looks like what you described, but we didn't get there the way the description implied.

I didn't move commits; I moved branches. This is much more straightforward in git, and it reflects that commits do not "belong to" branches in git the way they do in some systems. Consequently the statement "branch at a previous commit but include the following commits on that branch" doesn't really make sense in git.

The ambiguities in the before-and-after diagrams in the question do leave open the possibility that I've misunderstood exactly what branchC should include in its history. If branchC is not meant to include A, then you have to rewrite B, C, and D - and you can most easily do that with git rebase -i. After creating branchC and moving branchD you could say

git rebase -i branchD^ branchC

Here branchD^ is a possible name for "the commit before A. If there is such a commit, it may have other names - perhaps master, or certainly its commit ID. If there is no such commit, then I suppose you could say

git rebase -i --root branchC

(but in that scenario, trying to remove A from the branchC history probably makes no sense, so I doubt that's the what's going on here).

The rebase command will bring up a text editor with a TODO list with an entry for each commit. You can delete the entry for commit A from the list, then save and exit. This won't disturb branchD - it will still point at A - but it will create a new history for branchC by copying B, C, and D. so then you would have

x -- B' -- C' -- D' <--(branchC)
  \
   A <--(branchD)

In any event, to do what you want means removing B, C, and D from the history of branchD. You mentioned that these commits have not been pushed, so it should be no problem; but that is an important distinction to keep in mind. Any time you want to remove a commit from a branch's history, it's easier to do if the branch on the remote doesn't yet know about the commits in question.