简体   繁体   中英

is it possible to reset the head of a branch while being on another branch i.e. without using git checkout?

suppose I have two branches, A and B, and 3 commits C, D and E. C is the parent of both D and E and I'm on branch B which is on commit E. branch A is on commit C. Is it possible to, without checking out to branch A, reset branch A to commit D? The use case is that I'd like to git fetch and then update develop or master to origin/develop or master. Is this possible, ideally in a single command? I've seen git branch -f and I from my understanding, doing git branch -f AD might do what I want, but I'm not literate enough on the internals of git commands yet to be certain that that's what I want to do. So- to anyone who has more experience with git- does this achieve the desired effect? Could it have unintended side effects? And is there maybe a better way? One that might be safer? Also, would I need to address the commit I want to reset to as a hash or can I also use refs (so eg just do git branch -f master origin/master)? edit: forgot to mention what branch A points to

TL;DR

Yes, git branch -f can do what you want. Beware, though, that this will do what you tell it to, even if that's maybe not what you wanted after all. Consider using a slightly magic-looking git push. instead. Better yet, consider not doing this at all—see below.

Long

suppose I have two branches, A and B, and 3 commits C, D and E. C is the parent of both D and E and I'm on branch B which is on commit E.

Let's draw this. Let's use names branch-A and branch-B so that single uppercase letters always stand in for commit hash IDs (otherwise it's too easy to think that maybe A stands in for a commit hash ID too):

  D
 /
C
 \
  E   <-- branch-B

You haven't told us which of the three commits branch-A names / points-to, so I don't know where to put the branch-A label in this drawing.

Is it possible to, without checking out to branch A, reset branch A to commit D?

Yes. Since you've now mentioned "without checking out branch-A " we can assume you're currently on branch-B (since branch-A and branch-B are the only branches in this repository). We can also assume that the name branch-A must point to commit C now, so let's draw that:

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

How do you know the commit hash ID of commit D in the first place? There are no names by which to find it. But still, as long as you do know it:

I've seen git branch -f and I from my understanding, doing git branch -f branch-A D might do what I want...

That's precisely what it does. (There is a caveat, which we will get to in a moment, but that is what it does.) Given the hash ID of commit D , git branch -f branch-A D forces the name branch-A to being pointing there, regardless of where it pointed before:

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

Note that no commit ever changes. The branch names move around, but the commit graph remains the same (even if you have to change how you draw it, so as to squeeze the names in:-) ).

Once you've drawn the commits—using their real hash IDs, or these substitute one-letter names like I did—they stay the same forever, including the links backwards from a later commit, like D or E , to its immediate parent ( C in this case). However, if you take all the names away from some commit—as was the case in our drawing with commit D earlier—those commits become very hard to find. Eventually—some time after 14 or 30 days, typically, with the details depending on settings and reflog entries and the like—Git will really, truly delete any commits that you can't find, the next time git gc runs.

Beware the XY problem

We've just solved some problem Y that you plan to use to solve somewhat different problem X. Let's look at problem X, and introduce the caveat I mentioned.

The use case is that I'd like to git fetch and then update... master to [ origin/master ].

So, presumably, branch-A was actually a stand-in for master , and commit D was pointed-to by origin/master . Let's draw that case:

  D   <-- origin/master (after `git fetch`)
 /
C   <-- master
 \
  E   <-- branch-B (HEAD)

Here, you can run git branch -f master origin/master and get:

  D   <-- master, origin/master
 /
C
 \
  E   <-- branch-B (HEAD)

Note that commit C remains reachable: master now points to commit D , but commit D points to commit C , which is the root commit of the graph. (We have no commits A and B , but if we did, presumably they would come before C and hence still be reachable.)

But suppose your master points to new commit G , and the actual picture is this:

  D   <-- origin/master
 /
C--F--G   <-- master
 \
  E   <-- branch-B (HEAD)

The git branch -f master origin/master command will still make master point straight to commit D :

  D   <-- master, origin/master
 /
C--F--G
 \
  E   <-- branch-B (HEAD)

Commit G no longer has a way to find it! git log --all won't show it. Eventually—some time after 30 days or so, probably— git gc will remove it for real, along with commit F , which also can't be found.

To prevent moving master from G backwards to C and then forwards again to D , you'd presumably want to either do a real merge, by doing git checkout master and then git merge origin/master :

  D__  <-- origin/master
 /   `--.
C--F--G--H   <-- master (HEAD)
 \
  E   <-- branch-B

or maybe a rebase, by doing git checkout master && git rebase origin/master :

    F'-G'  <-- master (HEAD)
   /
  D   <-- origin/master
 /
C--F--G
 \
  E   <-- branch-B

The rebase also loses F and G , but only after copying them to new-and-improved F' and G' , so that it's OK to lose them.

Should you wish to find them again, for the ~30 days or more that they linger, they will be in git reflog master output.

Dealing with problem X

Let's call problem X "keeping master up to date, without losing commits".

One way to deal with it is just to git checkout master and then run merge or rebase as appropriate. I like to use git merge --ff-only myself, which updates master in a fast-forward fashion, moving it to commit D if and only if that can be done as a fast-forward not-really-a-merge-at-all operation. If not, I re-evaluate my commits F and G . Should they be on a branch after all, so that I can fast-forward my master ? Should I rebase them? What should I do with them?

If that would disturb my current work-tree, I can use git worktree add to do the job. The added work-tree can be on branch master , and then I can do whatever I need there.

Besides doing a git checkout master , though, there are several more ways to deal with this problem:

  • The simplest is just delete the name master entirely. Why do you need it? You start out with:

     C <-- origin/master \ E <-- branch-B (HEAD)

Then you run git fetch and have:

      D   <-- origin/master
     /
    C
     \
      E   <-- branch-B

You don't need your own master at all.

  • But if you like having a master (for whatever reason), you can use:

     git push. origin/master:master

(Or:

    git fetch . origin/master:master

I don't actually use these, but if I did, I'd generally prefer the push variant. Let's look only at the push variant here.)

The name . acts as a file URL for the "other" Git your Git will call up here. That other Git is, well, itself. So your Git calls itself up and asks or tells it to do something. The remaining argument—which is origin/master:master —tells your Git what to tell your Git to do:

Hey, other Git (which is really me): make sure you have the commit whose hash ID my origin/master identifies. Ah, I see that you (I) do have that commit. Now, please, if it's OK—if it's a fast-forward operation —set your master (which is really my master ) to point to that commit too.

Because your Git is talking to itself, what this really accomplishes is to use the "is this a fast-forward operation" test, just like git merge --ff-only would. Your name master will move, in a fast-forward fashion, to match your name origin/master . If that's not possible—if commit F exists, for instance—your Git will reject its own push request.

You can only use this on a branch that you do not have checked out. 1 If you do have the branch checked out, Git wants to you use git merge --ff-only instead, so that your index and work-tree get updated correctly.


1 In what amounts to a somewhat bad bug, Git's "is this branch checked out" test only checks the primary work-tree, without checking any added work-trees. If you use git worktree add , watch out!

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