我有一個feature分支,我需要合并到其中,master所以我通過 github 創建了一個拉取請求并將其合并。
我原以為featurebrach中的所有更改都master將覆寫該特定檔案,master但在合并后我注意到相反的特性分支更新了代碼(主檔案中不同的代碼)并且主檔案保持不變。
如果我錯了,請糾正我的理解。當我們將分支 A 合并到分支 B 時,分支 A 不應更改。不是嗎?
謝謝,
uj5u.com熱心網友回復:
我猜你跑了git merge master。這將 master 合并到當前分支中。
要將 feature 分支合并到 master 中,需要切換到 master 分支,然后運行git merge feature. 話雖如此,您可以在不將功能合并到母版中的情況下創建 PR。
uj5u.com熱心網友回復:
我期望
feature分支中與 不同的所有更改都master將覆寫該特定檔案master...
這種期望是錯誤的。
合并并不意味著使相同。合并意味著合并作業。
這有點復雜:Git 不存盤您所做的作業。Git的商店,而是充滿快照的每個檔案。那是:
每個提交都有編號,有一個大的、丑陋的、唯一的哈希 ID。這個數字是 Git 從 Git 的主資料庫中實際檢索提交的方式。(這個資料庫包含提交和其他支持物件,所有這些都有這些丑陋的大哈希 ID。)
每個提交存盤兩件事:
提交具有每個檔案的完整快照。每個提交中的檔案都以特殊的、只讀的、僅限 Git 的、壓縮和重復資料洗掉的格式存盤。如果您有一個包含大量提交的存盤庫,并且您檢查了一個包含一些非常大的檔案和一些非常小的檔案的提交,并且您只更改其中一個小檔案并添加并提交,則 Git 必須存盤所有檔案重來一遍,但它可以重用除了一個更改過的檔案之外的所有檔案。因此,只有更改后的檔案才實際占用任何空間。但是每次提交仍然有每個檔案!
提交還存盤一些元資料,或有關提交本身的資訊。這包括您在
git log輸出中看到的內容:例如,誰進行了提交、何時提交以及為什么(他們的日志訊息)。出于 Git 自身的目的,每個提交也在此元資料中存盤先前提交哈希 ID的串列。
這個先前提交哈希 ID 串列通常只有一個條目。我們稱該條目為提交的父條目。這意味著每個孩子提交“指向”其父母:
... <-F <-G <-H
這里H代表最新提交的哈希 ID 。這個提交是一個提交,所以它有一個快照——所有檔案和你制作時的格式一樣H——和元資料。元資料說你是去年八月做的,或者別的什么。它還說提交的父項H是較早的提交G(無論真實的、隨機的哈希 ID 是什么:git log都會顯示它)。
G但是,Commit是一次提交,因此它具有每個檔案的完整快照和一些元資料。Git 可以提取兩個提交(到記憶體中的臨時區域)并比較檔案。當檔案完全匹配時——因此被洗掉重復——這很無聊,而且 Git 通常什么都不說。當檔案不匹配時,Git 可以想出一個配方——一組應用于舊檔案的更改——使其與新檔案匹配。這就是你在git log -p.
已將提交顯示H為較早提交的更改G,git log現在后退一跳。現在它的作業是顯示 commit G。為此,它需要提交F,G的母公司獲得快照弄清楚發生了什么變化,如果你想看到的是,但那是容易的,因為G持有F的哈希ID。
因此,Git 可以向您顯示作者和訊息等資訊G,然后與Fto的差異G以查看更改的內容,現在 Git 可以再次向后移動一跳并顯示給您F。這是一個提交,所以它有一個快照和一個父項,而且......好吧,這個想法現在應該很明顯了。
That's all well and good for simple cases, but we have one problem: we have to somehow give the hash ID for commit H to Git. We could scribble it onto a whiteboard, or jot it down on paper. But why should we do that, when we have a computer? Let's have Git store this hash ID for us, in—say—a branch name. And that's just what we do.
Now, if we have a simple chain of commits like this:
...--F--G--H <-- master (HEAD)
and we create a new branch name feature, we get:
...--F--G--H <-- feature, master (HEAD)
We're currently using commit H, via the name master. If we switch to feature, with git checkout feature or git switch feature, we switch to using commit H via the name feature:
...--F--G--H <-- feature (HEAD), master
Nothing else has to change, so Git takes a shortcut and changes nothing else: we're still using commit H, just through another name.
If we now make new commits, these new commits get:
- a new snapshot, from what we tell Git to use;
- new metadata: your name as author/committer and "now" as the date and time, with the parent commit being the current commit.
By writing out the new commit, Git acquires a new hash ID—there's some fairly deep magic here, using cryptographic hashes, which also explains why nothing about any commit can ever change once you've made it—but we'll just call our new commit I. New commit I will point back to existing commit H:
I
/
...--F--G--H
and now Git pulls its clever little trick: it writes I's hash ID into the current branch name, i.e., feature:
I <-- feature (HEAD)
/
...--F--G--H <-- master
If we make yet another new commit before we do anything else, we get:
I--J <-- feature (HEAD)
/
...--F--G--H <-- master
If we now switch back to the name master, here's what happens:
I--J <-- feature
/
...--F--G--H <-- master (HEAD)
We've now had Git attach the special name HEAD to the name master, which points to commit H, not commit J. Since we're changing commits, Git will now remove, from our working tree, all the files from J and put in all the files from H instead (using the snapshots).1
If we were to run git merge feature now, you'd get the thing you expected. But before we do, let's make more commits. We'll modify some file that also got modified in I and/or J, as we make two new commits K and L:
I--J <-- feature
/
...--F--G--H
\
K--L <-- master (HEAD)
If we compare the files in J vs the files in L (in either order), we'll get a recipe that would change what's in one of those two commits to match what's in the other. But if we run git merge feature, we don't want to lose the good stuff we did in commits K-L. So that's not what git merge does.
Instead, git merge:
- locates the current commit: that's easy, it's whatever
HEADis attached to, and we already have that commit checked out too, as--ours; - locates the other commit, in this case commit
J, from the name we supply: we can actually give a raw hash ID as Git just needs the commit; this becomes the--theirscommit; - and, last, uses the graph we've been drawing to find the best shared starting-point commit.
That last commit is obvious from the drawing here: it's commit H. Commit H is on both branches. Commits I-J are only on feature, and commits K-L are only on master, but all commits up to and including H are on both branches. So all those commits are shared, and H is obviously the best one.2
Now, to do the merge, Git will:
- compare the snapshot in
Hto that inL: that shows what you did, on branchmaster, to get to the--ourscommit; - compare the snapshot in
Hto that inJ: that shows that they did, on branchfeature, to get to the--theirscommit; - combine the two sets of changes.
The combined changes then get applied to the snapshot from H. That keeps our changes and adds theirs, or, equivalently, keeps their changes and adds ours. Either way, what ends up in the files doesn't necessarily match either commit J or commit L, because we took two sets of changes.
If all goes well, Git makes a new merge commit. A merge commit is exactly the same as any other commit: it has a snapshot, and a list of parent hash IDs. What makes it a merge commit is that the list of parent hash IDs has not just one hash ID, but two:3
I--J <-- feature
/ \?
...--F--G--H M
\ /1
K--L <-- master (HEAD)
There's still just the one snapshot. The first parent, marked with a tiny 1 here, leads back to the commit we were on when we started the git merge. The second parent leads back to the commit we told Git to merge.
Note that in the much simpler case of:
I--J <-- feature
/
...--F--G--H <-- master (HEAD)
Git still does all the fancy merge-base calculation. This time, however, our commit is H, theirs is J, and the merge base is ... H again. If Git were to compare the snapshot in H vs the snapshot in H, the list of changes it would find would be empty.
We can force Git to do that anyway, with git merge --no-ff. The result is a new merge commit, with two parents as usual:
I--J <-- feature
/ \
...--F--G--H------M <-- master (HEAD)
but now the snapshot in M really does match the snapshot in J. That's because Git compared H to J to see what they changed, then applied those changes to H, and used no changes from our side, to make the snapshot. Algebraically,4 H (J - H) is just J again.
If we don't force a real merge, Git will do a sort of fake not-actually-a-merge, called a fast forward. But GitHub won't let you do a fast-forward, if you use their clicky buttons: they have MERGE, REBASE AND MERGE, and SQUASH AND MERGE and none of those is Git's internal fast-forward. (The REBASE AND MERGE option comes close, but isn't the same.)
1This explanation deliberately skips a lot of weird corner cases that Git allows.
2Proof by vigorous hand waving, number 8 on the list. More seriously, see Lowest Common Ancestor of a DAG.
3Technically, a Git merge can have more than two parents, but that's rare (and mainly used for showing off ??).
4Some version control systems do attempt to define an algebra of changes, and hence do this sort of computation. It ... gets messy.
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/360180.html
