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