How to change the starting point of a branch?

I usually create a branch by running a command like git checkout -b [branch-name] [starting-branch] . In one case, I forgot to enable starting-branch , and now I want to fix it. How to do this after the branch is already created?

+5
source share
3 answers

The short answer is that after you have some commits, you want to git rebase them using the long form of git rebase : git rebase --onto newbase upstream . To find out how to identify each one, see the (Very) long answer below. (Unfortunately, he looked a little out of hand, and I don’t have time to cut it.)

The problem you have here is that in Git, branches don't have a “starting point” - at least not in any useful form.

The term "branch" in Git is ambiguous

The first problem is that in Git the word "branch" has at least two different meanings. Usually, when we speak loosely about a "branch", it is clear from the context whether we mean the name of the branch - a thing that has a word like master or develop or feature-X , or a thing that I call a branch "or" branch structure " , or more informally, “DAGlet.” 1 See also What exactly do we mean by “branch”?

In this particular case, unfortunately, you mean both of them at the same time.


1 The term DAG is short for the Directed Acyclic Graph, which is a fixation schedule: a set of vertices or nodes and directed (from child to parent) edges, so there are no loops through directed edges from any node back to itself. To do this, I simply add a “- let” diminutive suffix . The resulting word has a happy resemblance to the word aglet , as well as a certain assonance with the word "dagger", which makes it a little dangerous: "Is this the DAGlet that I see in front of me?"

Draw a commit graph

Whenever you need to deal with these problems, it helps to make a schedule of what you have now, or at least some useful subset of what you have now. Of course, there are many ways to do this (see this related question for several options, including some bad ones :-)), but in the plain text in StackOverflow's answer, I usually draw them like this:

 ...--o--o--o <-- master \ o--o--o--o <-- develop 

Round o nodes represent commits, and the names of the master and develop branches point to one specific tip lock for each branch.

In Git, each latch points to its parent latch (s), and that is how Git forms branch structures. By "structural branches" I mean here certain subsets of the general part of the pedigree of the graph or what I call DAGlets. The name master indicates the largest commit on the top of the master branch, and this fixes the points backward (to the left) to another commit, which is the previous commit on the branch, and which fixes the points to the left again and so on.

When we need to talk about specific commits in this column, we can use their actual names, which are large ugly 40-character hashes that identify each Git object. However, they are really awkward, so here I am replacing the small round o capital letters:

 ...--A--B--C <-- master \ D--E--F--G <-- develop 

and now it’s easy to say, for example, that the name master points to commit C , and C points to B , and B points to A , which indicates more history that we really don’t care about and therefore simply left as ...

Where does the branch come from?

Now, it’s quite obvious for you and for me, on the basis of this graphic drawing, this develop branch, whose fixation of the G tip begins with commit D But this is not obvious to Git - and if we draw the same graph a little differently, it may be less obvious for both you and me. For example, look at this drawing:

  o <-- X / ...--o--o--o--o--o--o <-- Y 

Obviously, the X branch has only one commit, and the main line is Y , right? But let a few letters:

  C <-- X / ...--A--B--D--E--F--G <-- Y 

and then move Y down the line:

  C <-- X / ...--A--B \ D--E--F--G <-- Y 

and then see what happens if we move C down to the main line and understand that X is master and Y is develop ? Which branch actually captures B ?

In Git, commits can be on many branches at the same time; DAGlets are up to you

Git The answer to this dilemma is that commit A and B are on both branches. The beginning of branch X is to the left, in the part ... But branch Y also begins. As for Git, the branch is “launched” on any root fixator that it can find on the chart.

This is important to keep in mind overall. Git has no real idea of ​​where the branch begins, so we get more information. Sometimes this information is implied, and sometimes explicit. It is also important, in general, to remember that commits are often in many branches, so instead of specifying branches, we usually specify commits.

We just use branch names to do this. But if we give Git just the name of the branch and say, to find all the ancestors of the final commit of that branch, Git completely goes back in history.

In your case, if you write the name develop and ask Git to select this commit and its ancestors, you will get commits DEFG (which you wanted) and commit B and commit A , etc. (which you did not do). Thus, the trick is to somehow determine what obliges you not to want, along with what you do.

