简体   繁体   中英

I merged branches develop into master but on master branch no changes from develop

git checkout develop
git pull origin develop
git checkout master
git merge develop
git push origin master

With this method I merge develop into master but after merging I'am run app in master branch and on app doesn't get changes on branch develop.

在此处输入图像描述

There's nothing specific we can say about your particular repository unless we have access to your particular repository. In general, though, this statement:

doesn't get changes on branch develop

is meaningless. The reason is that Git branches don't have changes . In fact, Git does not even store changes. Instead, Git stores commits , and commits hold snapshots , not changes. To understand what git merge does, you must understand commits first, in a deep way: what a commit is and does for you, and how you gain access to a commit.

Git stores commits; commits store snapshots and metadata

The heart of any Git repository is a big database holding commits (plus other internal Git objects that are needed to support all of this). These commits (and other objects) are numbered, but the numbers aren't simple counting numbers: they don't go commit #1, #2, #3, etc. Instead, each commit or other internal Git object has a hash ID: a big ugly string of letters and digits that represent, in hexadecimal , a very large number. 1 These hash IDs look random, but are not: they are actually a cryptographic checksum of the content of the object (content of the commit, for commit objects).

These numbers—the hash IDs—are how Git actually finds commits. The hash ID is the key in a key-value database ; the stored data, which must hash back to the key used to find it, is the stored object. This means that once a commit, or any other internal object, is made, it can never be changed . If you take a stored object out of the database, change it, and store it back, it goes under a new and different key and the old object remains in the database, unchanged. So commits literally can't be changed.

Each commit holds two useful chunks of data. One of these, we call metadata , because it's data about the commit itself. This is where Git keeps things like the name and email address of the person who wrote the commit. The other important part of a commit is the main data, and that is a saved snapshot of every file as of the form the file had at the time you, or whoever, ran git commit .

Note that this means that commits do not have changes in them . Each commit's data is a full copy of every file . This full copy of each file is read-only (because no part of any commit can ever be changed). To save space, the files inside commits are stored in a special Git-only format—not as ordinary computer files at all—that's compressed and de-duplicated . Each commit can therefore share the individual file data from previous commits. Since this stuff is all read-only, it's safe to share: nobody, not even Git itself, can change the saved file-content data (it uses that same key-value database with hash IDs as the commits).

There's one other important item to know. The metadata for each commit contains a list of previous commit hash IDs that Git has stored for that commit. Most commits have just one previous commit. 2 What this means for you is that commits form simple backward-looking chains:

... <-F <-G <-H

Here, H stands in for the actual big ugly hash ID of the last commit in the chain. As long as we know this commit's hash ID—for instance, maybe we wrote it down on paper, and type it in again each time we want it—we can have Git extract this commit, both its data (snapshot) and metadata (information about the commit). In the metadata, we—or Git—can find the actual hash ID of earlier commit G .

Earlier commit G , of course, has both data and metadata. The metadata for G holds the actual hash ID of still-earlier commit F , so Git can find F from G . F in turn points back to another even-earlier commit, and this goes on all the way back to the very first commit ever, and then stops.

The files inside each commit are stored forever (or as long as the commit itself lasts anyway). We can have Git copy them out of the commit, to ordinary files, any time. They're stored in a form that only Git can use , and nothing else can change , but once we extract them, we'll be able to use and change them. This extraction process is called checking out the commit. 3


1 The range of possible numbers goes from 1 to 2 160 -1, or 1461501637330902918203684832716283019655932542975, at the moment. That's a little more than 10^48. In the future the range will be even larger, going up to 2 256 -1—about 10 77 possible numbers.

2 The usual exceptions are the very first commit, which can't have any previous commit and therefore doesn't, and merge commits , which tie two previous commits together, and therefore remember each one's hash ID. Commits with one previous-commit ID are ordinary commits . Commits with none are root commits —most often there's only that one, from the first commit ever—and commits with two or more are, by definition, merge commits .

3 We actually get a couple of different ways of extracting, in modern Git. In older versions of Git there was an issue with extracting specific files from old commit, but now that git restore exists, we can do it properly. This answer won't get into the finer details of Git's index , though.


Branch names, and how git log and your screenshot work

This backward-looking chain is what allows git log to work. Given some starting point, or more than one starting point if you like, Git can find these starting-point commits. These commits point back to earlier commits, so Git can use these to find earlier commits.

To get started, though, Git needs the hash ID(s) of some last commit(s). Where will these come from? Above, I suggested that we could write down the actual hash ID for commit H on paper. But if we do that, and retype it, we'll make mistakes. We don't have to do that: we have a computer , and the computer can remember these hash IDs for us.

This is what names—branch names, tag names, and all of Git's other names—are for. Each one goes in a second database—another key-value store —that's indexed by name instead of hash ID. The values in this second database are the hash IDs. This database, however, is writable: we can replace the hash ID for each branch name whenever we like. The object database only allows adding new objects, but the names database lets us overwrite any name, any time.

We need to use this power with a bit of care, because by definition, whatever hash ID is stored in a branch name is the last commit that we want to call "part of the branch". In Git terms, the branch name holds the hash ID of the tip commit . The tip of the chain is the last commit in that branch, even if the chain keeps going:

