简体   繁体   中英

Why does git pull not update locally changed files?

From what I understand "git pull" does a fetch and merge. For an iOS project, I have the settings configured with distribution settings (certificates, provisioning profiles etc.). However, when on the master branch, when I do a git pull and a git log , I see that git commits are up to date with the latest commit from the remote master branch BUT git diff still shows that my pbxproj file is different (ie with the provisining profile strings etc. which I have configured locally).

To be clear, this is ideal for me because I want only the code changes and not having to mess with setting the distribution and provisioning profiles every time I update the branch just before releasing to production. But I also want to understand the way git is merging. I was expecting that git would merge the files which I had changed locally and/or indicate a conflict. But no such thing is happening.

Note : I know a "force update" with make my local master resemble remote exactly but want to understand the difference between this and the regular merge.

git will merge changes from the remote with locally committed changes. It will not attempt to merge with your uncommitted changes, and if there are remote changes in the same files, it will warn you that the merge would wipe out your local changes and will reject the merge.

Update - I realized I didn't really answer "why":

The reason for this distinction between locally committed changes, vs. uncommitted local changes, is that you can always get your locally committed changes back. They're in a commit, and you can checkout that commit, create a new branch there, even reset the current branch back to that point if need be. Nothing is lost by merging with locally committed changes.

But your worktree (uncommitted local changes) gets no such protection. If git were to merge changes into it and the result were not what you wanted, you couldn't easily "back out" to what you had before.

We could also go into technical differences in what would be involved to merge with uncommitted changes, but those technical reasons could be overcome, if it were desirable to merge with uncommitted changes. Because doing so could result in data loss, though, it's instead desirable to avoid merging changes into any file that has local uncommitted changes.

From what I understand "git pull" does a fetch and merge.

That's correct. More precisely, git pull actually runs git fetch , and then runs git merge . Or rather, it used to do that literally, back in the old days; in the last year or so, Git was modified to make git pull faster by doing the fetch and merge steps internally, rather than running separate commands, when possible—and you can also tell it to do git rebase instead of git merge , and there are some other corner cases. But essentially, pull = fetch + merge.

The tricky part is what each of these two steps means . Both fetch and merge are, or can be, complicated.

Fetch

The fetch step is the simpler one: your Git calls up some other Git. Your Git and their Git have a little conversation, in which their Git tells your Git about any new commits they have, that you don't. Your Git loads up these new commits into your repository, so that you have the commits now; and then your Git sets up your remote-tracking names —names like origin/master —to remember their Git's master and so on. (When git pull runs git fetch , it may limit your Git's fetching. In very old versions, this even inhibits your Git from updating your origin/master and the like as well. Modern Git does away with that last unhelpful peculiarity.)

As long as you don't complicate this on your own (by adding refspecs or various fetch flags), that's all there is for the fetch. It's the merge part that remains tricky. Having done the fetch, git pull now runs git merge and this is where things get a little complex.

Merge

The goal of a merge is the easy part: Git begins by assuming that you have been working and making commits, and someone else has also been working and making commits. If we draw those commits with earlier commits towards the left, and later commits towards the right, giving them simple one-letter names instead of their actual incomprehensible Gitty hash IDs, we get something like this:

          C--D--E   <-- branch (HEAD)
         /
...--A--B
         \
          F--G--H   <-- origin/branch

Here E is your latest commit on your branch. Its parent is commit D ; D 's parent is C ; and so on. Commit H is their latest commit, which you just brought in via git fetch . H 's parent is G , whose parent is F , whose parent is B . We can see that these two lines of development happened in parallel, starting from the common point—the merge base —of commit B .

What git merge does for this case is to run, in effect, two git diff commands. The first one finds out what you changed:

git diff --find-renames <hash-of-B> <hash-of-E>

The second diff finds out what they changed:

git diff --find-renames <hash-of-B> <hash-of-H>

Git can combine these two sets of differing changes, as long as they do not conflict with each other. Or rather, as long as Git doesn't think they conflict—Git does not know , it just makes a lot of assumptions about line-by-line differences from git diff . Having successfully (maybe) combined the changes, Git applies the combined changes to the snapshot in the merge base to produce your new snapshot, and makes a merge commit , with two parents:

          C--D--E
         /       \
...--A--B         I   <-- branch (HEAD)
         \       /
          F--G--H   <-- origin/branch

Making the new merge commit causes the branch name itself, branch , to point to the new commit, just as making a new ordinary commit does.

This is a true merge: Git has to combine changes since a merge base , which means it must first locate the merge base commit, then do the two diffs. This is the verb part of a merge, to merge (two sets of changes). Having done the to-merge verb part, Git makes a merge commit , merge as an adjective, or less formally, a merge , which uses merge as a noun.

Not all merges are true merges

Suppose that you run git fetch (perhaps via git pull ) and nothing at all has changed, so that you have:

...--D--E   <-- branch (HEAD), origin/branch

That is, you have two different names here. You have the name branch —your branch—pointing to your commit with hash ID E (really some big ugly Git hash). You also have your remote-tracking name origin/branch , remembering their Git's branch named branch , pointing to your commit with hash ID E , as that same commit exists in both Gits.

Then, having done no work and made no commits, you run git fetch again later, only this time they have done some work and made some commits:

...--D--E   <-- branch (HEAD)
         \
          F--G   <-- origin/branch

If you now ask your Git to merge your work (or lack of work) with their work, Git will find the merge base as usual, but discover that the merge base is commit E itself. Obviously, diffing commit E against itself would find no changes. Git could go ahead and build a new merge commit anyway, combining no-changes with some-changes to get:

...--D--E------H   <-- branch (HEAD)
         \    /
          F--G   <-- origin/branch

where the snapshot in H exactly matches the snapshot in G . But the default for git merge is to not bother . Merge detects this as a fast-forward operation and will instead not do the to-merge verb, and not make the adjective-merge commit. Instead of making new commit H , Git will simply move the branch name branch forward:

...--D--E
         \
          F--G   <-- branch (HEAD), origin/branch

and check out commit G so that G 's snapshot fills the index and work-tree.

Git calls this a fast-forward merge but there's no merging going on! The fast-forwarding is simply an operation on the branch name. There is ultimately no trace left behind that this has happened, which is why Git allows you to prohibit a fast-forward non-merge "merge". However, for local branches that correspond to remote-tracking names, this kind of thing is actually extremely often what you want anyway, which is why this is Git's default.

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