Usually we use the X..Y point-to-point syntax

With most Git commands, when we want to select a specific DAGlet, we use the two-dot syntax described in gitrevisions , for example master..develop . Most 2 Git teams that work with multiple commits see this as: "Select all commits starting at the top of the develop branch, but then subtract from this set the set of all commits starting at the top of the master branch." Take a look at our graphic drawing master and develop : it says: “Make commits starting with G and working backwards”, that there are too many of us, since it includes commits B and A and earlier - “, but exclude the commit starting with C and work backwards. " This excludes the part that gives us what we want.

Therefore, the record master..develop is what we call commits DEFG , and we have Git calculate this automatically for us, without having to first sit down and pull out a large piece of the graph.


2 Two notable exceptions: git rebase , which is located in its own section just below, and git diff . The git diff treats X..Y as just an XY value, i.e. In fact, it just completely ignores the two points. Note that this has a completely different effect than the given subtraction: in our case, git diff master..develop just distinguishes the tree for commit C from the tree for commit G , although master..develop never has a C commit in the first set.

In other words, mathematically speaking, master..develop usually ancestors(develop) - ancestors(master) , where the ancestors function includes the specified commit, i.e. it tests ≤, not just <. Note that ancestors(develop) does not include commit C . The set subtraction operation simply ignores the presence of C in the ancestors(master) . But when you write this before git diff , it does not ignore C : it is no different, say, B from G Although this may be wise, git diff steals three-point syntax master...develop instead to do this.

Git rebase little special

The rebase command rebase almost always used to move 3 of one of these DAGlet commit subsets from one point in the graph to another. In fact, this is what rebase, or was originally in any case, defined. (Now it has an unusual interactive reinstall mode that does this and more history editing operations. Mercurial has a similar hg histedit with a slightly better name and much more strict default semantics. 4 )

Since we always (or almost always) want to move the DAGlet, git rebase builds a selection for us in this subset. And since we always (or almost always) want to move the DAGlet right after the tip of some other branch, git rebase by default chooses to commit the target (or --onto ) using the branch name, and then uses that same branch name in the X..Y syntax X..Y . 5


3 Technically, git rebase actually copies commits rather than moving them. This is necessary because commits are immutable, like all internal Git objects. The true name, the SHA-1 hash, is the checksum of the bits that make up the commit, so anytime you change anything, including something as simple as the parent identifier, you must create a new, slightly different commit.

4 In Mercurial, unlike Git, branches do have start points, and, more importantly for histedit , record their phase: secret, draft or published. History editing is easily applied to secret or project phases, and not so much to published commits. This is true for Git, but since Git does not have the concept of commit phases, Git rebase must use these other methods.

5 Technically, the <upstream> and --onto can simply be commit identifiers. Note that 1234567..develop works fine as a range selector, and you can re-assemble --onto 1234567 to put new commits after committing 1234567 . The only place git rebase really needs a branch name is the name of the current branch, which usually just reads from HEAD . However, we usually want to use a name, so I describe it all here.


That is, if we are currently on the develop branch in this situation, which we drew earlier:

 ...--A--B--C <-- master \ D--E--F--G <-- develop 

we probably just want to move the DEFG chain to the tip of the master to get the following:

 ...--A--B--C <-- master \ D'-E'-F'-G' <-- develop 