...--G--H   <-- main
         \
          I--J   <-- feature

Here, we have two branch names, main and feature . The name feature locates commit J (by storing its hash ID). Commit J points backward to commit I , which points back to commit H . The name main locates commit H . Commit H points back to commit G , and so on.

This means commits up through and including H are on both branches . Git is a little odd here: many version control systems don't do this thing of having commits be on more than one branch at a time. But Git does, so if you're going to use Git, you must accept it.

Getting changes from snapshots

Suppose we have this series of snapshots:

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

The last snapshot, the tip of branch master , is the one in commit H . But there are lots more snapshots. For instance, there's one in commit G .

What would happen if we asked Git to extract commit G —its snapshot, that is—to one location, and commit H to another location? We could compare all these files. Some of them would likely be exactly the same. In this case, Git will have de-duplicated the files' content, so that G and H literally just share those files. Others will be different: G and H have different copies of those files.

The file that are the same aren't very interesting. Let's toss those out (perhaps by removing them from the checked-out copies in the two locations). What remain are still snapshots, but we can now compare them, side-by-side, in a game of Spot the Difference . Whatever has changed in these files, well, that's what changed between commit G and commit H .

If we ask Git, or other viewing software, to show a commit as changes, this is what it does. It "extracts" (in memory) both commits, using the internal way that commits are stored to very quickly discard all the exact duplicate files—this is easy because of the earlier de-duplication. Then it compares the different files and comes up with a set of instructions. Applying these instructions to the earlier-commit's files produce the later-commit's copy of those files. This is a diff and this is how we like to view commits. But it's not how they're stored : Git will make a new diff every time we look for one.

We don't have to compare adjacent commits. We can compare commit F vs commit H , for instance, to see what changed from F to H . Using the git diff command, we can compare any two commits we like . Git will compare the snapshots and give us a recipe for changing one into the other. We can do the comparison backwards, if we like, to get a recipe for changing H back into G (a "reversed diff"). All commits hold a snapshot, so you can pick any two commits and compare them this way.

Merge is about combining work

All the concepts presented so far are not really difficult. They have a lot of tricky parts when you open each one up and look inside, but the concepts are simple enough. It's crucial that you understand them, at least at a high level, so that we can jump into how git merge works, for the case of a true merge.

Suppose we have the following series of commits, ending in two branch tip commits:

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

We'd like to run git checkout br1 and then git merge br2 , and the idea here—the goal of this operation—is to combine the work done in the two branches . Moreover, we'd like to have Git do as much of this combining as possible.

Now, commit J has just a snapshot , plus the usual metadata. The same holds for commit L . We could use git diff to compare the snapshot in J to the one in L , but that's not going to work terribly well. Suppose some file is different in J vs L :

This is
quite
a file.

vs:

This is
not
a file.

Comparing the J and L copies of this file just tells us that they're different . We don't know anything about any work done in the branches.

What about using commits I and K ? Well, if we compared the snapshot in I to the one in J , that could tell us about things done in br1 . So that seems at least a little better. But what about things done in commit I that make that snapshot different from commit H ? That was "work done" in br1 too. So we'd best go all the way back to H .

Meanwhile, the same arguments apply for commits L , K , and H . We need to go all the way back to H before we start seeing "work done in branch br2 ". Commit H shows up in both of these. What's special about commit H ? Think about this for a moment. Look at this drawing again:

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

If you said that the special thing is that commit H is on both branches , you're right. Of course, so is commit G , and any commits before G . We could actually make use of those, but G -vs- H is already on "both branches", via the snapshot in H . So there's no reason to go back further. H is the right place to stop: it's the best shared commit on both branches .

Git calls this best-shared-commit the merge base . It's extremely common to have only one merge base, as in this example, although there are complicated cases that have two or more. We won't worry about those cases here, though if you want to explore the theory behind all of this, look into the Lowest Common Ancestor of a Directed Acyclic Graph . If you're using Git and want to find the LCA(s), run git merge-base --all br1 br2 for instance, to see these commit hash IDs. In this particular case you would get just the one commit hash ID for commit H , so that is the merge base for this merge.

Now, Git could compare H vs I , then I vs J . But in fact, it doesn't need to do this. 4 So it just compares H vs J directly, as if by running:

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

Then it does the same for H -vs- L , with another git diff --find-renames and the two hash IDs:

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

This produces two recipes for changing the snapshot in H into the ones in J and L respectively. The merge process is now straightforward: we look at what needs to be done in each recipe , combining any individual changes. If H -vs- J said to do nothing to the:

This is
not
a file.

file, but H -vs- L said to change the middle line to read quite , then the combination is to make that change.

Git applies these combined instructions to the snapshot found in commit H . That way, we keep all the work we did on br1 —remember, we started with git checkout br1 —and add all the work they—whoever they are—did on br2 .


4 The way Git handles file renames is tricky, and might actually benefit from a commit-by-commit scan from the merge base to the branch tip. Git currently does not do this, though.


Making a merge commit

