简体   繁体   中英

Visual Studio/git's "Merge into current branch" is not merging my changes

I'm using git for source control, and when I use Visual Studio 2019's "Merge into current Branch" function, the changes in the other branch are not being fully applied to the current branch.

An example: I renamed a variable from "EmphaticAdjective" to "StrongAdjective" in the master branch. I then switched to a prototype branch, right clicked on "master", and selected "Merge into Current Branch".

No merge conflicts were reported, and I can see the commit where the change occurred in the history of the prototype branch. However, immediately after the merge, I see a series of "The name 'EmphaticAdjective' does not exist in the current context" errors. The variable definition was correctly renamed, but a variety of the secondary files didn't get the change.

Is it possible I have accidentally set git to ignore incoming files when there's a conflict or something? Where would I look to check?


Note: I've never been able to get git terminal commands to work (git command not recognized) and am unsure how to install and where to enter them. I usually interact with git via Visual Studio.


To clarify, based on the examples given in answers:

There are commits on both branches, but the files I'm seeing problems with were only updated on branch 2 ("their" branch). So for those files specifically, it's this situation:

--G--H   <-- br1
         \
          I--J   <-- br2

And I'm calling the VS equivalent of git switch br1 && git merge br2 .

I would therefore expect to get the "J" version of those files after the merge. But I don't, I consistently get the "H" version of those files.

Is it possible I have accidentally set git to ignore incoming files when there's a conflict or something?

Technically, yes, via .gitattributes . Practically speaking, this is very unlikely. See the longer explanation below.

Note: I've never been able to get git terminal commands to work (git command not recognized) and am unsure how to install and where to enter them.

I avoid Windows, but I believe most people install Git-for-Windows from the Git-for-Windows distributions such as the ones found here . The same site has Scott Chacon's Pro Git book available for free.

The rest of this is strictly about Git's merging; VS has its own front end that might not even be using Git's merging.

How Git performs merges, as a high level overview

First, you need to know that Git is ultimately about commits (not files, not branches, but commits), and that each commit holds two things:

  • a full snapshot of every file, frozen for all time, rather like a WinRAR archive, except that Git cleverly saves a ton of space (vs making one archive of every file for every commit); and
  • some metadata , including the raw hash ID of this commit's parent commit(s).

Every commit has a unique, random-looking (though not actually random at all) number, encoded in hexadecimal , such as 63bba4fdd86d80ef061c449daa97a981a9be0792 . Git formally calls this an object ID (OID), or less formally, a "hash ID". Git needs this number to find a commit, but obviously these numbers are very bad for humans, so Git lets us use branch or tag names. We'll skip all the details about how these work and just note that the numbers exist, and that the metadata in any one particular commit includes the number of a previous commit.

This means that, given the hash ID of the latest commit in some branch, Git can easily find earlier commits in that same branch. Drawn as a picture, with newer commits towards the right, we can imagine two branches that fork off from some common series of commits ending at commit H , like this:

          I--J   <-- br1
         /
...--G--H
         \
          K--L   <-- br2

Here, the name br1 locates (for us and Git) the latest commit on branch br1 , which is commit J . Commit J 's metadata includes the raw hash ID of earlier commit I . Commit J also has a full archive of every file as of its form at the time of commit J . Commit I , being a commit, has a full archive of every file (as of its form at the time of commit I of course), and some metadata, and the metadata in commit I locates commit H via the saved hash ID.

Similarly, the name br2 finds L , which finds K , which finds H .

When you're on either branch and run git merge with the other branch name, Git finds the other branch's latest commit by using the name. It then uses the graph , plus the Lowest Common Ancestor algorithm , to locate the best shared commit, which in this case is commit H . We call commit H the merge base . This merge base is how merges really work.

Remember that the goal of a merge is to combine work , but each commit has only a snapshot . Any one given commit, by itself, doesn't have "work" in it, it just has a snapshot. The "work done" in a commit is instead implied: using the commit and its metadata, Git will back up to the previous commit, and compare the two snapshots. The "work done" is the difference between those two snapshots.

The work that needs to be combined, here, is any work done in both commits I and J on br1 —that's the "work done on br1 ", as it were—with any work done in both commits K and L on br2 . By finding the best shared commit, Git can obtain a common starting point, to find the "work done".

Let's say, for concreteness, that we run:

git switch br1

(to be "on" br1 , at commit J ) and then:

git merge br2

(to combine work as done "on" br2 ). Having found H , Git now has, for every file in all three commits, three versions of that file:

  • the merge base version from commit H ;
  • the "ours" or HEAD version in commit J ; and
  • the "theirs" version in commit L .

Ignoring the more complex cases of files that are new or deleted or renamed, let's just think about the cases where someone might have modified some files in some of the three commits:

  • It's possible that nobody changed the file at all. In that case, all three versions match exactly, and Git can just use any one of them.

  • Or, perhaps "we" changed the file from H to J , and they didn't touch the file at all. Then Git should use our version of the file, and that's what Git does.

  • Perhaps "they" changed the file from H to L , but we didn't touch the file at all. Then Git should use their version of the file, and that's what Git does.

  • As a final possibility, perhaps both we and they changed the file. This is where Git has to do real merge work. This is the only case where Git bothers to do real work , which is important for the section on .gitattributes and merge drivers below.

