简体   繁体   中英

How can I git rebase a single commit onto master and push as new branch

I made a wrong commit commit_c on branch branch_a .
How can I git rebase a single commit ( commit_c ) onto master and push as a new branch branch_b .
So that i have the same scenario as if i would have created a branch from master, did the same changes like in commit_c and pushed the branch_b .
Note: I want to rebase the commit, I don't want to cherry-pick, since that would create a new commit_d right?

No commit, once made, can ever be changed. So you must make a new commit regardless of everything else.

It's important to understand this about Git: commits are what matter, and commits are forever. You have a commit you don't like. You will need to make a new commit—"new and improved", as it were—to use instead.

Another thing to know about commits is that every commit has a unique hash ID. That hash ID is now (and forever) reserved for that commit. Every Git repository everywhere will use that hash ID for that commit, if the repository has that commit. If the repository doesn't have that commit it won't have anything with that hash ID. 1

Saying that commits are forever overstates things slightly, though. These hash IDs are big, and ugly, and impossible for humans to get right and to remember. So we don't use them. Git uses them internally. We use names . A name—a branch name like master or a tag name like v2.1 —holds a hash ID. That's most of what a name does: hold a hash ID. This lets us—or Git— find one particular commit by name.


1 Technically, as long as two Gits never "meet", it's OK for them to have different objects that have the same hash ID. If they do ever meet, they'll think they have each other's objects already, and if you're trying to get them to have repository-sex so that they acquire each other's objects, it will go wrong. In practice, this kind of duplication never happens. You could in theory make it happen on purpose, but see How does the newly found SHA-1 collision affect Git?


What good does finding one commit do?

Every commit contains a full snapshot of all of your files. Commits are not changes , they're snapshots. This snapshot is typically the main bulk of any one commit: its data. But each commit also has some metadata , or information about the data. This includes stuff like who made the commit, when, and why (the log message). There's one other key item—or list—in the metadata, though. Each commit contains the raw hash ID of its immediately- previous , or parent commit. (Merge commits have two or more parents, so this is a list of hash IDs, rather than a single hash ID.)

This means that if we can find the last commit in a chain, we can find the whole chain:

... <-F <-G <-H   <--master

The name master holds the hash ID of the last commit. In this case, master holds the hash ID of some commit with a big ugly hash that we're just going to call H . We say that the name master points to H .

Meanwhile, the actual commit itself, as retrieved by Git from its big database of all-commits-and-other-internal-objects, contains the hash ID of some earlier commit. We'll call it G , rather than guessing some hash ID, so we say that H points to G .

Commit G is an ordinary commit too, so it points to—contains the hash ID of—its parent F . F of course also points back to some earlier commit, which continues to point back. The process only stops when we reach the very first commit ever, which—since it can't point back—has no parent at all.

Hence, finding one commit is as good as finding every commit from here backwards . Merge commits add a bit of a wrinkle as each merge has two (or more) parents, so suddenly you're able to find two (or more) previous chains and work backwards along both. We don't need to look any further than that here, though, so we won't.

Draw your commits

Whenever you have a series of commits and some branch names, draw them . Get into practice, doing this on a whiteboard or on paper or whatever, because you'll need it as a general skill when working with Git. (You can have Git do it for you, using git log --graph —which draws a rather crude ASCII-graphics graph—or have a graphical interface do it, but it's a good idea to do it yourself a few times too.)

Let's watch the addition of a single commit on master in a tiny repository with just three commits. We start with:

A <-B <-C   <--master

We git checkout master , which selects commit C as the current commit . That extracts all of its frozen-forever files into a work-area (our working tree or work-tree ), and also into Git's index (from which Git makes new commits; we won't go into the details here, but knowing about the index is important).

We'll now make some changes to the work-tree files, use git add to copy them back into the index, and run git commit to make a new snapshot. This new snapshot will get some big ugly hash ID, but we'll keep things simple and just call it D . D needs to point back to C . Because my crude-ASCII-graph-drawing talents are limited, I'll draw it like this:

A--B--C   <-- master
       \
        D

Now, the name master must point to the latest commit, so Git now writes D 's hash ID into the name master :

A--B--C
       \
        D   <-- master

Draw your branch names

Let's rewind a bit to:

A--B--C   <-- master

If you have more than one branch name, you can draw in the additional names. Let's make a new branch, dev for development. A name has to point to a commit. Which commit should we use? Let's use the latest, which is C :

A--B--C   <-- master, dev

Note that all three commits are now on both branches. The latest master commit is C , and the latest dev commit is also C .

We need to know which branch name we're using. Either way, we'll use commit C now, but to remember which name we are using, let's add the name HEAD . You can draw this as another pointer:

                      HEAD
                       |
                       v
A--B--C   <-- master, dev

or use less space like this:

A--B--C   <-- master, dev (HEAD)

Basically, we attach the special name HEAD to one of the branch names. The name that HEAD is attached-to is the branch we used with our git checkout .

Let's make commit D now, on dev :

A--B--C   <-- master
       \
        D   <-- dev (HEAD)

So what Git does, when we make a new commit, is to make the new commit point back to the current commit—as found by our current branch namem as found by HEAD —and then write the new commit's hash ID into the current branch name, as found by HEAD .

If we go back to master , with git checkout master , we get this:

A--B--C   <-- master (HEAD)
       \
        D   <-- dev