Merging—as performed by running git merge br2 for instance—is a fairly long and complicated process. We first pick out the branch we want to be "on", with git checkout br1 :

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

The HEAD in parentheses here shows us the branch we are "on". Any new commits we make will be "on" this same branch and will cause Git to write the new commit's hash ID into the branch name.

The git merge command then does a bunch of analysis. This includes finding the merge base commit or commits—in this case, commit H —and deciding whether to actually do the work to compute the merge result. Some of the options to the git merge command can help control this. In our particular case, git merge has to do a real merge anyway, so we don't need any options to get Git to do the work. 5

This means that Git begins the merging process, or what I like to call merge as a verb . It has located commit H and determined that a real merge is required to combine H -vs- J and H -vs- L . It generates the two diffs, internally, to figure out how to do this combining. Then it looks at each of the various to-be-merged files to actually do the combining.

If something goes wrong here, Git will stop with a merge conflict , leaving us to clean up the mess. Such a merge is still in progress, but no Git commands are running any more: they have recorded everything in files and quit. You must now use individual fixing commands 6 to fix each conflict; these record what you said is the right result. Once you've recorded it all, you run git merge --continue to get the merge to read the recordings and finish the merge.

Assuming nothing goes wrong, however, Git will finish all the work-combining on its own, and make the final merge commit all by itself, without a git merge --continue . The result is a commit. Like every commit, it has a snapshot and metadata. The only thing special is that the metadata lists two previous commits. That's what makes this a merge commit —merge as an adjective—or a merge , using the word merge as a noun. Let's draw the result:

          I--J
         /    \
...--G--H      M   <-- br1 (HEAD)
         \    /
          K--L   <-- br2

Note that the name br1 now points to the new commit, as usual. The only thing special about this new merge— merge as a noun —is that it points back not only to commit J like any other commit would, but also to commit L , which is what makes it a merge commit. The merge as a verb process leaves this merge as a noun/adjective commit, which lets us know that br2 is now merged into br1 : M has L as a parent, and br2 points to L , so the name br2 is no longer necessary and can be deleted immediately if we want.


5 If the merge base is the current commit and the other branch tip commit is "ahead of" this commit, git merge can, and by default will, do a fast-forward operation instead of merging. You can force Git to do a real merge here with git merge --no-ff . If the merge base is the other commit and that's either the same as this commit, or "behind" this commit, no merge is possible, and git merge will say Already up to date and quit. If the two commits are not related—so that there is no merge base at all— git merge will complain that the histories are unrelated, and quit; the --allow-unrelated-histories option makes it do a merge anyway, using the empty tree as a fake merge base. The -n / --no-commit and -s / --squash options prevent Git from making a new commit, and the -s option makes Git "forget" the merge by the time it stops. We won't go into any detail for these options here though.

6 This can consist of running your editor on the work-tree file and using git add , or you can use git mergetool if you like it (I don't), or any other procedure you like. Git does not force you to use any particular method, but it does trust that you get the merge result correct. It doesn't check , it just assumes that whatever you did was right!


Merge can mean you're done, but does not have to mean that

Now that we have:

          I--J
         /    \
...--G--H      M   <-- br1 (HEAD)
         \    /
          K--L   <-- br2

we can simply delete the name br2 . A branch name's purpose is to find some commit (the tip commit of the branch) and if L doesn't need to be found specially, we can leave it to be found by starting at M and working backwards. But if we like, we can keep making new commits on br2 , by doing:

git checkout br2

and then doing work and running git commit . The result might look like this:

          I--J
         /    \
...--G--H      M   <-- br1
         \    /
          K--L--N--O--P   <-- br2 (HEAD)

At this point, there are commits on br2 that can only be found by starting at P —the via the name br2 —and working backwards, so now we can't delete the name br2 .

At this point, too, we can run git checkout br1 and then git merge br2 . Merge will once again find the best shared commit . This time, that's actually commit L .

The merge-as-a-verb process will start by comparing L -vs- M , to see what "we" did on br1 , and then comparing L -vs- P , to see what "they" did on br2 . Git will then try to combine these changes, applying them to the snapshot in L . That will keep "our" changes on M —the ones we brought in via the merge that made M —and add "their" changes on P , and if that works, we'll get a new merge commit M2 or Q or whatever we want to call it:

          I--J
         /    \
...--G--H      M--------M2   <-- br1 (HEAD)
         \    /        /
          K--L--N--O--P   <-- br2

At this point, we can delete the name br2 safely (again), or keep it (again), as we like.

Conclusion

Merge is about combining work—changes—but Git doesn't store changes. The only way to find changes is to compare some particular commits. This means that if you want to find out what a merge will do, or why a merge did what it did, you must compare the various commits:

  • Find the merge base, using git merge-base --all .
  • Compare the base to the branch tips using git diff --find-renames .

What you see as these two sets of differences were the inputs to the merge. Any options given to the merge, such as -s ours or -X ours , can affect how the changes got combined . Note that -s ours means disregard their changes entirely . Unfortunately, git merge does not record, in the final merge commit, any of the options used to produce the merge. (I consider this a bug: it really should be in the merge commit's metadata.)

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