For all the easy cases marked above, Git simply takes one of the three copies of the file—whichever one is the right one (and if they're all the right one, you won't be able to tell which one Git took, so it won't matter).

Assuming all goes well, Git will now make a new merge commit . What's special about a merge commit is that instead of one parent, in its metadata, it lists two (technically "two or more" but we won't cover the "more" case here). We can draw this new commit as:

          I--J
         /    \₁
...--G--H      M   <-- br1
         \    /²
          K--L   <-- br2

The snapshot in commit M is that made by the work-combining. The metadata is all the usual standard metadata except for these two parents.

Some special cases

Now, there are two degenerate cases involving the commit graph . Above, we drew a situation where the merge base commit was followed by commits on both branches. (Side question as an exercise: which branch(es) hold commits H and earlier?)

Suppose we have this:

...--G--H   <-- br1
         \
          I--J   <-- br2

If we git switch br1 && git merge br2 as before, we're asking Git to combine the work we did with the work they did. But the merge base is commit H , as before, and the "ours" commit is also H and the "theirs" commit is J . So the work we did is automatically nothing at all . The difference between the snapshot in H and the snapshot in H is no difference at all .

Obviously, combining "something" with "nothing" produces the "something". The end result of a true merge would look like this:

...--G--H-----¹M   <-- br1
         \    /²
          I--J   <-- br2

with the snapshot in M exactly matching the snapshot in J .

You can force Git to make this "true merge", using git merge --no-ff . But if you don't do that—and most people don't—Git uses an even faster short-cut than usual, and simply makes the name br1 select the same commit as the name br2 :

...--G--H--I--J   <-- br1, br2

There is no new commit M at all! Git calls this a fast forward merge , even though there's no actual merge. (Other parts of Git documentation sometimes call this a fast forward or fast forward operation , leaving out the word "merge". Git has never been big on self-consistency...) You're left with the files from commit J in your working tree, as if you ran git switch br2 , but you're still "on" br1 and the name br1 now selects commit J .

Now consider this drawing:

          I--J   <-- br1
         /
...--G--H   <-- br2

We switch to br1 and ask Git to merge br2 . The best shared commit here is commit H as before, but commit H is already there in branch br1 , because the set of commits in a branch is determined by starting at the end and working backwards forever (or until we get to the very first commit anyway). So br2 's tip commit is already included. There is literally nothing to do, and Git will say so and do nothing.

(This provides you with the answer to the exercise above, too: commit H is on both branches , in our example where Git had to do a real merge.)

The hard case: "both sides" have changes

When Git is doing a true merge:

          I--J   <-- br1
         /
...--G--H
         \
          K--L   <-- br2

and some file(s) have changes on "both sides", Git has to do real work to combine those changes. Git's built in rules here are simple and stupid:

  • the diff, from merge base to J and from merge base to L , shows which lines were deleted and added, line-by-line;
  • if the areas where lines were deleted and added on "one side" don't overlap with, or abut, areas where lines were deleted and added on the "other side", Git takes those changes;
  • if the areas show that the two diffs touched the same lines in the merge base ( H commit) file, Git declares a merge conflict .

When Git has a merge conflict, Git makes you, the human programmer who presumably can figure out the right answer, clean up the mess. You're supposed to do this—however you like, perhaps using all three input files, or perhaps using Git's messy attempt at combining files, complete with conflict markers—and once you have the correct version of the file, you tell Git: This is the correct version and that's the one Git stores for the final merge result.

Git believes you, regardless of what you put into this file . So if you get it wrong, Git believes you got it right, and uses that. The whole result is up to you, for this merge conflict case.

Now, the main problem here is that Git is simple and stupid. It's working line by line, even if that makes no sense. You can therefore provide a merge driver for specific file names ( xyz.blah ) or patterns ( *.xml ). To provide your own, presumably smarter, merge driver, you list the file name or pattern in a .gitattributes file or similar, using merge= name . You then also have to define the merge driver itself, in a .git/config or .gitconfig file, by defining the name you used here.

This is all fairly complicated. It's easy to get it wrong, and hard to get it right, and it is hard to write a good merge driver. The one kind of merge driver that isn't hard to write is the "keep just one of the inputs" driver; "keep ours" is the easiest for technical reasons that we won't cover here. But because it is so hard, most people never do it at all. 1

Still, it's the answer to your question as asked: Could you have set Git up to ignore incoming files? Yes, using a "keep ours" merge driver. Note that such drivers don't actually work in general anyway, because if you didn't change a file, and they did change a file, Git takes their version, without stopping to run your merge driver at all . So the merge=ours that people try to do doesn't actually work. That's the other reason people don't do it: it doesn't work. 2


1 Someone needs to write a good XML merge driver. This itself is a pretty hard problem, regardless of whether one is to hook it up to Git. That XML driver should then be added to Git as a standard built-in optional merge driver. This would be a big service to all those who have XML files that get stored in Git. Merging XML files line-by-line, as Git does, tends to destroy them.

2 Git needs a way to make it work. It's ugly, but a simple annotation in the merge driver or gitattributes entry to mark a driver as "required" would suffice.

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