No commits have changed at all, but now we are once again using commit C . If we make a new commit E , we need to draw it in, either on a new line:

        E   <-- master (HEAD)
       /
A--B--C
       \
        D   <-- dev

or on the same line:

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

Both drawings represent the same thing: in both cases, commits ABC are on both branches . Commit D is only on dev and commit E is only on master .

Which way should you draw them? Well, any way you like. You can put newer commits towards the top or bottom, instead of towards the right. Just remember that each commit points backwards , and each branch name points to the last commit. Git calls that last commit the tip commit.

This is the normal way branches work in Git: we add new commits to them . The commits that were on the branch are still there, on the branch; the new commit is the new tip of the branch.

Forgetting a commit

Suppose we decide commit E is bad and we should not have made it.

We can tell Git to move a branch name. When we do this, we can move it backwards from the normal direction. That is, suppose we have:

        E   <-- master (HEAD)
       /
A--B--C
       \
        D   <-- dev

and we tell Git: move the name master back one step, to C . The result is:

        E
       /
A--B--C   <-- master (HEAD)
       \
        D   <-- dev

Commit E still exists, but we can't find it. It's been abandoned! There's a way to find A , by starting at C and going back to B and then A , or by starting at D and going back—but commits only let you go back, not forward, so you can't get from C to E . Eventually, Git will see that we can't find it and have not used it for a long time (at least 30 days, by default) and will remove it after all.

Hence, we can get our Git to forget this bad commit E that we made. It will stick around for a while, but eventually it will go away. This assumes we haven't copied commit E into some other Git repository, of course. If we used git push to send E to some other Git, that other Git now has a copy of E , and having our Git forget ours won't do anything about theirs .

Back to your problem

Now that we have this structure, we can draw your problem and see, clearly, what we should do:

          I--J   <-- branch-a (HEAD)
         /
...--G--H   <-- master

Let's say that it's commit J that's wrong. We'd like a different commit K to come from H , and to make the name branch-b point to J . Here is how we can do that with git cherry-pick , which copies a commit to a new (and perhaps improved) one. First we'll git checkout master to select commit H :

          I--J   <-- branch-a
         /
...--G--H   <-- master (HEAD)

Then we'll create a new branch name, branch-b , here, and git checkout branch-b :

          I--J   <-- branch-a
         /
...--G--H   <-- master, branch-b (HEAD)

Then we'll run git cherry-pick branch-a . The name branch-a selects commit J , so that's the commit that Git will copy:

          I--J   <-- branch-a
         /
...--G--H   <-- master
         \
          K   <-- branch-b (HEAD)

If we want Git to forget commit J , we can force the name branch-a to point to I now. There are multiple ways to do that—I'll use a short one in a moment—but we want:

            J   [abandoned]
           /
          I   <-- branch-a
         /
...--G--H   <-- master
         \
          K   <-- branch-b (HEAD)

A very short sequence of Git commands to go from the starting graph, to this one, is:

git checkout -b branch-b master
git cherry-pick branch-a
git branch -f branch-a branch-a~1   # a little bit magic

But: what if it's commit I that we want to copy to K ? Well, we can start out with the same git checkout -b branch-b master to get:

          I--J   <-- branch-a
         /
...--G--H   <-- master, branch-b (HEAD)

Now we would run git cherry-pick branch-a~1 . This ~1 suffix means count back one commit , ie, start at J , where branch-a points, then move back one step to commit I . So we'll now have:

          I--J   <-- branch-a
         /
...--G--H   <-- master
         \
          K   <-- branch-b (HEAD)

where K is a copy of I .

More alternatives

In fact, since I and K both have the same parent and the same snapshot , we could have done this instead, which might be cleverer:

            J   <-- branch-a
           /
          I   <-- branch-b
         /
...--G--H   <-- master

You need to define what result you want

The real question here is what you want done with commit J . Whatever else we do, commit J still has I as its parent. That's literally impossible to change: nothing about any commit can ever be changed.

Suppose the result you want , in the end, looks more like this:

        I--J   [abandoned]
        |
        | L   <-- branch-a
        |/
...--G--H   <-- master
         \
          K   <-- branch-b

In this case, you'll need to copy I to K , and J to L . But perhaps you can use instead:

          L   <-- branch-a
         /
...--G--H   <-- master
         \
          I   <-- branch-b
           \
            J   [abandoned]

The key is always:

  • Draw what you have.
  • Think about what you want instead (and maybe draw that too).
  • Pick Git commands that do the thing you want.

The history in your Git repository consists of all the commits that you can find, by starting at branch names (and other names) to find one specific commit, and working backwards from there.

The branch names in your Git repository are under your control. You can do anything you want with them, at any time. You can rename them, move them around, or delete them. You can make new ones. Each of your branch names locates one tip commit , and from there, Git will work backwards.

You're only really stuck with a commit once you copy it into another Git repository . That other Git repository has its own branch names, and if it can find the commit—which will have that same hash ID, as hash IDs can never change—then it will continue to find that commit. If you let a bad commit get out, you have to get every Git repository that has it, to give it up. It's usually much easier to just let it be out there, bad, and add a new commit to fix it, because Git repositories are greedy for new commits: give them a new commit and they'll usually take it. Try to take a commit away, and you need to use a lot of force ( git branch --force or git reset ).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM