在我發布我的問題之前,我想提一下我在另一個 Stack Exchange 網站上問過這個問題,并被告知這個問題需要在 Stack Overflow 中提出。
我最近在 Bitbucket 中創建了一個新存盤庫,我打算在其中簽入一個我已經從事了一段時間的新專案。當我在 Bitbucket 中創建新專案時,我選擇了包含.gitignore檔案的選項。當我嘗試推送我的新專案時,它導致我無法解決與此檔案的沖突。目前,我的代碼卡在我的本地存盤庫中。
我試過的
- 我試圖將 .gitignore 標記為在 Eclipse 中合并。我收到此錯誤:無法拉入狀態為: MERGING 的存盤庫。然后我按照Stack Overflow answer中的建議執行了git merge --abort。
error: Entry '.gitignore' not uptodate. Cannot merge.
fatal: Could not reset index file to revision 'HEAD'.
- 出現上述錯誤后,我嘗試了硬重置(來自 Eclipse)。然后我再次嘗試中止(來自上面的第 1 項。這導致了以下錯誤:
fatal: There is no merge to abort (MERGE_HEAD missing).
- 然后,我嘗試從 Git Bash 中提取結果:
$ git pull
error: Pulling is not possible because you have unmerged files.
hint: Fix them up in the work tree, and then use 'git add/rm <file>'
hint: as appropriate to mark resolution and make a commit.
fatal: Exiting because of an unresolved conflict.
- 嘗試通過運行git rm .gitignore洗掉 .gitignore ,然后呼叫git pull。我收到以下錯誤:
fatal: refusing to merge unrelated histories
As you can see, I tried to fix from Git Bash and from Eclipse, but I have not been able to make any progress. I don't care losing the information on the .gitignore file. I can always recreate the file. I just need to resolve this conflict by any means necessary so that I can push the stuff in my local repo to the remote one. I am at a loss.
uj5u.com熱心網友回復:
TL;博士
如果您只想覆寫.gitignoreon Bitbucket,請考慮使用git push --force完全丟棄初始 Bitbucket 提交。
如果您想保留該檔案,請將其從該提交中取出:
git show origin/master:.gitignore > ignore.bitbucket
例如,然后根據需要合并檔案,然后使用強制推送丟棄(單個)Bitbucket 提交。
長
這是問題的根源:
當我在 Bitbucket 中創建新專案時,我選擇了包含
.gitignore檔案的選項。
Git 與檔案無關,因此不存盤檔案——至少在您可能想到的意義上不是。Git 是關于什么以及 Git 因此存盤的是commits。1 提交——但不是分支名稱——可以通過祖先關聯:就像大多數人有兩個父母一樣,大多數提交都有一個父母。例如,一些提交可以是其他一些提交的偉大-偉大-偉大-孫子,或者兩個提交可能是兄弟姐妹(都具有相同的父級),或其他類似的關系。
(這些關系形成了一個有向無環圖或DAG。特別是提交圖允許 Git 找到兩個分支提示提交的共同祖先。)
提交本身,我們讓 Git 通過它們的哈希 ID 找到,每個都包含兩件事:
每個提交都以壓縮和只讀格式保存每個檔案的完整快照,其中內容已洗掉重復(跨所有提交)。這些快照就像 tar 或 rar 或 winzip 檔案;需要先提取它們,然后才能實際使用檔案(然后提取的檔案此時不在 Git中,盡管在 Git中顯然有一個副本:它只是存盤在這個壓縮和去重的 Git-ified格式)。
每個提交還存盤一些元資料,或有關提交的資訊。例如,元資料包括提交人的姓名和電子郵件地址,以及顯示他們何時提交的日期和時間戳。在這個元資料中,為了自己的提交圖目的,Git 添加了先前提交串列的原始哈希 ID——通常只有一個,因此 Git 必須一次向后走一個提交,從最新提交到第二個提交-latest,到第三個最新,依此類推。
用戶當然想要他們的檔案。這些檔案在提交中,在它們永久存盤的檔案中。因此,我們必須選擇一些提交并讓 Git 提取該提交,以便我們可以查看和處理一些檔案。
If you have Bitbucket (or any other web hosting site) make a new, totally-empty repository, it will have no commits in it yet. That's fine, but if and when you clone this empty repository, you will get a warning: Git will say that there was nothing for it to check out (which is true!). This means if you make your own new, totally-empty repository, your new empty repository has the same lack-of-commits as your Bitbucket repository: the two are completely identical, in that both have nothing at all.
You can then make your own first commit, in your own repository, on your laptop or wherever it is stored. Being the first commit ever, this commit will have no parent commit. It can't have a parent, as there is no earlier commit, so it doesn't have a parent, and everything is fine. This commit is slightly special though, and Git will tell you that you have made a—or "the"—root commit:
$ git commit -m initial
[master (root-commit) c5f8984] initial
1 file changed, 1 insertion( )
create mode 100644 README.txt
But you didn't do that: you had Bitbucket create a repository and then create its own root commit in that repository, so that their repository is not empty. Their first commit contains a .gitignore file (and perhaps nothing else, or perhaps a README and/or LICENSE file; those are the typical GitHub options).
If you go on and make a root commit in your own initially-empty new repository, these two root commits are not related. They are not parent-and-child. They are not siblings, with a common parent. They are completely unrelated.
I use the terms your Git and their Git as shorthand here to refer to your Git software working with your repository and their Git software working with their repository. When you cross-connect two Gits like this, one of them is a sender and one is a receiver, and the sender will send some or all of his commits to the receiver, if the receiver doesn't have them yet. That's how we synchronize two repositories, especially when one was a clone of the other at some point.
Cloning copies all of the commits (and none of the branches, sort of), so a clone made at some point in some repository's lifetime will start out with the same commits as the original, and any added commits will generally have some sort of ancestry-relationship. But again, this doesn't quite work with an empty repository, since there's no initial commit.
So, what happened in this case is that after you set up the remote, you had your Git call up their Git and get, from them, their root commit that contains a .gitignore file. Meanwhile you had your own root commit that contains a .gitignore file.
It's not clear to me exactly what happened in Eclipse (Eclipse has its own Java-based Git implementation that doesn't quite do the same things as the C based implementation you get with command-line Git from bash or other shells). However, it left you in a weird state, which you fixed up, sort of, with your git rm command. At this point git pull from bash ran:
git fetch: anygit pullalways runs this first. Thisgit fetchhad nothing to do though, as you'd already obtained the Bitbucket root commit.git merge: thegit pullcommand means—is shorthand for—rungit fetch, then run a second Git command and the default second Git command isgit merge.
For git merge to work, it needs to find the best common ancestor between two tip commits. To describe this properly we need some annotations.
1More precisely, a Git repository consists of two databases, one holding commits and other internal Git objects, and one holding names. The two databases are simple key-value stores, with the objects database keyed by hash ID, allowing Git to retrieve the raw object contents by hash ID, and the names database pairing individual refs—branch names, tag names, and other such names—with a single hash ID.
Drawing branches in Git
As I mentioned above, each commit is a two-part entity, holding metadata (information about the commit) and data (the snapshot archive, in the Git-ified format). Each commit has a unique2 hash ID. To draw this, in a format suitable for human comprehension, we need to drop a lot of irrelevant detail, keeping just two things:
- a name for each commit, and
- the arrows coming out of the commit that form the arcs in the directed graph.
The result very often looks something like this:
... <-F <-G <-H
where H, on the right, stands in for our latest commit's hash ID. Commit H has metadata that include the raw hash ID of an earlier commit, which we'll call G. Commit G has metadata with the hash ID of still-earlier commit F, and so on. By holding each previous-commit hash ID, the commit can be said to "point to" the earlier commit.
To quickly find any particular commit—in our case, our latest commit H—Git uses a branch name like master or main, or a tag name like v1.2, or some other name like origin/master. This name contains the raw hash ID of the actual commit, so it too can be said to "point to" a commit:
...--G--H <-- main
What makes a branch name special in Git is that you can check out or switch to some branch so as to extract the files from its latest commit. Running git switch main here tells Git that you'd like to have, to see and work on/with, the contents of the files as they exist saved forever in commit H. Git therefore extracts those files to a work area—your working tree—and, as soon as it has done that, remembers which branch name you asked for. To remember the branch name, Git attaches the special name HEAD, which I draw like this:
...--G--H <-- main (HEAD)
You can create new branch names at any time. Each name must point to exactly one commit. If you were to pick commit G, for instance, to create the new name br1, we would draw it this way:
...--G <-- br1
\
H <-- main (HEAD)
We're still "on" branch main, and still using the files from commit H, but now there's a direct way to find the hash ID of earlier commit G, rather than, e.g., running git log and finding the hash ID of G manually. But most often, when we create a new branch name, we make it point to the current commit:
...--G--H <-- br1, main (HEAD)
We can now switch to that branch with git switch br1. This attaches our HEAD to the name br1, and extracts the files from H—except we already have all the files from H, so Git doesn't bother doing anything at all, other than shuffling the attachment of HEAD here:
...--G--H <-- br1 (HEAD), main
If we now create a new commit, the new commit gets a new, unique hash ID. We will call it "commit I" for simplicity. New commit I will point back to the commit we were using when we made new commit I, i.e., commit H, and once Git has saved away all the files and metadata for new commit I, Git will write I's hash ID into the name to which HEAD is attached:
I <-- br1 (HEAD)
/
...--G--H <-- main
If we make another new commit J, J will point backwards to I, and Git will write J's hash ID into br1:
I--J <-- br1 (HEAD)
/
...--G--H <-- main
If we now switch back to main, Git will remove, from our work area, the files that go with J, and put in the files that go with H instead, and leave us with this:
I--J <-- br1
/
...--G--H <-- main (HEAD)
We can now create and switch to a new branch br2:
I--J <-- br1
/
...--G--H <-- br2 (HEAD), main
and as we make more commits, now br2 will get updated:
I--J <-- br1
/
...--G--H <-- main
\
K--L <-- br2 (HEAD)
That, in a nutshell, is how Git branches really work: we add commits to them and they grow, and the branch name always means the last commit on the branch.
Git allows us to move, or even delete, any branch name at any time (although you're not allowed to delete the one HEAD is attached-to; you have to switch away from it first). If we move the name br2 back one step to commit K, we get:
I--J <-- br1
/
...--G--H <-- main
\
K <-- br2 (HEAD)
\
L ???
We won't look at how we do this, here, we'll just note that commit L still exists—but now you can't find it, unless you memorized its random-looking hash ID.3
2The unique hash ID is the real key to making Git work. The hash ID is unique across every commit in every repository, so that if two Gits meet, and compare hash IDs, they have the same hash ID if and only if they have the same commit, which they must have gotten by fetching or pushing (or during the initial clone, which is mostly one big fetch).
3Git has ways to get commit L back again, but if you leave it unreachable like this for more than a month or so, Git may get around to deciding that you don't care about it, and remove it for real. So commits are not always forever, but removing them is tricky: you arrange for Git not to see them—to have no names that let you and Git find them—and then eventually they will go away, unless they got sent to some other Git that decides to hang on to them.
Because Git is very greedy for new commits, once you've sent a commit somewhere else, it is very hard to get rid of for real. If you think of commits like viruses for which there is no vaccine, you're not far off. ??
Merging
Git has a lot of things that git merge will do that aren't merges, but to understand them and Git, it's best to start with the kind of git merge that is a full-blown merge. Suppose we have:
I--J <-- br1 (HEAD)
/
...--G--H
\
K--L <-- br2
from before. We're on br1, using commit J—our working tree has the files from J—and we decide we'd like to combine our work on br1 with the work someone did—maybe that was us, maybe it was someone else; Git won't really care, as Git only cares about commits. So we run:
git merge br2
Git will find not one or two but three commits. Git starts with our current commit J, which is easy to find: read HEAD, look up the branch name, that's the hash ID, and with the other commit we mention, which is also easy to find: see the name br2, look up the branch name, that gives hash ID L. So J and L are the two tip commits to merge.
But: in order to find work done, Git has a bit of a problem. Commits hold snapshots. To see changes, Git has to compare two snapshots. What snapshots can Git compare? Some people think Git will compare J and L directly, but that doesn't work. Suppose some file in J is different from its counterpart in L. Did we change it, or did they change it? Or maybe we both changed it. We're back to that question of what the heck does it mean to change a file when all the commits hold snapshots.
But suppose we work backwards from J. Moving back one hop we find commit I, which isn't on branch br2, but moving back two hops we find commit H, which is on br2. Likewise, moving back two hops from L, we land on H, which is on br1. So commit H is on both branches. That makes it a candidate. Git uses a particular algorithm here to find the best merge base commit,4 and in this case, always finds H.
Git then runs two diff operations, one from H to J to find out what we changed, and one from H to L to find out what they changed. The two diffs tell Git which lines of which files we changed, and which lines of which files they changed. By adding these two sets of changes together, Git may—and often is—able to combine the changes. Git then applies the combined changes to the files in the merge base. Depending on how you like to look at it, this keeps our changes while adding theirs, or keeps their changes while adding ours.
If Git is able to do this combining all on its own, git merge will make a new commit as usual, with one special feature. The new commit will have a snapshot as usual. It will have metadata as usual as well, listing us as the author and committer and "now" as the date-and-time-stamps. The merge commit can have a log message where we explain why we made the merge, and the merge commit has a list of previous commit hash IDs. Once the new merge commit is written out, Git updates the current branch name as usual, too.
The only thing special about this new merge commit is that its list of previous commits doesn't just list the current commit J. It also lists the commit we said to merge, i.e., commit L. So the updated graph looks like this:
I--J
/ \
...--G--H M <-- br1 (HEAD)
\ /
K--L <-- br2
That's the basics of git merge. (All the complications come in with things like merge conflicts, and options that tell Git not to actually merge after all, or force Git to do a real merge even if Git would default to not bothering to merge. We won't cover any of this here.)
4The algorithm in question is the Lowest Common Ancestor one, modified for directed graphs. There may be multiple LCAs in some complex graphs; this answer does not cover this case.
Your case gives Git heartburn because of disjoint subgraphs
In your case, you have, in your own repository, one or more commits—I'll just draw one—and a branch name pointing to the last of these:
A <-- main (HEAD)
plus one more commit obtained from the Bitbucket Git, with a remote-tracking name (origin/main or origin/master; I'm using main here):
B <-- origin/main
You're now asking Git to combine these two root commits:
A <-- main (HEAD)
B <-- origin/main
Git works backwards from A and ... oops, there's nothing there. Git works backwards from B and, again, oops! There is no best common ancestor.
The output from git merge, at this point, is:
fatal: refusing to merge unrelated histories
because there's no common ancestor.
Git used to just fake one up (in Git versions before 2.9).5 If Git simply pretends that there's a common pre-root commit ε and makes things look like this:
A <-- main (HEAD)
/
ε
\
B <-- origin/main
then suddenly the general merge algorithm works. But what files are in this fake epsilon-commit ε? The answer is: none at all. That means that each file in A is completely new, and each file in B is also completely new. You'll often get an "add/add conflict" from this kind of merge and have to resolve the conflict manually.
To tell Git that you'd like to do that, use the --allow-unrelated-histories option to git merge. This emulates the pre-2.9 behavior. Once you resolve any conflicts, if needed, and finish the merge, you end up with a graph like this:
A--C <-- main (HEAD)
/
B <-- origin/main
which has two root commits. Commits A and B remain unrelated, but commit C has two parents, A and B, and hence you can git push commit C to a repository that has its own main pointing to commit B.
There's nothing good about having two root commits in a repository. It's not wrong, but it may confuse those who are not up on their graph theory. It's probably best to avoid this as long as it's easy, and give the description of your initial setup, it is easy.
5My guess here is that Eclipse is doing this too, so that you didn't get an "unrelated histories" error in Eclipse, but instead got some merge conflict(s). You might also not have made your own root commit yet, at that point.
轉載請註明出處,本文鏈接:https://www.uj5u.com/gongcheng/435164.html