(The reason I changed the names from DEFG to D'-E'-F'-G' is because rebase is forced to copy the original commits rather than moving them. The new copies are as good as the originals, and we we can use the same one-letter name, but we should at least vaguely note that they are actually copies. That means the signs "prime", the characters ' .)

Since this is what we usually want, git rebase will do this automatically if we just name another branch. That is, we are now on develop :

 $ git checkout develop 

and we want to reinstall the commits that are on the develop branch and not on master , moving them to the end of master . We could express it as git somecmd master..develop master , but then we would have to enter the word master twice (such a terrible fate). So instead, Git rebase indicates this when we simply enter:

 $ git rebase master 

The name master becomes the left side of the point-to-point selector .. DAGlet, and the name master also becomes the target of rebase; and Git, then reformat DEFG to C Git gets the name of our develop branch by reading the current branch name. In fact, it uses a shortcut, which is that when you need the name of the current branch, you can just simply write HEAD . So, master..develop and master..HEAD mean the same thing, because HEAD develop .

Git rebase calls this name <upstream> . That is, when we say git rebase master , Git, in the documentation, that master is the <upstream> argument to git rebase . The rebase command then runs to commit to <upstream>..HEAD , copying them after the commit is in <upstream> .

This will soon become a problem for us, but for now just pay attention to it.

(Rebase also has a hidden, but desirable, side-by-side skipping function for any of the DEFG commits, which is pretty similar to commit C For our purposes, we can ignore this.)

What is wrong with another answer to this question

If another answer is deleted or becomes one of several other answers, I will summarize it here as "use git branch -f to move the branch label". The flaw in the other answer is - and perhaps more importantly, just when this is the problem, it becomes apparent when we draw our DAGlets graph.

The names of the branches are unique, but the tips will not end.

Let's see what happens when you run git checkout -b newbranch starting-point . This asks Git for a root search in the current graph for a given starting point and makes the new branch label the specified concrete commit. (I know what I said above that the branches do not have a start point. This is still basically true: we pass the git checkout command as the starting point now, but Git is going to install it, and then forget about it.) Let's say that starting-point is another branch name, and let's draw a whole bunch of branches:

  o--o--o--o <-- brA / ...--o--o--o--o--o--o <-- brB \ o--o--o <-- brC \ o--o <-- brD 

Since we have four branch names, we have four branch tips: four branch tips, denoted by brA through brD . We select one and create a new branch name newbranch , indicating the same thing as one of the four. I arbitrarily chose brA here:

  o--o--o--o <-- brA, newbranch / ...--o--o--o--o--o--o <-- brB \ o--o--o <-- brC \ o--o <-- brD 

Now we have five names and five ... uh, four? ... well, some tips are being made. The hard bit is that brA and newbranch point to the same completion of accumulation.

Git knows, because git checkout installs it, that we are now on newbranch . In particular, Git writes the name newbranch in HEAD . We can make our drawing more accurate by adding this information:

  o--o--o--o <-- brA, HEAD -> newbranch / ...--o--o--o--o--o--o <-- brB \ o--o--o <-- brC \ o--o <-- brD 

At this point, the four commits that were previously only on the brA branch are now on both brA and newbranch . And thus, Git no longer knows that newbranch starts at the end of brA . As for Git, both brA and newbranch contain these four commits and all the previous ones, and both of them “start” back in time somewhere.

When we make new commits, the current name moves

Since we are in the newbranch branch, if we now make a new commit, the new commit parent will be the old accumulation commit, and Git will adjust the name of the newbranch branch to indicate a new commit

  o <-- HEAD -> newbranch / o--o--o--o <-- brA / ...--o--o--o--o--o--o <-- brB \ o--o--o <-- brC \ o--o <-- brD 

Note that none of the other labels moved: the four "old" branches remain on, only the current branch ( HEAD ) changes. It is changing to match the new excellence we just made.

Note that Git does not know that the newbranch branch newbranch “running” in brA . Now it’s just that newbranch contains one commit that brA does not have, plus four commits that they both contain, plus all those that they previously commit.

What git branch -f does

Using git branch -f allows us to move the branch label. Say, for some mysterious reason, we don’t want the brB industry brB indicate what it does in our current drawing. Instead, we want it to point to the same commit as brC . We can use git branch -f to change the place at which brB points, i.e. To move a tag:

 $ git branch -f brB brC o <-- HEAD -> newbranch / o--o--o--o <-- brA / ...--o--o--o--o--o--o [abandoned] \ o--o--o <-- brC, brB \ o--o <-- brD 

This makes git “forget” or “discard” those three commits that were only on brB before. Perhaps this is a bad idea - why did we decide to do this strange thing? - so we probably want brB back.

Reflogs

Fortunately, "abandoned" commits are usually remembered in that Git calls reflogs. Reflogs uses the extended syntax name@ {selector} . The selector part is usually a number or date, for example brB@ {1} or brB@ {yesterday} . Each time Git updates the branch name to point to some kind of commit, it writes a reflog entry for that branch with a fixed commit identifier, timestamp, and an optional message. Run git reflog brB to see them. The git branch -f team wrote the new target as brB@ {0} , typing all the old numbers, so now brB@ {1} names the previous hint. So:

 $ git branch -f brB ' brB@ {1}' # you may not need the quotes, ' brB@ {...}' -- # I need them in my shell, otherwise the shell # eats the braces. Some shells do, some don't. 

will return it (and also renumber all the numbers again: each update replaces the old @{0} and makes it @{1} , and @{1} becomes @{2} , etc.).

In any case, suppose we do git checkout -b newbranch while we are at brC , and cannot mention brA . That is, we start with:

  o--o--o--o <-- brA / ...--o--o--o--o--o--o <-- brB \ o--o--o <-- HEAD -> brC \ o--o <-- brD 

and run git checkout -b newbranch . Then we get the following:

  o--o--o--o <-- brA / ...--o--o--o--o--o--o <-- brB \ o--o--o <-- brC, HEAD -> newbranch \ o--o <-- brD 

If we want to make newbranch point to fix brA , we can do it right now, with git branch -f . But let me say that we made a new commit before realizing that we made a newbranch start at the wrong point. Let me draw it:

  o--o--o--o <-- brA / ...--o--o--o--o--o--o <-- brB \ o--o--o <-- brC \ \ | o <-- HEAD -> newbranch \ o--o <-- brD 

If we use git branch -f , we will leave-lose - the commit we made. Instead, we want to reset it to the commit that the brA branch points to.

Simple git rebase copy too much

What if git rebase brA use git rebase brA instead of git branch -f ? Let's analyze this using something else - our DAGlets. Let's start with the above picture, with the extended leg facing brD , although in the end we get to ignore that leg, and with the section going to brB , most of which we also ignore. What we cannot ignore is all that is in the middle that we get by tracing the lines back.

The git rebase in this form uses brA..newbranch to select the commits to copy. So, starting with the whole DAGlet, check (with * ) all the commits that are included (or contained) by newbranch :

  o--o--o--o <-- brA / ...--*--*--*--o--o--o <-- brB \ *--*--* <-- brC \ \ | * <-- HEAD -> newbranch \ o--o <-- brD 

Now let un-mark (with X ) all the commits that are included (or contained) brA :

  x--x--x--x <-- brA / ...--x--x--*--o--o--o <-- brB \ *--*--* <-- brC \ \ | * <-- HEAD -> newbranch \ o--o <-- brD 

All that remains - everything * does * is those that git rebase will copy. It's too much!

We need to get git rebase to copy only one commit. This means that for the <upstream> argument we need to give git rebase name brC . 6 So Git will use brC..HEAD to select the commits to copy, which will be the only commit we need to copy.

But alas! - , git rebase <upstream> , . , brC . , ! (, .) !

, git rebase escape-, --onto . , , . , brA , --onto . Git rebase <upstream> , --onto , . So:

 $ git branch # just checking... brA brB brC brD master * newbranch 

, , newbranch . ( , git status , , , git status .)

 $ git rebase --onto brA brC 

Git brC..HEAD , , brA , . , , Git 7 newbranch , .

, , . , git branch -f . , git rebase :-), newbranch , , brA . , git branch -f ; git rebase , : <upstream> --onto .


6 , , git rebase , brC . , upstream .

7 , , reflog newbranch@ {1} , , . reflog newbranch , , tip commit , . reflog - 30 90 - - .

+11
source

, git reset --hard '.

...

, , "git rebase".

If, less likely, your branch starts with a new commit or on a completely different branch, use 'git rebase -onto'

+1
source

You want the branch to point to a different commit. You can do this by running

 git branch -f <branch-name> <starting-branch> 

Note that if it branch-nameis the current branch, you need to switch to another branch first, for example with git checkout master.

-3
source

All Articles