我有main
上的代码myscript.py
,我正在尝试将其移动到现有的分支branchA
。
% git branch
branchA
* main
myscript.py
main
,如何将其移动到branchA
,使其不再出现在main
上?
代码(或者更准确地说,文件)驻留在提交中。
分支(或更准确地说,分支名称)选择提交。 更准确地说,分支名称保存一个特定提交的原始哈希 ID。 分支中的所有其他提交由于该特定提交而位于分支中。
任何提交一旦完成,就无法更改。 但是,存储在分支名称中的原始哈希 ID始终可以更改。 事实上,这就是分支名称的全部意义:存储不断变化的哈希 ID。
当我们进行新的提交时,Git :
- 打包新的源快照;
- 添加一些元数据,或有关我们现在进行的新提交的信息;
- 将所有这些写成一个提交,获得一个新的、唯一的哈希ID;和
- 将新提交的哈希 ID写入分支名称。
因为 Git 在第二步中添加的一段元数据是当前提交的哈希 ID,存储在我们运行git commit
时的分支名称中,这使得新的提交链接返回到曾经是当前提交的内容。 如果我们从带有哈希 ID 的提交链开始,其中我们用单个大写字母表示原始哈希 ID 以保持我们的个人理智,我们可能会有这样的东西:
... <-F <-G <-H
在这里H
代表当前(到目前为止,最后一个!)提交的哈希ID。 分支名称main
存储此原始哈希 ID,因此名称main
指向提交H
:
... <-F <-G <-H <-- main
同时存储在提交H
中的元数据包含早期提交G
的原始哈希ID,所以我们说H
指向G
。 那是从H
里出来的箭。 当然,提交G
是一个提交,所以它有一个存储的哈希ID,在这种情况下指向更早的提交F
,依此类推。
现在,再一次,任何现有的提交都无法更改。 我们对 Git 的正常用途是添加新的提交,如下所示:
...--G--H <-- main, somebranch
请注意,我们有两个名称——main
和somebranch
——*都指向提交H
。
我们运行git checkout main
或git switch main
,编辑代码,运行git add
,并运行git commit
。 Git 打包一个新的快照并进行一个新的提交,我们称之为I
. 因为我们已经签出main
,所以 Git 将新的哈希 ID 写入名称main
. 为了记住我们已经签出了哪一个,让我们将名称HEAD
附加到main
,并绘制新的提交:
...--G--H <-- somebranch
I <-- main (HEAD)
注意main
是如何移动的,而somebranch
没有。 如果我们现在git checkout somebranch
或git switch somebranch
,我们得到:
...--G--H <-- somebranch (HEAD)
I <-- main
来自提交的文件I
消失,取而代之的是,我们有来自提交H
的文件。 Git 删除了那些与提交I
一起使用的那些——它们安全地存储在I
快照中——并用H
中的文件替换了它们。
现在,我们可以解决一种回答您问题的方法:
myscript.py
在main
上,如何将其移动到branchA
使其不再出现在main
上?
我们应该画出你所拥有的。 我不确定你的branchA
选择什么提交,也不确定你的main
选择什么提交,所以我不得不猜测:你自己的绘图会更好,或者你可以运行git log --all --decorate --oneline --graph
或任何其他花哨的命令从 Pretty Git 分支图。 但是,假设我们有:
G--H <-- branchA
/
...--F
I <-- main (HEAD)
让我们进一步假设所有文件都已提交(因为如果没有,您有更多选择)。
您可以简单地运行:
git rm myscript.py
git commit
要在缺少该文件的main
上进行新提交,请执行以下操作:
G--H <-- branchA
/
...--F
I--J <-- main (HEAD)
提交J
不再包含该文件。 现在,您可以git switch branchA
切换到现有的分支branchA
并提交H
:
G--H <-- branchA (HEAD)
/
...--F
I--J <-- main
您现在可以看到提交H
中的所有文件,这当然意味着您看不到myscript.py
。 但是我们知道它永久保存在提交I
中,所以我们需要告诉 Git:从这个现有提交中获取这个保存的文件。
有多个命令可以执行此操作;我通常推荐的是git restore
,具有--source
和-SW
选项:
git restore --source main~1 -SW -- myscript.py
这有点啰嗦,并且与旧命令执行相同的操作:
git checkout main~1 -- myscript.py
也就是说,它使用带有后缀~1
的名称main
首先查找提交J
(main
),然后后退一次(~1
)以提交I
。 然后,它会在该提交中找到名为myscript.py
的文件,并将该文件复制到两个位置:
-W
将文件复制到工作树中,您可以在其中查看和编辑它。-S
将文件复制到 Git 的暂存区域,现在可以提交该文件了。
git checkout
命令没有-S
和-W
标志:它总是复制到两个位置。
现在您已经有了这个文件并且它已经git add
-ed,您只需运行git commit
进行新的提交,该提交将更新当前 (HEAD
) 分支名称:
G--H--K <-- branchA (HEAD)
/
...--F
I--J <-- main
提交K
与现有的提交H
完全相同,只是它添加了这个新文件。
请注意,这些分支中的这些提交是历史记录。 通过提交向上提交F
位于两个分支中。 Git 找到这些提交的方法是使用分支名称查找最后一个提交,然后向后工作。
还有更多选择
如果您尚未提交该文件,则它当前仅在您的工作树中,也可能在 Git 的暂存区域中。 请参阅约什梅兰达的答案,但请注意,git switch -c
尝试创建一个新分支;您需要git switch
,它使用现有分支。
在这里,我们可以指望这样一个事实,即由名称branchA
标识的提交中没有名为myscript.py
的文件。 这在实践中变得非常复杂,尽管在这种情况下很简单。 有关所有血腥的详细信息,请参阅当当前分支上有未提交的更改时签出另一个分支。
以上所有内容都是关于添加更多提交。 在某些情况下,我们有一些提交,出于某种原因我们不喜欢它们。 在这种情况下,我们可以在限制范围内告诉 Git停止使用这些提交。
考虑以下情况:
G--H <-- branchA
/
...--F
I <-- main (HEAD)
我们决定我们不喜欢提交I
. 我们所做的是最初不要管它,并使用它来将文件myscript.py
复制到我们添加到branchA
的新提交中:
G--H--J <-- branchA (HEAD)
/
...--F
I <-- main
使用相同的git restore
技术(尽管这次我们使用--source main
,而不是--source main~1
,因为我们没有在main
上进行新的提交)。
但是,然后,我们git switch
回到main
并运行:
git reset --hard HEAD~1
或:
git reset --hard main~1
~1
后缀的作用与以前相同:找到提交,然后后退一跳。 这将定位提交F
。 我们可以运行git log
并使用鼠标剪切并粘贴提交F
的原始哈希 ID:
git reset --hard <hash>
这里。 接下来,git reset
:
- 将分支名称移动到我们选择的提交;
- 重置 Git 的索引/暂存区;和
- 使您的工作树匹配(由于
--hard
)。
这给我们留下了:
G--H--J <-- branchA
/
...--F <-- main (HEAD)
I ???
请注意,如何不再有任何名称来查找提交I
。 如果你记住了它的哈希ID,或者把它写在纸上,或者其他东西,你仍然可以这样找到它。 Git 还提供了其他方法来恢复它,默认情况下至少 30 天。 但大多数情况下,它似乎已经消失了,好像它从未存在过。
所以现在看起来main
上的最后一次提交是提交F
,而不是提交I
。 只要没有其他人提交I
——你从未将其发送到其他 Git 存储库——"摆脱"这样的提交是安全的。 如果你确实把它发送到某个地方,它可能会从那里回来,因为 Git 真的很喜欢添加提交,并且会像病毒一样传播它们,只要有一半的机会。 因此,一旦你发出了提交,"倒带"或"删除"提交通常是不明智的(通常使用git push
)。
如果您尚未提交文件,只需移动新分支并继续:
git switch -c branchA
如果已提交文件,则可以在将其移动到新分支之前将其还原到某个提交:
git restore -s <commit> myscript.py
git switch -c branchA