Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
2.0k views
in Technique[技术] by (71.8m points)

git fetch non fast forward update

I know that git fetch always does a fast forward merge between the branch and it's remote tracking after it fetches the commits from the remote.

My question deals with a scenario in which we will be requiring git fetch to do a non fast forward merge. Is it possible to make git fetch non fast forward merge ? If not , how will I solve this below scenario ?

My local repo (made some 2 local commits - the C and B commit)

...--o--o--A   <-- origin/master
            
             C--B   <-- master 

After that I run git fetch (to update my branch)

...--o--o--A-- D  <-- origin/master (updated)
            
             C--B   <-- master

Here , origin/master needs to be merged in master but this won't be fast forward. git fetch will fail. I don't want force fetch as I don't want to lose my commits C and B also.

How can I make git fetch to make a non fast forward merge. Something like this :

...--o--o--A-- D --  
                  
                   F <-- master ,origin/master (updated) (my merge commit for non fast forward)
                  /
               C--B   
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

(Note: I started writing this early this morning, and finished it late this evening; the question was answered in between, but having done all this work I'm still going to post the answer. :-) )

TL;DR

The git fetch command never merges anything. It can update references, and is very willing to update branch-like references in a fast-forward fashion. It is more reluctant to update such references in a non-fast-forward fashion; to do so, you must force the update.

Fast-forwarding—once properly divorced from the idea of merging—is a property of a change to a reference that refers to a commit. More specifically, we're usually interested in whether a branch name value, or a remote-tracking name value, changes in a fast-forward fashion. This means that we must look at the commit graph, because it's the new place in the commit graph, combined with the commit currently selected by the reference, that determines whether the update to that reference is a fast-forward.

Long

The original claim here is wrong, in at least one important way:

I know that git fetch always does a fast forward merge between the branch and it's remote tracking after it fetches the commits from the remote.

Let's take this apart a bit, so that we have the right words and phrases to use. We need to know:

  • what a reference is;
  • what a refspec is; and most importantly,
  • what it means to do a fast-forward update vs a non-fast-forward update to a reference.

This last bit also involves the force flag: each reference update can be forced, or non-forced. You may be familiar with git push --force, which sets the force flag for every reference that Git is pushing. The git fetch command has the same flag, with the same effect—but in general "all or nothing" is too broad, so Git has a way to set the force flag on a more individual basis. (The git push command has even more refinements here, but we'll only mention them in passing.)

Definition of reference and refspec

A reference, in Git, is just a name—ideally, a name that makes sense to a human—for some specific commit or other Git object.1 References always2 start with refs/ and mostly go on to have a second slash-delimited component that declares which kind of reference they are, e.g., refs/heads/ is a branch name, refs/tags/ is a tag name, and refs/remotes/ is a remote-tracking name.3

(The references we care about here, for deciding whether some update is or is not a fast-forward, are those that we would like to behave in a "branch-y manner": those in refs/heads/ and those in refs/remotes/. The rules we will discuss in a moment could be applied to any reference, but are definitely applied to these "branch-y" references.)

If you use an unqualified name like master where Git either requires or could use a reference, Git will figure out the full reference, using the six-step procedure outlined near the beginning of the gitrevisions documentation to resolve the abbreviated name to a full name.4

A refspec, in Git, is mostly a pair of references separated by a colon (:) character, with an optional leading plus sign +. The reference on the left side is the source, and the reference on the right is the destination. We use refspecs with git fetch and git push, which connect two different Git repositories. The source reference is meant for the use of whichever Git is sending commits and other Git objects, and the destination is meant for the use of the receiving Git. For git fetch in particular, then, the source is the other Git, and the destination is ourselves.

If a reference in a refspec is not fully qualified (does not start with refs/), Git can use the process above to qualify it. If both references in a single refspec are unqualified, Git has some code in it to attempt to put them both into an appropriate name-space, but I've never trusted this code very much. It's not clear to me, for instance, who really qualifies the source and destination during a fetch: there are two Gits involved, but the other Git usually sends us a complete list of all of their references, so our Git could do the resolving using this list. It's obviously wiser to use fully-qualified references here, though, in case their set of references does not match your own expectations: if they have only a refs/tags/xyz and you were expecting xyz to expand to refs/heads/xyz, you can be surprised when it doesn't.

In any refspec, you can omit either the source or the destination part. To omit the destination, you write the refspec without a colon, e.g., refs/heads/br. To omit the source, you write the refspec with a colon, but with nothing where the source part would go, e.g., :refs/heads/br. What it means when you do these things varies: git fetch treats them very differently from git push. For now, just note that there are the source and destination parts, with the option of omitting them.

The leading plus, if you choose to use it, always goes at the front. Hence git push origin +:refs/heads/br is a push with the force flag set, of an empty source, to the destination refs/heads/br, which is fully-qualified. Since this is a push, the source represents our Git's name (none) and the destination represents their Git's name (a branch named br). The similar-looking string +refs/heads/br has the force flag set, has a fully-qualified source, and has no destination. If we were concerned with git push we could look at the meanings of these two refspecs for push, but let's move on now.


1Any branch-like reference must point to a commit. Tag names may point to any object. Other reference names may have other constraints.

2There's some internal disagreement within Git itself whether every reference must be spelled, in its full-name form, as something matching refs/*. If that were the case, HEAD would never be a reference. In fact, the special names like HEAD and ORIG_HEAD and MERGE_HEAD sometimes act like normal references, and sometimes don't. For myself, I mostly exclude these from the concept of reference, except whenever it's convenient to include them. Each Git command makes up its little Gitty mind about how and whether to update these *_HEAD names, so there's no formal systematic approach like there is—or mostly is, given the other weird special cases that crop up in some commands—for refs/ style references.

3There are more well-known sub-spaces: e.g., refs/replace is reserved for git replace. The idea here, though, is simple enough: refs/ is followed by another human-readable string that tells us what kind of reference this particular reference is. Depending on the kind, we might demand yet another sub-space, as is the case in refs/remotes/ where we next want to know: which remote?

4Some Git commands know, or assume, that an abbreviated reference must be a branch name or a tag name. For instance, git branch won't let you spell out refs/heads/ in some places: it just rudely shoves refs/heads/ in on its own, since it only works on branch-names. The six-step procedure is generally used when there's no clear must be a branch name or must be a tag name rule.


The commit graph

Before we can define what it means to do a fast-forward update, we need to look at the commit graph. Fast-forward vs non-fast-forward only makes sense in the context of commits and the commit graph. As a result, it only makes sense for references that refer specifically to commits. Branch-like names—those in refs/heads/ and those in refs/remotes/—do always point to commits, and those are the ones we care about here.

Commits are uniquely identified by their hash ID.5 Every commit also stores some set of parent commit hash IDs. Most commits store a single parent ID; we say that such a commit points to its parent commit. These pointers make up a backwards-looking chain, from most-recent commit to oldest:

A  <-B  <-C

for instance, in a tiny repository with just three commits. Commit C has commit B as its immediate parent, so C points to B. Commit B has commit A as its immediate parent, so B points to A. A is the very first commit made, so it has no parent: it is a root commit and it points nowhere.

These pointers form an ancestor / descendant relationship. We know that these pointers always look backwards, so we don't need to draw the internal arrows. We do need something to identify the tip commit of the data structure, though, so that Git can find the ends of these chains:

o--o--C--o--o--o--G   <-- master
       
        o--o--J   <-- develop

Here master points to some commit G, and develop points to J. Following J backwards, or G backwards, eventually leads to commit C. Commit C is therefore an ancestor of commits G and J.

Note that G and J have no parent/child relationship with each other! Neither is a descendant of the other, and neither is a parent of the other; they merely have some common ancestor once we go far enough back in time / history.


5In fact, every Git object is uniquely identified by its hash ID. This is, for instance, how Git stores only one copy of some file's contents even when that particular version of that one file gets stored in dozens or thousands of commits: commits that don't change the file's contents can re-use the existing blob object.


Definition of fast-forward

Fast-forward-ness is a property of moving a label. We can move the existing names (master and develop) around, but let's avoid doing so for a moment. Suppose, instead, we add a new name, and make it point to commit C. Let's add one-letter hash IDs for the rest of the commits as well:

        ............ <-- temp
       .
A--B--C--D--E--F--G   <-- master
       
        H--I--J   <-- develop

We can now ask Git to move the new name from commit C to any other commit.

When we do so, we can ask another question about this move. Specifically, temp currently points to commit C. We pick another ID out of the <code


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...