问题描述
我有一个项目,我觉得我更愿意将所有漫长而丑陋的 GIT 历史抛在脑后,重新开始。
(一个原因是合并会在数百个文件中产生无数冲突,然后解决它太乏味和令人厌烦了。我还没有发现如何说所有我的工作'new_features'分支合并到旧的主分支)。
我是否应该在本地和 GitHub 上启动一个新的 GIT 存储库并将工作版本文件添加到新的存储库中?
或者我应该尝试使用 git rebase 吗?
解决方法
我认为你需要决定一个特定的提交是否因为一个问题而不会被重新访问,例如,这种情况发生的可能性非常高,在未来,进行更改会非常困难,如果你没有提交。
您可以将 git clone
与 --no-single-branch
参数一起使用,这将获取所有分支尖端附近的历史记录。另一个参数--shallow-since=<date>
在指定时间后创建一个带有历史记录的浅克隆。这将保留一些最近的提交,如果您觉得您或您的团队可能很快会重新访问这些提交,但保留提交的时间不会超过指定的日期。
Git 文档中 git clone
的更多参数信息,
https://git-scm.com/docs/git-clone
这个问题有两个部分:
- 我应该开始一段新的历史吗? (或者如果没有,我可以做一些花哨的 rebase 工作或其他什么)
- 如何开始新的历史记录?
问题 1 的答案是基于意见的,这使得它大多偏离主题(但请参阅下面的侧边栏)。问题 2 的答案是技术性的,所以这里是:
方法A:一个全新的仓库
历史是提交;提交是历史;这就是全部。 (嗯,带注释的标签添加了一些提交之外的信息。是否要考虑注释“历史”取决于您。)
要开始新的历史记录,您只需创建一个空的存储库:
mkdir new-empty-repo
cd new-empty-repo
git init
用文件和 git add
填充工作树,然后 git commit
进行第一次提交。 Presto,你有一个提交作为你的(整个)历史。创建第一个提交也会创建(单个)分支。
在至少有一次提交之前,不能存在任何分支名称。之后,无限数量的分支名称允许,尽管所有分支名称都必须选择单个提交,因为仅此而已。创建第二次提交后,您仍然可以拥有无限多个分支名称,现在每个分支名称都可以选择第一次提交或第二次提交。
如果您的 Git 足够现代,您可以告诉它在 git init
时间创建哪个分支名称。否则,例如在 git init
之后,运行 git checkout -b main
将要创建的分支名称切换为 main
。完成第一次提交后,创建一个新的分支名称会将新的分支名称链接到(单个)提交:
A <-- main
进行新提交将更新您现在“打开”的任何分支名称,以便它指向 new 提交而不是它以前指向的任何提交:
A <-B <-- main
随着提交越来越多,每个提交都指向前一个提交:
A <-B <-C ... <-G <-H <-- main
(一旦你总共有 8 个提交)。在这里,大写字母代表真实提交的实际哈希 ID。实际的哈希 ID 太大太丑,人类无法处理(尽管计算机很擅长),所以在我为 StackOverflow 绘制的图纸中,我只使用这样的大写字母。
方法 B:一个“孤立分支”
如果你愿意,你可以保留所有现有的分支,但创建一个新的分支,作为它的第一个也是唯一的提交,一个没有历史的提交。也就是说,为了让 next 提交,我们设置的东西就像你刚刚创建了一个新的、完全空的存储库一样。
棘手的部分是旧的 git checkout
命令有轻微的缺陷;这已在新奇的 git switch
中得到纠正。至少,如果您同意 Git 人员的意见,它是有缺陷的/更正的。
这里的想法很简单。请记住,每个提交,除了第一次提交,记录一个上一个提交。毕竟,这就是历史在 Git 中的工作方式:一个新的提交链接回您在创建时使用的提交。但是当您在一个新的完全空的存储库中进行第一次提交时,没有现有的提交作为起点。所以没有什么可以链接回的。我们就是这样得到的:
A <-- main
above: A
是 first 提交,因此它不会连接到之前的提交。这使得提交 A
成为 root 提交。
但是:如果我们已经有一些提交,在(比如说)master
上,像这样:
... <-G <-H <-- master (HEAD)
我在绘图中添加了 HEAD
以记住我们使用的分支名称。
通常,如果我们创建一个 new 分支,例如 git checkout -b main
,我们会得到:
... <-G <-H <-- main (HEAD),master
两个名称都指向现有提交 H
。这样,当我们进行新的提交 I
时,名称 main
将更新为指向 I
,而 I
将指向回 H
。>
为了回到我们在没有提交时恢复的那种“原始存储库”状态,我们需要“创建”新的分支名称而不实际创建 strong> 新的分支名称。也就是说,Git 必须说我们是 on branch main
,但还没有有一个分支名称 main
。在我使用的符号中,没有正确的方法来绘制它,但是:
git checkout --orphan main
或:
git switch --orphan main
会解决问题。稍后我们将回到 git checkout
的“缺陷”。
此时,Git 已将 HEAD
附加到一个不存在的分支名称。运行 git status
会说 on branch main
,但 git branch
不会 list main
作为分支名称,也不会 listed 姓名将被加星标。换句话说,您处于一个不存在的分支上——与您创建一个完全空的存储库时所处的情况相同。
此时,创建一个 new 提交,好吧,创建一个新提交。这创建了分支,就像使用空存储库一样。但是由于该分支在此之前不存在,因此新提交不会链接回任何现有提交。所以:
... do some work ...
git commit
结果:
A--B--C--D--E--F--G--H <-- master
I <-- main (HEAD)
您有一个与现有历史无关的新历史。这个存储库现在有两个分支;一个有 8 个提交,一个有 1 个提交。
您现在可以对各种提交执行任何您喜欢的操作,包括添加更多提交。您也可以创建链接回提交 I
的新分支或链接回提交 I
的任何提交。这只是一个新的开始,在同一个旧存储库中。
git checkout --orphan
vs git switch --orphan
是时候谈谈 Git 的索引了。任何合适的 Git 教程都需要讨论 Git 的索引,因为这是 Git 进行新提交的方式。
提交,在 Git 中,是一个由两部分组成的实体:
-
每次提交都保存每个文件的完整快照。这些是压缩的、冻结的、Git 化的,重要的是,去重,因为大多数提交大多只是重用其他一些提交的文件。换句话说,它们被归档。您实际上不能使用这些文件:在它们变得有用之前,它们必须被复制和扩展。
-
而且,每个提交都包含一些元数据:关于提交本身的信息,例如谁提交、何时提交以及为什么(他们的日志消息)。在元数据中,Git 保留了向后指向的历史链接。这东西也被冻结了。一旦将任何提交写入 Git 数据库-of-all-commits-and-supporting-objects,几乎不可能更改任何提交的任何部分。
不过,Git 不会从您看到和编辑的文件中制作这些提交。相反,Git 插入了这个额外的东西——Git 的index,Git 也称它为暂存区,或者有时是缓存——在之间>当前提交和你的文件。
索引中的内容(至少在最初)是提交中每个文件的 Git 化副本。但是这些文件副本在这个特定点上并不是完全只读的:如果你覆盖了其中一个文件,Git 会将其与共享取消链接,并且 Git 化新数据。现在索引中的内容是提议的下一次提交。
事实上,之前索引中的内容也是提议的下一次提交。只是它与实际、当前提交相匹配。在您更新 Git 的索引之前,没有必要重新提交。
更新 Git 索引的方式是使用工作树中的文件。这些是普通的、非 Git 化的文件,每个程序实际上都可以使用。一旦你更改一个,或者添加一个新的,或者删除一个,现在你需要告诉 Git:嘿,现在更新提议的提交,我进行了一些更改!您通常使用 git add
或有时使用 git rm
来执行此操作。 git rm
命令可以方便地同时删除文件的工作树副本和暂存副本。
当您运行 git commit
时,Git:
- 立即打包 Git 索引中的任何内容;
- 添加适当的元数据,包括使用当前提交的哈希 ID 作为新提交的父项;
- 将所有内容作为提交写出,并一直冻结;和
- 将新提交的哈希 ID 写入当前分支名称。
当你处于那个特殊的“原始”状态时——在一个不存在的分支上——新提交的元数据不列出父级,使新提交成为新提交根提交。哈希 ID 的写入创建分支名称。
现在,git checkout --orphan
和git switch --orphan
都为分支设置了这个“原始”状态。这两个命令之间的区别在于它们对 Git 的 index 和您的工作树的作用。
git checkout --orphan
命令对 Git 的索引和您的工作树没有任何作用。因此,如果您的索引和工作树包含各种跟踪文件——一个被跟踪的文件只是 Git 索引中的一个——那么在 git checkout --orphan
之后,您的索引和工作树仍然 em> 满是跟踪文件。
您可以使用它来获得良好的效果:现在进行新的提交会生成与您之前签出的提交匹配的提交,当然假设您从那时起没有进行任何更改并且 git add
更改。
然而,Git 人员认为这是 Git 的错误行为方式。因此,如果您使用 git switch --orphan
进行设置,这个 命令将通过删除其中的每个文件来清空 Git 的索引。同时,Git 将删除文件的工作树副本。 (在 Git 执行任何一项之前,它会确保索引和工作树副本与提交的副本匹配,当然。否则这可能会破坏未保存的工作。)
因此,如果您想要之前签出的提交中的文件,并且使用 git switch --orphan
,则需要使用 git restore
取回文件进入 Git 的索引和/或您的工作树。但是,如果您使用 git checkout --orphan
,它们只会在那里,如果您不想要这些文件,则需要使用 git rm
清除它们。
这就是 git checkout --orphan
和 git switch --orphan
之间的巨大区别:switch
变体会删除您的索引和所有相应的工作树文件。 checkout
变体没有。
侧边栏:“凌乱”与“干净”的历史
您确实提到了为什么要开始新的历史记录:
合并会在数百个文件中产生无数冲突,然后解决起来太乏味和烦人了
这类问题应该只发生一次(也许每个分支,也可能取决于你如何解决问题)。
真实的历史常常凌乱,有时非常凌乱。一些 Git 用户投入了大量的工作来生成不那么混乱的历史记录。这有一些优点:例如,阅读和思考清理过的历史记录要容易得多,而且通常,某人犯了一堆小错误这一事实完全无关紧要。但有时记录错误很重要,要么是因为它已经暴露并可能需要处理,要么因为它很容易再次,我们应该牢记和 (a)避免再次出现和/或 (b) 展示如何修复它。
因此,保留真实历史肯定有一些优势。这也意味着花在“干净的历史”上的时间更少,留下更多的时间来完成……呃,东西,完成。最后,每个人都需要自己评估这种权衡。