我对 git 比较陌生,所以细节将不胜感激。请指出我是否做错了什么。
所以这就是问题的样子:
- 我从远程下载,它只有一个分支,主。
- 我承诺给师父
- 我将母版重新定位到新获取的源/主站后将更改推送到远程(尽管更改仍未合并到远程)
- 我从 master 创建了一个名为 bug1 的新分支并进行了一些更改
- 我在获取后将 bug1 重新定位为源/主(此更改尚未合并到远程)
- 我从 master 制作了一个分支,叫做 bug2。 我
- 修改了我最近在 master 上的提交(包含对 bug2 有用的所需信息)
- 我检查了 bug2,并做了
git rebase master
,希望在 master 中所做的所有更改现在都会反映在 bug2 中。 - 我相信,从观察中,我的目标确实实现了,关于 master 的工作现在在 bug2 中。
- 奇怪的是,当我做 git 状态时,我得到这个:
rebase in progress; onto ec578ba
You are currently rebasing branch 'bug2' on 'ec578ba'.
(fix conflicts and then run "git rebase --continue")
(use "git rebase --skip" to skip this patch)
(use "git rebase --abort" to check out the original branch)
Unmerged paths:
(use "git reset HEAD <file>..." to unstage)
(use "git add <file>..." to mark resolution)
both modified: c.py
both modified: f.py
both modified: s.py
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: t.py
modified: co.txt
modified: h.py
Untracked files:
(use "git add <file>..." to include in what will be committed)
blablaa.pyc
no changes added to commit (use "git add" and/or "git commit -a")
此外,git 分支 -a 显示:
* (no branch, rebasing bug2)
bug1
bug2
master
remotes/origin/HEAD -> origin/master
remotes/origin/master
我真的不知道我做错了什么...我以为这一切都会从我在网上学到的东西中解决,但显然我需要做一些"合并"来解决这个问题......
有人可以帮忙吗? 非常感谢!这似乎是一个太具体的问题,谷歌,我什至不知道我认为正确的术语。
正如git status
所说,你真的处于变基的中间。 (如果你的 Git 版本是 2.0 或更高版本git status
非常好且可靠;1.7 和 1.8 版本中有一些重大改进,所以如果你有一个真正古老的git status
,你仍然应该使用它,但它不如现代 Git。
您陷入合并的原因是git commit --amend
是一种善意的谎言:它实际上并没有改变现有的提交。 相反,它进行了一个新的和改进的提交,然后说:让我们使用这个而不是旧的。不幸的是,任何已经拥有并依赖于旧分支的分支仍然具有旧分支。
让我们详细浏览一下这些:
- 我从远程下载,它只有一个分支,主。
我将假设你的意思是你跑了:
git clone <url>
cd <new-clone>
这里。
- >我承诺给主人
因此:
<edit some file>
git add <that file>
git commit
现在让我们在这里退后一点,并描述提交是什么和做什么,并快速深入了解它是如何做到的。 (这里会有很多,但你可以记住有很多,稍后在适当的时候再回来。
提交和分支名称
您所做的每次提交都有所有文件的完整快照。 这些文件将永远冻结:它们无法更改。 提交还包含一些元数据,例如您的姓名和电子邮件地址、提交时的日期和时间戳以及提交原因- 您的日志消息。 Git 几乎不使用日志消息,所以你可以输入任何你喜欢的东西,但日志消息的目的是提醒你自己(或其他人)不是你做了什么——Git 可以自己向你展示——而是你为什么这样做,即只是更改本身无法显示的东西。
每个提交都有自己唯一的哈希 ID。 哈希 ID 是一个像83232e38648b51abbcbdb56c94632b6906cc85a6
一样丑陋的大字符串;这些出现在很多地方,包括git log
输出,有时以缩写形式(83232e3
等)。 哈希 ID 是提交的真实"真实名称",也是 Git找到提交的方式:Git 需要在其小爪子中具有哈希 ID,以便提取提交。 因此,如果不是分支名称,你会一直把这些东西记在纸上,或者其他什么——但这很愚蠢,我们有一台电脑,我们可以让它把它们记在一个文件中,或者其他什么。
这就是分支名称的含义:它是一个文件,或文件中的一行,或者记住哈希 ID 的东西。 事实上,它只记住一个哈希 ID:分支中最后一个提交的哈希 ID。 但这没关系,因为每次提交还会记住一个哈希 ID。 具体来说,每个提交都会记住它之前的提交的哈希 ID,Git 称之为父级。 所以我们最终得到:
... <-F <-G <-H <--branch
大写字母代表真正的哈希 ID。名称branch
保存上次提交H
的哈希 ID。H
本身保存早期提交G
的哈希ID,该F
的哈希ID保存,依此类推。 这允许 Git 向后浏览一系列提交。
要创建一个新分支,您只需选择一些现有的提交并为其附加一个名称:
...--F--G <-- newbranch
H <-- master
或:
...--F--G--H <-- newbranch, master
现在你有多个名称,Git 需要一种方法来知道你正在使用哪个名称。 这就是HEAD
的用武之地:
...--F--G--H <-- newbranch (HEAD), master
表示当前名称为newbranch
(当前提交为H
)。
请记住,我们注意到提交中的文件将始终被冻结。 它们都是只读的。 它们也被压缩,以便占用更少的空间。 我喜欢称这些为冻干。 像这样被冻结,如果你有很多提交不断重用大多数相同的文件,Git 可以而且实际上只是重用已经冻结的文件。 这是 Git 用来防止存储库快速增长的几个技巧之一,即使每次提交都会保存每个文件。 但是,虽然这对存档很有帮助,但对于完成任何新工作显然毫无用处。 您需要可以读写的文件。 因此,git checkout
提取一个提交,该提交将成为您当前的提交,并将其所有冻干文件重新冻结到您的工作树中。 在这里,您的文件具有普通的日常格式。 您可以使用它们并使用它们。
Git 可以停在这里——冻结的提交文件,一组特别有趣的冻结文件是当前提交中的文件,以及工作树中的可用集——其他版本控制系统确实停在这里。 但是由于各种原因,Git 隐藏了文件的第三个副本,介于冻结集和可用集之间。 这些副本位于 Git 的索引或暂存区域中- 这是同一事物的两个名称。 索引中的内容采用冻干格式。 但与实际提交不同的是,这些文件可以随时替换,或者添加新文件或删除文件。
每当你进行新的提交时,Git 真正做的是将预冻干的索引副本打包到一个提交中。 这运行得非常快,并且根本不需要使用工作树中的内容! 所以你首先必须让 Git 更新它的索引,这就是git add
的意义所在。git add
命令冻结干燥工作树文件并覆盖索引中的副本,或者如果该文件以前不在索引中,则在那里创建它。
打包文件并添加您的名称和日志消息等后,Git 将新提交的父级设置为当前提交。 然后,它将新提交写入所有提交数据库:
...--F--G--H <-- newbranch (HEAD), master
I
现在提交已经存在,git commit
的最后一步是使名称(在本例中为newbranch
)指向提交I
而不是提交H
:
...--F--G--H <-- master
I <-- newbranch (HEAD)
虽然提交是快照,但 Git 可以显示更改
如果你要求 Git 向你展示提交I
,或者为此提交H
,Git可以向你显示其中的所有文件。 但这通常不是我们想知道的。 对于提交H
,我们想知道:H
和G
有什么不同?所以当git log -p
或git show
显示提交H
时,它实际上同时查看G
和H
。 无论文件有何不同,Git 都会比较这两个文件并告诉您更改了哪些内容。 在查看提交I
时,Git 会比较H
和I
中的快照,并告诉您更改了哪些内容。
这是一个一般主题:对于任何只有一个父级的提交,Git 可以轻松地提取父级和提交,并比较两者。 这会将提交转换为一组更改。 请务必记住,这需要选择以前的提交。 然后,Git 将比较两个快照。 只是,对于大多数提交来说,选择是如此明显,以至于我们根本不需要命名它。
返回您的脚步
因此,这让我们回到第 2 步:您向master
添加了一个新的提交。 让我们这样画
I <-- master (HEAD)
/
...--F--G--H <-- origin/master
(origin/master
是你的 Git 记住master
在你之前克隆的 Git 存储库中的位置或曾经的位置的方法)。
- 我在将母版重新定位到新获取的源/主站后将更改推送(尽管更改仍未合并到远程)
也就是说,您运行了:
git fetch origin # or just `git fetch`, which does the same thing
git rebase origin/master # or `git pull --rebase origin master`
git push origin master
假设没有人在origin
时向Git仓库添加任何新的提交,变基步骤什么也没做。 不过,最后一步,git push origin master
,做了一些重要的事情:
- 你的 Git 在
origin
调用 Git - 您的 Git 会找出他们拥有哪些提交(通过哈希 ID)。
- 您的 Git 提供了您没有的任何新提交,即提交
I
,也通过哈希 ID。 - 您的 Git 以向他们发出请求来结束
git push
会话:嘿,其他 Git,请将您的master
设置为指向提交I
。
如果他们服从这个请求——这里没有列出他们不应该这样做的理由——他们最终会得到:
...--F--G--H--I <-- master
在他们的存储库中。 您的 Git 将看到他们接受了请求,并将更新您的origin/master
以匹配,因此我们可以理顺存储库内容绘制中的扭结并将它们列出为:
...--F--G--H--I <-- master (HEAD), origin/master
您添加了:
(不过,更改仍未合并到远程中)
但这无关紧要:它既不是真的也不是假的,因为没有什么可以合并的;但他们确实有你的新提交I
和他们的master
指向I
。
我从 master 创建了一个名为 bug1 的新分支并进行了一些更改
所以你跑了,例如:
git checkout -b bug1
它添加了一个新的分支标签bug1
指向提交I
并使该HEAD
:
...--F--G--H--I <-- bug1 (HEAD), master, origin/master
然后你修改了一些文件,运行git add
,并运行git commit
来创建一个新的提交J
:
...--F--G--H--I <-- master, origin/master
J <-- bug1 (HEAD)
- 我在获取后将 bug1 重新定位为源/主(此更改尚未合并到远程)
同样,"尚未合并"几乎毫无意义:您的 Git 调用了他们的 Git,向他们提供了您的提交J
,并要求他们创建他们的名字bug1
指向提交J
。 在任何git push
期间都没有合并的问题:这只是他们是否接受您更改或创建某些分支名称的请求的问题。 所以他们现在只有两个分支名称,master
指向I
,bug1
指向J
。 然后你自己的 Git 对它说:啊,他们接受了创建bug1
的请求,所以我现在会记得他们有一个指向提交J
bug1
。这意味着您自己的存储库现在可以绘制为:
...--F--G--H--I <-- master, origin/master
J <-- bug1 (HEAD), origin/bug1
- 我从master做了一个分支,叫做bug2。
那是:
git checkout -b bug2 master
或同等学历。 这会选择提交I
作为当前提交,并为其创建一个新名称,将I
的内容提取到索引和工作树中,并将HEAD
附加到新名称:
...--F--G--H--I <-- bug2 (HEAD), master, origin/master
J <-- bug1, origin/bug1
(如果你使用git branch
,那不会调整HEAD
,你可能仍然在提交J
,但除此之外,它的工作方式几乎相同。
我
- 修改了我最近在 master 上的提交(包含对 bug2 有用的所需信息)
所以:
git checkout master
此步骤选择提交I
作为当前提交,并将HEAD
附加到名称master
。 如有必要,您的索引和工作树会进行调整以匹配I
,但如果您已经在I
则这里没有工作;最终结果是:
...--F--G--H--I <-- bug2, master (HEAD), origin/master
J <-- bug1, origin/bug1
所以在这里,唯一真正的变化是更改HEAD
附加到的名称,除非您在提交J
,在这种情况下它也签出了提交I
。 然后你做到了:
<make some changes and run git add>
git commit --amend
--amend
选项实际上根本不会更改提交I
。 相反,它会进行新的、不同的提交。 我们可以称它为I'
(以反映它是I
的副本),或者只是给它一个全新的字母K
。 我会在这里做后者。git commit --amend
的棘手之处在于,新的提交不是指向I
,而是指向I
的父H
:
K <-- master (HEAD)
/
...--F--G--H--I <-- bug2, origin/master
J <-- bug1, origin/bug1
请注意,提交I
仍然存在,并且仍然通过bug2
和origin/master
被记住。origin
的 Git 存储库仍然有提交I
;它仍然记得通过其master
提交I
。 你自己的bug1
会记住提交J
,它记得提交I
,并且 Git 在源上也有J
(它必然记住I
- 提交在每个拥有它们的 Git 存储库中都是相同的)。
您可以在本地通过查看您自己的所有分支名称(bug1
、bug2
和master
)并向后移动(一次一个提交)来跟踪谁拥有什么,以查看您可以到达哪些提交。 对远程跟踪名称执行相同的操作,origin/bug1
和origin/master
以查看他们可以访问哪些提交。1
现在我们进入有问题的步骤:
- 我检查了 bug2,并做了
git rebase master
,希望在 master 中所做的所有更改现在都会反映在 bug2 中。
现在我们必须看看 rebase 做了什么——但简而言之,它复制了提交,就像git cherry-pick
一样。 它复制的提交基于您为其提供的参数。
1至少,这是他们尽你自己的 Git 所知所拥有的。 它没有在超过三秒钟内调用另一个 Git,谁知道它在那段时间里可能发生了多大的变化! 这是了解谁在更改上游 Git 存储库可能很重要的地方。 它更新的速度有多快?
哪个提交变基副本
让我们回到图表,针对git checkout bug2
进行调整:
K <-- master
/
...--F--G--H--I <-- bug2 (HEAD), origin/master
J <-- bug1, origin/bug1
和命令:
git rebase master
在这里,您使用了最简单的git rebase
形式。 您可以使用git rebase --onto
将变基目标(或多或少地放置副本的位置)与限制器(即不复制的内容)分开。 使用最简单的形式,两者是一回事:master
既是在哪里放置副本,又是不可以复制什么。
Git 现在将从bug2
开始,向后浏览图形,列出提交。 这些是I
,然后是H
,然后是G
,等等。 它还将从master
开始并向后移动,列出提交:K
,然后是H
,然后是G
,依此类推。第二个列表中的任何内容都将被排除在外。阿拉伯数字这样就留下了提交I
.
生成的列表是要复制的提交列表(顺序错误,因此 Git 将反转列表,但这里只有一个条目)。 Rebase 现在将尝试复制提交I
,以便在提交K
之后进行。 复制的确切机制有点复杂——有些git rebase
操作实际上git cherry-pick
运行,有些则不然——但它意味着无论哪种方式都有相同的结果,并且像git cherry-pick
一样工作。 这实质上意味着将某人在H
-vs-I
路径中所做的更改与某人在H
-vs-K
路径中所做的更改合并。
当然,那个"某人"就是你。 这些变化相互冲突。 Git 无法自行解决冲突。 如果 Git 能够自行解决冲突,Git 会尝试进行新的提交——I
的副本,除了新提交的父级将被K
——然后它会完成并将名称设置为bug
指向新提交。 由于它是I
的副本(但实际上并没有发生),因此让我们将其称为I'
,以显示如果它确实发生,事情会是什么样子:
I' <-- bug2 (HEAD)
/
K <-- master
/
...--F--G--H--I <-- origin/master
J <-- bug1, origin/bug1
您可以通过从提交K
中选择所有文件来解决冲突,即删除H
-vs-I
中尚未在H
-vs-K
中的所有更改,并仅保留K
中的内容。 但是,打扰新提交根本没有真正的意义:只需将名称bug2
指向直接提交K
,如下所示:
K <-- bug2 (HEAD), master
/
...--F--G--H--I <-- origin/master
J <-- bug1, origin/bug1
你可以用git rebase
来做到这一点,你只需要更长的形式:
git rebase --onto master <anything that specifies commit I>
--onto master
告诉 Git:将副本放在master
标识的提交之后,即在K
之后,其余的告诉 Git:不要复制提交I
,也不要复制任何更早的内容。 所以这将完成这项工作:它不会复制任何提交,然后将名称bug2
移动。
在这种情况下,只需将名称移动到自己bug2
就更简单了。 这特别容易,因为你从来没有用git push
告诉任何其他Git 让这个名字bug2
记住任何东西。 所以不需要告诉其他人:哦,嘿,我让你做的bug2
? 算了,改用这个。
但是,您确实有问题master
和bug1
. 你已经告诉其他一些 Git(origin
的那个)记住提交I
是他们的master
,并将提交J
作为他们的bug1
。 他们大概还记得那些。 你可以说其他 Git:这是提交K
。 而且,哦,嘿,你记得的那个承诺I
是你的master
......记住新的提交K
作为您的master
。这需要力推、git push -f
或git push --force-with-lease
。 你还必须对自己的bug1
做一些事情,比如使用git rebase --onto
将提交J
复制到K
之后的新改进提交,然后在origin
调用Git,向他们发送新的和改进的提交,并强制他们移动他们的bug1
。
现在该怎么办
不过,首先,你需要摆脱变基地狱。 最简单的方法是使用:
git rebase --abort
这会让一切恢复到您开始之前的状态。 也就是说,您现在返回到:
K <-- master
/
...--F--G--H--I <-- bug2 (HEAD), origin/master
J <-- bug1, origin/bug1
现在,您可以做许多事情中的一件(或多件)。 以下是两个简单的选项:
git checkout -B bug2 master
或git checkout master; git branch -D bug2; git checkout -b bug2
这些结果是:
K <-- bug2 (HEAD), master
/
...--F--G--H--I <-- origin/master
J <-- bug1, origin/bug1
您现在可以继续处理bug2
。
在某个时间点(现在或以后的任何时间),您可以运行:
git push --force-with-lease origin master
让你的 Git 调用他们的 Git,向他们提供你的新提交K
,并告诉他们:我认为你的master
点提交I
。 如果是这样,请改为提交K
。阿拉伯数字这与--force
/-f
非常相似,只是普通的强制只是向他们发送一个命令:设置你的master
!如果其他人(可能不是你)在I
之后添加了更多的提交,你会导致 Git 在origin
丢失这些提交。 在这种情况下,您希望他们的master
"丢失"提交I
:它已被新的和改进的K
所取代。
然后,最终,您可能还需要:
git checkout bug1
git rebase --onto master bug1~1
这个特殊的语法——bug1~1
——告诉 Git:从bug1
开始,通过一次提交向后工作。也就是说,它找到bug1
(提交J
),然后后退一步提交I
。 这些是不复制的提交 -I
和更早的任何内容 - 所以git rebase
现在将通过I
与J
进行比较来复制J
,并在master
的尖端提交之后添加这些更改。 如果您已经进行了一些bug2
提交,但没有新的master
提交,并强制origin/master
记住K
,那么现在可能如下所示:
L--M <-- bug2
/
K <-- master, origin/master
/
...--F--G--H J' <-- bug1 (HEAD)
I
J <-- origin/bug1
现在你已经用新的和改进的J'
替换了J
,你可以在origin
告诉 Git,忘记所有关于I
和J
的事情是可以的:他们应该将bug1
设置为记住J'
:
git push --force-with-lease origin bug1
或者当然是更简单的git push -f origin bug1
,它只是假设他们的bug1
指向J
,让他们忘记I-J
部分,只记住J'
。
2--force-with-lease
选项必须告诉另一个 Git:我认为你的名字<名称>指向提交origin/master
,记住您上次运行 Gitgit fetch
或成功git push origin master
时您的 Gitmaster
在哪里。 所以如果要更新的名称是master
,旧的哈希来自你自己的origin/master
;如果要更新的名称是bug1
,则旧哈希来自您自己的origin/bug1
。