Merging 2 files with the same name in 2 different branches in git

偶尔善良 提交于 2020-07-10 07:05:21

问题


I currently have a file named test1 in a branch named branch1 created from master and another file also named test1 in a branch named branch2 also created from master. What's gonna happen to the code written in both files if I merge the 2 branches in master?


回答1:


Steps do to probably :

  1. Merge branch1 to master.

    This should go smoothly.

  2. Merge master to branch2.

    Here you will probably get a merge conflict. Resolve it.

  3. Merge branch2 to master.

This will make sure your master is never "poluted" even if you made some mistakes , there would be on branches, and would be resolved.




回答2:


If you made changes to both files in the same place, there will be a merge conflict.

If you didn't made changes to both files in the same place, it will merge without problems(maybe a conflict at another location).

You can solve a conflict by editing the conflicting files, removing the commit markers, adding the files and committing it.




回答3:


As amer answered (correctly), you'll get a merge conflict in one of the two merges. You will have to do something to handle the merge conflict. What to do is up to you. It's worth pointing out, though, why you will get one merge conflict, not two, and why the merge conflict happens.

Git's merging is not really about branches. It's about commits. Most of Git is really about commits, and git merge is no different here.

Let's note here what a commit is and does. Each commit has two parts: its data—a saved snapshot of all of your files—and its metadata, or information about the commit.

  • The saved snapshot is pretty straightforward: if you clone a repository and use --no-checkout, you have an empty work-tree (no editable or usable copies of any file, yet). Then you pick some commit—any commit anywhere—and tell Git to check out that particular commit, perhaps by its raw hash ID. Now you have a copy of all the files as of the way they looked when whoever made that commit, made that commit.

    In general, this is what git checkout or the new-in-Git-2.23 git switch command is for: you pick some commit and say get me all the files from that commit. They go into your working tree or work-tree where you can see them and work on them. You can also put other, un-Git-ified files into your work-tree, e.g., compiled files or outputs or whatever. These files remain untracked (I won't go into the precise details here, but they're not in Git, they just sit there in your work-tree unless/until you remove them).

  • The metadata in a commit records things like who made it—their name and email address—and when they made it. This is the stuff that you see in git log output.

    There is one part of the metadata, though, that is specifically for Git itself. Every commit has a unique hash ID, which every Git everywhere agrees on: that one hash ID is for that commit, and never for any other commit. So it's easy to tell if you have some commit: you just give its hash ID to Git, and your Git either has it, in which case, it has that commit, or your Git doesn't have it, in which case you'll have to find some other Git hat has it.

    Anyway, every commit stores, as part of its metadata, the hash ID of its previous or parent commit. Most commits just have one. Merge commits are slightly special because they have two parents (or more, but most just have two).

    These parent commits—or parents, for merges—are how Git stores history. Every snapshot is just a snapshot, but every snapshot also says: and my previous snapshot is ______ (fill in the blank with a hash ID). For merges, this is the first parent. For regular commits, it's the only parent (and hence also the first parent). So by going back to the first parent of each commit, Git can trace back what has happened over time. Put up two snapshots: an old snapshot, on the left, and a new snapshot, on the right, and compare them. What's different? That difference tells you what happened: what changed between the old one and the new one.

Once you know this about commits, we need only add one more thing to make branches work. In Git, a branch name records the hash ID of the latest commit that we wish to call "part of the branch". That's mostly it—that's what a branch name does for us and for Git. It records the last commit. The commits themselves record the history.

So, given a series of commits, in a very small repository with just three commits and one branch name, we have, for instance:

A <-B <-C   <--master

The last commit is C. We have the name master to store its actual hash ID—which is really some big ugly random-looking string of letters and digits, that we never could guess. Commit C itself stores the hash ID of earlier commit B, so that C points to B; and commit B stores the hash ID of earlier commit A.

Commit A is special: it doesn't point back at all, because it was the very first commit and can't point back to an earlier commit. That's how Git knows to stop going back: when it can't.

We could, given a Git repository, go in and find all commits, and see which ones are the last one(s), but having a name that finds them quickly is faster. This also becomes important when we start having more than one branch. Let's start with a small repository with about eight commits:

...--G--H   <-- master

Now let's add a new branch. We'll start by having the new name also select commit H. We need a way to know which branch we're using, so we'll attach the special name HEAD to one of the branch names:

...--G--H   <-- master, feature1 (HEAD)

Now we'll add a new commit, which gets some new random-looking hash ID that we will just call I:

          I   <-- feature1 (HEAD)
         /
...--G--H   <-- master

When we add a new commit, Git automatically updates the branch name to point to the new commit. Which branch name gets updated? The one HEAD is attached-to. The others all stay in place.

Now all the commits through H are on both branches, and commit I is *only on feature1. Let's make another commit, then make a new branch feature2 that selects commit H, and start using that one:

          I--J   <-- feature1
         /
...--G--H   <-- master, feature2 (HEAD)

Now let's add two commits to feature2:

          I--J   <-- feature1
         /
...--G--H   <-- master
         \
          K--L   <-- feature2 (HEAD)

Now, suppose in commit I or J, we created a new file test1, that's not yet in commit H. Suppose that in commit K or L, we also created a new file named test1.

Merging

We're now going to merge the two features into master, one at a time. For no obvious reason,1 we will use the --no-ff option:

git checkout master
git merge --no-ff feature1

to achieve this.

When we git checkout master, we direct Git to:

  1. extract the commit identified by the name master—commit H—to our work-tree (and to Git's index, which we won't get into here); and
  2. set up our work-tree to match, which means removing the file test1, which is in commit L—there is a saved snapshot file with that name—but is not in commit H.

So, now we have:

          I--J   <-- feature1
         /
...--G--H   <-- master (HEAD)
         \
          K--L   <-- feature2

and we're ready to run git merge --no-ff feature1.

Git now finds three commits, not just two. The three commits of interest are:

  • Our current commit, HEAD. That's really easy to find because HEAD is attached to a branch name and the branch name points to the commit, so Git finds commit H.

  • The other commit we named. That's really easy too: we said to merge feature1. The name feature1 identifies commit J. (Just look at the drawing!)

  • The merge base. The merge base is defined by the commit graph, formed by the inter-connections from one commit to another. While we won't go into all the details, you can think of this as the best shared commit, i.e., the best commit that's on both branches. Starting from J—as found by name feature1—we work backwards; and starting from H, as found by master, we also work backwards. When some commit is on both branches, that's a shared commit. The newest such commit—with newest not being properly defined here, but in most cases it's obvious—is usually the best commit.2

In this case, the merge base is clearly commit H itself.


1The merge I'll do here is the kind you would get on GitHub, using its "merge pull request" button. From the Git command line, you get more options. The --no-ff option forces command-line Git to make a real merge, instead of using its short-cut "fast forward not-really-a-merge" option.

2Technically, what Git is doing is finding the Lowest Common Ancestor (LCA) in a directed graph. In a tree, there is always one well-defined LCA, but Git's commit graph is not necessarily a single tree: it's just a Directed Acyclic Graph or DAG. Two commits may have no LCA, or may have more than one LCA, and merge does different things for these cases.


Merging, part 2

Having found the merge base, Git now runs two of its compare two commits and see what changed operations. Comparison #1 compares the merge base to the --ours commit, i.e., to HEAD. So Git will do:

git diff --find-renames <hash-of-H> <hash-of-H>   # what we changed on master

Obviously, commit H is the same as commit H. Nothing at all changed!

Then, Git does a second diff, to see what "they" (we) changed on the other side:

git diff --find-renames <hash-of-H> <hash-of-J>   # what they changed on feature1

What merge does, then, is to combine these two sets of changes. Where we changed some file, and they didn't, Git takes our change. Where they changed some file, and we didn't, Git takes their change. These combined changes get applied to the merge-base snapshot. That way, we keep all our work and add their work—but wherever we and they made different changes to some file or files, Git will show a merge conflict.

In this case, the --ours diff is completely empty: we didn't change anything. So whatever "they"—really, we on feature1—did, Git takes those changes. That includes adding a new file test1. This combining goes well, so Git makes the new merge commit on its own.

The first parent of the new merge commit is our current commit, H, on master. The second parent of the new merge commit is their commit J, on feature1. We can draw that—the drawing here doesn't show first vs second commit properly, but we can just remember it if we need to, or ask Git about both parents to see which one is first, or whatever.

The result looks like this:

          I--J   <-- feature1
         /    \
...--G--H------M   <-- master (HEAD)
         \
          K--L   <-- feature2

Note how no other branch name moved: we're still on master, and it has moved to point to M, and feature1 still names commit J and feature2 still names commit L.

The conflicting merge

If we now run another git merge—this time with feature2—Git will once again locate three commits:

  • The ours and theirs commits are commits M and L, of course.
  • The merge base is the best shared commit.

Look at the diagram. Which commits are on both master and feature2? Commits G-H-I-J-M are all on masterH in two ways, directly from the first parent of M, and indirectly by going from J to I to H through the second parent of M—and hence G is there in two ways, and so on, but all we really care about is that H and G are there.

Meanwhile, feature2 ends at L, goes back to K, then goes back to H. So commits H and G are both shared. Commit H is the best one, though. Once again, then, the merge base is commit H.

Git will once again run two git diffs, both with --find-renames (to check for renamed files) and both from H to the two branch tips. So Git is going to compare the snapshot in H against the one in M, to see what we changed.

What did we change, from H to M? Well, in M, we added all the changes we got by comparing H vs J. So any files we changed in feature1 are changed in M. But we also added a new file, test1, in either I or J, so this change-set says add all-new file test1.

When we compare H vs L, that, too, says add an all-new file test1. So both changesets say to add a new file.

Git calls this kind of conflict an add/add conflict. In the work-tree, Git just leaves you with the entire contents of both files as your conflict. You must resolve this conflict in some way. How you go about it is up to you. Whatever you choose to put in file test1, you can now run:

git add test1

and Git will assume that whatever is in the file test1 is the correct resolution for that conflict.

Be sure to edit the file! If you don't, it just has the conflict markers in it, and Git thinks that's the right answer! It probably isn't.

Once you have resolved all conflicts, and are sure that the merge result is correct—you've done any testing you need to do, for instance—you can safely finish the merge by running either:

git merge --continue

or:

git commit

(The git merge --continue just makes sure you're still finishing the merge, then runs git commit for you, so they wind up doing the same thing—unless you already finished, or terminated, the merge, that is.)

Git will now make another merge commit; we'll call it commit N, and draw it like this:

          I--J   <-- feature1
         /    \
...--G--H------M--N   <-- master (HEAD)
         \       /
          K-----L   <-- feature2

The first parent of N is M, and the second parent of N is L. There are now three ways to get from N to H, and all commits in the diagram are on master.

It's now safe to delete the names feature1 and feature2 because Git can find those commits—including J and L—by going backwards from commit N. You don't have to delete the names, if you want to retain the ability to find commits J and L directly and quickly, but they're no longer necessary, like they were before the merge operations.



来源:https://stackoverflow.com/questions/59532257/merging-2-files-with-the-same-name-in-2-different-branches-in-git

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!