简体   繁体   English

如何仅通过一个压缩的初始提交发布新的 git 分支?

[英]How to publish a new git branch with just one squashed initial commit?

So imagine a scenario where you've been working on just one branch (you are new to git), and you made some weird commits and thought the repo was just for you.所以想象一个场景,你只在一个分支上工作(你是 git 新手),你做了一些奇怪的提交,并认为 repo 只适合你。 Then the time comes when you actually want other people to see it, but you don't want the world to see your silly dev commits (also, there may have been.env files commited, gitignored too late, etc.).然后是时候你真的希望其他人看到它,但你不希望世界看到你愚蠢的开发提交(另外,可能已经提交了 .env 文件,gitignored 太晚了,等等)。 Instead of editing history, how about a shiny new squash merge and creating a brand new branch?与其编辑历史记录,不如使用 shiny 新的壁球合并并创建一个全新的分支? I can only do it by deleting the repo and initializing a new one (something I've been doing a lot out of frustration, maybe I just don't get it).我只能通过删除存储库并初始化一个新存储库来做到这一点(出于沮丧,我一直在做很多事情,也许我只是不明白)。 So I want it to seem like by the time the initial commit was created, I already had a working script.所以我希望它看起来像在创建初始提交时,我已经有了一个工作脚本。 How can I reproduce this while also keeping a dev branch?如何在保留开发分支的同时重现这一点? I don't have any other branch, and even the initial commit is crap.我没有任何其他分支,甚至最初的提交都是废话。 It's not GitHub, "everything is local."不是 GitHub,“一切都是本地的”。

I have GitKraken, but even with the UI, I have no luck.我有 GitKraken,但即使有 UI,我也没有运气。 I already renamed master to dev.我已经将master重命名为dev。 The result would be: lots of silly commits on dev that no one sees.结果将是:在 dev 上出现许多没人看到的愚蠢提交。 Squash all that and publish to a new branch with no history, and only that will get pushed to GH.压缩所有内容并发布到没有历史记录的新分支,只有那个会被推送到 GH。

There are two easy ways有两种简单的方法

Given your setup (one repository with many commits but just one branch name main or master or whatever), here are the two easy ways to get a single commit containing your desired new commit:给定您的设置(一个包含许多提交但只有一个分支名称mainmaster或其他名称的存储库),这里有两种简单的方法来获取包含所需新提交的单个提交:

  1. git branch save && git reset --soft $(git rev-list --max-parents=0 HEAD) && git commit --amend

  2. git checkout --orphan new && git commit

The difference between these two is that with method 1, the current branch name ( main or master or whatever) will now select your new root commit;这两者之间的区别在于,使用方法 1,当前分支名称mainmaster或其他)现在将 select 您的新根提交; with method 2, the new branch name ( new above) will select your new root commit.使用方法 2,的分支名称(上面的new )将 select 您的新根提交。

(If all you want are recipes, you can stop here, if you like. But do read on!) (如果你想要的只是食谱,你可以在这里停下来,如果你喜欢。但请继续阅读!)

Why this seems hard为什么这看起来很难

The very first commit ever , in any shiny new empty repository, is kind of special.有史以来第一次提交,在任何 shiny 新的空存储库中,有点特别。 To understand why—and hence what the above two magic commands actually do—we should start at the beginning:要理解为什么——以及上述两个魔法命令的实际作用——我们应该从头开始:

  • A Git repository is really all about the commits . Git 存储库实际上是关于提交的。 It's not about files, although each commit holds a full snapshot of every file, and it's not about branches (branch names), although we (and Git) use the branch names to help us find the commits.这与文件无关,尽管每个提交都包含每个文件的完整快照,也与分支(分支名称)无关,尽管我们(和 Git)使用分支名称来帮助我们找到提交。 It's really all about the commits.这真的都是关于提交的。

  • Each commit:每个提交:

    • Is numbered, with a big ugly random-looking hash ID (more formally object ID or OID).编号,带有一个大丑随机的hash ID (更正式的 object ID或 OID)。 Git stores the commits, along with other supporting objects, in a big key-value database where the keys are the hash IDs. Git 将提交以及其他支持对象存储在一个大键值数据库中,其中键是 hash ID。 This means Git needs the hash to retrieve the object (the commit).这意味着 Git需要hash 来检索 object(提交)。 The numbering scheme is deeply magic.编号方案非常神奇。

    • Is read-only.是只读的。 In fact, no part of any object in the objects database may ever be changed as the number (by which Git retrieves the object) is literally a checksum of the contents.事实上,对象数据库中的任何 object 的任何部分都不会被更改,因为数字(通过 Git 检索对象)实际上是内容的校验和。 To make this work well, it's a cryptographic checksum that works like a digital signature.为了使这项工作顺利进行,它是一个像数字签名一样工作的加密校验和。 If you take an object (commit or other object) out, modify it, and put the new data back in, you get a new, different object with a new, different number, unless you exactly repeat some existing object.如果您取出 object(提交或其他对象),对其进行修改,然后将新数据放回原处,您会得到一个新的、不同的 object,它具有一个新的、不同的数字,除非您完全重复一些现有的 ZA8CFDE6331BD59EB2AC96F8911C46。 Commits have stuff in them that doesn't repeat, so they always get unique numbers (er, well, as long as we ignore the pigeons in the pigeonhole principle ...).提交中有不重复的东西,所以它们总是得到唯一的数字(呃,好吧,只要我们忽略鸽子坑原则中的鸽子......)。

    • Stores that full snapshot of every file.存储每个文件的完整快照。 Each files' content—the bytes making up the data within a file—are turned into objects (in that same database) so they're all magically de-duplicated —repeat occurrences re-use the existing object—and they get compressed (initially, zlib compressed only; later, further compression happens) so they really don't take much space after all.每个文件的内容 - 构成文件中数据的字节 - 都被转换为对象(在同一个数据库中),因此它们都被神奇地重复数据删除 - 重复出现重复使用现有对象 - 并且它们被压缩(最初, zlib 仅压缩;稍后,会发生进一步的压缩),因此它们实际上并不占用太多空间。

    • Stores some metadata: information about the commit itself.存储一些元数据:关于提交本身的信息。 This includes, eg, the name and email address of the person who made the commit.这包括例如提交人的姓名和 email 地址。

Git writes the metadata itself: you supply parts of it, like a log message, but the metadata are in a Git-ized format, and Git includes, in this metadata in each commit, a list of previous commit hash IDs . Git 自己写入元数据:您提供它的一部分,如日志消息,但元数据采用 Git 化格式,并且 Git 在每次提交的此元数据中包括先前提交 Z0800FC577294C34E0495 IDs AD2 的列表 This list is usually exactly one entry long, producing what Git calls an ordinary commit .这个列表通常只有一个条目长,产生 Git 所谓的普通提交

If we have exactly one branch name and we've only ever done totally linear work (we've never branched-and-merged), then we have a really simple repository, which is easy to draw.如果我们只有一个分支名称并且我们只做过完全线性的工作(我们从未分支和合并),那么我们就有一个非常简单的存储库,它很容易绘制。 Let's say our branch is named main .假设我们的分支名为main You can use any name you like as long as it meets the constraints described in the git check-ref-format documentation , but this is one of the new standard names, so we'll use it here.您可以使用任何您喜欢的名称,只要它符合git check-ref-format文档中描述的约束,但这是新的标准名称之一,因此我们将在此处使用它。

Your branch has a latest commit, which Git calls the tip commit .您的分支有一个最新的提交,Git 称之为提示提交 The branch name actually contains the hash ID of this tip commit, so that Git can find that commit quickly in its big objects database.分支名称实际上包含此提示提交的 hash ID 以便 Git 可以在其大对象数据库中快速找到该提交。 (The names—branch names, tag names, and all other sorts of names, are stored in a secondary key-value database, with the names as the keys; each gets one hash ID as its value.) We say that the branch name points to the commit. (名称——分支名称、标签名称和所有其他类型的名称,以名称作为键存储在辅助键值数据库中;每个名称都有一个hash ID 作为其值。)我们说分支名称指向提交。 Let's call this commit's hash ID H (for Hash), and draw the name main pointing to H :让我们将此提交的 hash ID 称为H (用于哈希),并绘制指向H的名称main

            <-H   <-- main

What's this arrow sticking out of H ?H中伸出的这个箭头是什么? It represents the metadata in commit H , which stores the hash ID of H 's parent commit .它表示提交H的元数据,其中存储了H父提交的 hash ID。 Storing the hash ID of a commit makes something point to the commit.存储提交的 hash ID 可以指向提交。 Let's call that parent commit G , and add it to our drawing:让我们称其为父提交G ,并将其添加到我们的绘图中:

        <-G <-H   <-- main

But like H , G is an ordinary commit, so it points back to yet another, still-earlier commit:但是像H一样, G是一个普通的提交,所以它指向另一个更早的提交:

... <-F <-G <-H   <-- main

and this goes on forev—well, no, not forever!这会永远持续下去——嗯,不,不会永远! It stops at some point:它在某个时候停止

A--B--C--D--E--F--G--H   <-- main

(assuming there are eight total commits). (假设总共有八次提交)。 Commit A is the very first commit, and it doesn't have a left-pointing arrow sticking out of it.提交A是第一个提交,它没有指向左的箭头。 This makes it special.这使它变得特别。 It is a root commit , in Git jargon.这是一个根提交,用 Git 术语来说。

Note that I've also gotten lazy about drawing the arrows between commits.请注意,我也懒得在提交之间绘制箭头。 That's in part because no part of any commit can ever change .这部分是因为任何提交的任何部分都不能改变 It's only the arrow coming out of a branch name that can change.只有从分支名称出来的箭头可以改变。 The names are stored separately, in that names database, with their values being hash IDs, and we can replace the stored hash ID at any time.名称分别存储在该名称数据库中,其值为 hash ID,我们可以随时替换存储的 hash ID。

Making new commits from existing commits从现有提交进行新提交

When we're using an existing Git repository, it has some existing commits, one of which is the latest on some branch.当我们使用现有的 Git 存储库时,它有一些现有的提交,其中一个是某个分支上的最新提交。 Git attaches a special name HEAD to the one branch name you're actually using (though if you have only one name, that's going to be the one name you're using), so I'll draw that in: Git 将一个特殊名称HEAD附加到您实际使用的一个分支名称(尽管如果您只有一个名称,那将是您正在使用的一个名称),所以我将它画在:

...--G--H   <-- main (HEAD)

If and when we choose to make a new commit—without worrying about how we update the files that are to go into the new commit;如果以及何时我们选择进行的提交——不用担心我们如何将 go 的文件更新到新的提交中; we'll come back to this—Git will create that commit by:我们将回到这一点——Git 将通过以下方式创建该提交:

  • freezing a snapshot into a permanent archive, so that we can get it back;将快照冻结到永久存档中,以便我们将其取回;
  • wrapping that snapshot with commit metadata, including a parent;用提交元数据包装该快照,包括父级; and
  • writing all of that stuff into the objects database.将所有这些内容写入对象数据库。

This produces a new, random-looking (but not actually random), unique hash ID;这会产生一个新的、看起来随机的(但实际上不是随机的)唯一的hash ID; we'll just call it I .我们就叫它I吧。 Commit I stores commit H 's raw hash ID in commit I' s metadata, so that I points back to H .提交I将提交H的原始 hash ID 存储在提交I'的元数据中,以便I指向H And then Git simply writes I 's hash ID, whatever it turned out to be—we have no idea what it will be, until we have it, as one of the inputs to this is the exact second at which we make the commit—into the current branch name as represented by the name HEAD :然后 Git 简单地写入I的 hash ID,无论结果如何——我们不知道它会是什么,直到我们有了它,因为它的输入之一是我们进行提交的确切时间——进入由名称HEAD表示的当前分支名称

...--G--H--I   <-- main (HEAD)

We now have a new latest commit, and main automatically selects that latest commit.我们现在有一个新的最新提交, main会自动选择最新的提交。 Git can use the latest commit to work backwards to find H , which Git can use to work backwards to find G , and so on, all the way back to A . Git 可以使用最新的提交向后工作以找到H ,Git 可以使用它向后工作以找到G ,依此类推,一直回到A

So we make a new ordinary commit and it has a single parent.所以我们做了一个新的普通提交,它有一个单亲。 That's the very definition of an ordinary commit.这就是普通提交的定义 The git commit command does that for us, using the current commit as the parent of the new commit. git commit命令为我们执行此操作,使用当前提交作为新提交的父级。

Note that it works the same way even if we have more than one branch name.请注意,即使我们有多个分支名称,它的工作方式也是相同的。 Suppose that main points to H , as it did before we made I :假设main指向H ,就像我们之前所做的那样I

...--G--H   <-- main (HEAD)

Suppose we now create a second branch name, also pointing to H :假设我们现在创建第二个分支名称,也指向H

...--G--H   <-- develop, main (HEAD)

If we now switch to the name develop , we get:如果我们现在切换到名称develop ,我们会得到:

...--G--H   <-- develop (HEAD), main

We are still using commit H .我们仍在使用提交H We're just using it via the name develop now.我们现在只是通过名称develop来使用它。 When we make our new commit I , we get:当我们进行新的提交I时,我们得到:

...--G--H   <-- main
         \
          I   <-- develop (HEAD)

If we switch back to name main now, Git removes the commit- I files (which are archived forever in commit I ) and puts back the commit- H files (which are archived in commit H ).如果我们现在切换回名称main ,Git 会删除 commit- I文件(commit I中永久存档)并放回 commit- H文件(在 commit H中存档)。 We can, if we want, create another branch name for yet more commits, or make commits that extend main .如果需要,我们可以为更多提交创建另一个分支名称,或者进行扩展main的提交。 As we do more work with different branch names, we build up a commit graph that has obvious branches in it:随着我们对不同的分支名称进行更多的工作,我们构建了一个包含明显分支的提交图

          I--J   <-- br1
         /
...--G--H
         \
          K--L   <-- br2

We can do all this with ordinary Git commands: branches grow just by adding commits, and if we grow different branches that start out sharing commit H and all earlier commits, those branches grow in "different directions", as in this case.我们可以使用普通的 Git 命令来完成所有这些操作:分支仅通过添加提交来增长,如果我们增长开始共享提交H和所有早期提交的不同分支,那么这些分支会在“不同方向”增长,就像在本例中一样。

Note that commits up through H are on all the branches here.请注意,通过H的提交都在此处的所有分支上。 If there's a name main pointing to H , commit H is on three branches.如果名称main指向H ,则提交H位于三个分支上。 Add another name pointing to H and now commits up through H are on four branches.添加另一个指向H的名称,现在通过H的提交在四个分支上。 Nothing changes in the commits when we do this.当我们这样做时,提交中没有任何变化。 The branch names don't really matter!分支名称并不重要! The point of the branch name is to find the last commit .分支名称的重点是找到最后一次提交 Git uses the last one to work backwards, and if we remove all the names, so that we can't find the last commit somehow, we "lose" that commit. Git 使用最后一个向后工作,如果我们删除所有名称,以便我们无法以某种方式找到最后一个提交,我们会“丢失”该提交。

(Git may eventually remove totally-unused commits, and in fact, Git relies on this and generates "junk" or "trash" commits and other objects at various times, whenever it seems convenient. This is normally quite invisible and you don't have to care.) (Git 最终可能会删除完全未使用的提交,事实上,Git 依赖于此,并在看起来方便的不同时间生成“垃圾”或“垃圾”提交和其他对象。这通常是不可见的,你不会必须关心。)

History, in Git, is nothing but commits.历史,在 Git 中,只不过是提交。 We find history by using the branch names (and other names such as tags or remote-tracking names) to find "last" commits, then working backwards as much as we want to.我们通过使用分支名称(以及其他名称,例如标签或远程跟踪名称)来查找“最后”提交,然后尽可能多地向后工作来查找历史记录。

Merge commits are a bit special合并提交有点特别

Though this has nothing to do with the answer to your question, it's worth mentioning.尽管这与您的问题的答案无关,但值得一提。 Git makes a merge commit to tie two histories together. Git 进行合并提交以将两个历史记录联系在一起。 Consider this structure again:再次考虑这个结构:

          I--J
         /
...--G--H
         \
          K--L

Git can merge commits J and L to produce: Git 可以合并提交JL以产生:

          I--J
         /    \
...--G--H      M
         \    /
          K--L

Commit M is a merge commit , which in Git is defined as any commit with two or more parents.提交M是一个合并提交,在 Git 中定义为具有两个或多个父级的任何提交。 Here, commit M points backwards to both J and L .在这里提交M指向JL "Two" is the usual number—three or more parents makes an "octopus merge" and this doesn't do anything you could not do with regular merges—and since we're not getting into merging here, I'll stop there except to note one more thing. “二”是通常的数字——三个或更多的父母进行“章鱼合并”,这不会做任何常规合并无法做到的事情——因为我们没有在这里进行合并,我会停在那里,除了还要注意一件事。

In the "before" picture, where there was no merge commit M , we needed two branch names, one to find J and one to find L .在“之前”图片中,没有合并提交M ,我们需要两个分支名称,一个用于查找J ,一个用于查找L In the "after" picture, we can get by with just one branch name, to find M (or any new ordinary commits we add after M , as long as they don't fork off into more branch-y structures).在“之后”的图片中,我们只需一个分支名称就可以找到M (或者我们在M之后添加的任何新的普通提交,只要它们不分叉成更多的分支结构)。 This is why, after merging like this, we can delete one of the two branch names.这就是为什么像这样合并后,我们可以删除两个分支名称之一。

More about making commits: Git's index and your working tree更多关于提交:Git 的索引和你的工作树

You'll note that, at this point, all the commits we've made—whether with git commit or git merge —have been normal or merge commits, except that first one in the new, totally-empty repository.您会注意到,此时,我们所做的所有提交——无论是git commit还是git merge ——都是正常的或合并的提交,除了新的、完全空的存储库中的第一个提交。 An ordinary commit has the current commit —as found by HEAD until Git updates the branch name—as its parent, and a merge commit has the current commit as its first parent, and the tip of the merged-commit—eg, commit L above, perhaps, assuming we were using J when we merged—as its other parent.普通提交将当前提交(由HEAD找到,直到 Git 更新分支名称)作为其父提交,合并提交将当前提交作为其第一个父提交,以及合并提交的尖端——例如,上面的提交L ,也许,假设我们在合并时使用J - 作为它的另一个父级。

When Git does make a new commit, Git does so from the files that Git has in what Git calls its index , or the staging area , or (rarely these days) the cache . When Git does make a new commit, Git does so from the files that Git has in what Git calls its index , or the staging area , or (rarely these days) the cache . These three names all refer to the same thing.这三个名字都指同一个东西。

When you check out a commit, with git checkout or git switch , Git fills in its index from the commit you've picked to switch-to.当您签出提交时,使用git checkout出或git switch , Git 从您选择切换到的提交中填写其索引。 The index therefore holds all the files from the snapshot in the commit .因此,索引将快照中的所有文件保存在 commit 中 Git also copies these files to an area where you can see and work on / with them. Git还将这些文件复制到您可以查看和处理/使用它们的区域。 Remember how I emphasized earlier that all parts of every commit are read-only, and are compressed (Git-ized) and de-duplicated as well.记住我之前强调过的每个提交的所有部分都是只读的,并且被压缩(Git-ized)和重复数据删除 That means the files in the commit are completely unusable for any purpose except to be un-archived like this, or more generally, used internally by Git for reading.这意味着提交的文件完全不能用于任何目的,除非像这样取消归档,或者更一般地,由 Git 内部用于读取。

Most version control systems face this same kind of problem (immutable stored files) and have the same solution (extract those files).大多数版本控制系统都面临着同样的问题(不可变的存储文件)并且具有相同的解决方案(提取那些文件)。 But most of them just extract the files to your working tree, so that you can see them and work on / with them, and that's it.但是他们中的大多数只是将文件提取到您的工作树中,以便您可以查看它们并使用它们/与它们一起工作,仅此而已。 This produces two copies of each file: the permanent archived one in the commit, and the usable one.这会生成每个文件的两个副本:提交中的永久存档副本和可用的副本。

Git, however, stores three copies.然而,Git 存储三个副本。 There's those two, but in between , Git sticks a third copy—or "copy", because it's pre-de-duplicated —into what Git calls that index or staging area.有这两个,但介于两者之间,Git 将第三个副本(或“副本”)粘贴到 Git 所谓的索引或暂存区域中,因为它是预先去重的 The key difference between the index copy and the committed copy is that you can have Git replace the staging-area copy.索引副本和提交副本之间的主要区别在于,您可以让 Git替换暂存区域副本。

This is what git add does.这就是git add所做的。 When you git add an existing file, Git reads the file, compresses it, Git-izes it, and checks for duplicates.当您git add现有文件时,Git 会读取该文件,对其进行压缩、Git 化并检查是否存在重复文件。 If there's a duplicate, Git uses the existing object (uses the old hash ID).如果有重复,Git 使用现有的 object(使用旧的 hash ID)。 If not, Git readies the contents for committing (they get a new, unique hash ID).如果没有,Git 准备提交内容(他们获得一个新的、唯一的 hash ID)。 Either way, the file is now ready to be committed—or re-used, if it's a duplicate—and Git updates the index slot that holds that file.无论哪种方式,文件现在都可以提交或重新使用,如果它是重复的,并且 Git 会更新保存该文件的索引槽。

So, before git add , the entire set of files was ready to be committed.因此,在git add之前,整个文件集已准备好提交。 After git add , the entire set of files is still ready to be committed.git add之后,整个文件集仍然可以提交。 You have merely changed one of those files (changed its contents, really).您只是更改了其中一个文件(实际上更改了其内容)。

If you git add a new file name, Git compresses its contents just as for an existing file, and checks for duplicate contents the same way as usual and either re-uses an old hash or gets a new one.如果你git add一个新的文件名,Git 压缩它的内容就像一个现有的文件,并像往常一样检查重复的内容,或者重新使用一个旧的 Z0800FC577294C34E0B28AD283Z43。 Then Git writes a new index slot for the new file name, and now the staging area is ready to go into a new commit.然后 Git 为新的文件名写入一个新的索引槽,现在暂存区准备 go 进入一个新的提交。 So it was ready before git add , and it's still ready after git add : it's just acquired one new file name.所以在git add之前准备好了,在 git git add之后仍然准备好了:它只是获得了一个新的文件名。

In other words, at all times , the index holds the next commit, ready to go.换句话说,在任何时候,索引都保存着下一次提交,准备好 go。 This skips over the problem of merge conflicts (in which case the staging area holds files that aren't ready to be committed: they're in the right form, but they're marked "conflicted", in ways we won't cover here).这跳过了合并冲突的问题(在这种情况下,暂存区域包含尚未准备好提交的文件:它们的格式正确,但它们被标记为“冲突”,我们不会介绍这里)。 So thinking of the index / staging-area as "my proposed next snapshot" is not quite perfect , but it's good enough to get things done, and if git commit tells you you have unmerged files, that's a reminder that your job is to fix up the index / staging-area now.因此,将索引/暂存区域视为“我建议的下一个快照”并不是很完美,但它足以完成任务,如果git commit告诉您有未合并的文件,这提醒您您的工作是修复现在增加索引/暂存区。

Because the index is separate from your working tree, it's possible to have three different copies of some "active" file.因为索引与您的工作树是分开的,所以可能有一些“活动”文件的三个不同副本。 For instance:例如:

git switch somebranch
vim foo.py        # make some changes and write them out
git add foo.py    # update index copy of foo.py
vim foo.py        # make more changes and write them out

Now the committed ( HEAD ) foo.py still has the original content, the index / staging-area foo.py has the middle version, and the working tree foo.py has the most recent version.现在提交的( HEADfoo.py仍然具有原始内容,索引/暂存区域foo.py具有中间版本,工作树foo.py具有最新版本。 If you git add again, the index copy starts matching the working tree copy again.如果您git add ,则索引副本开始再次匹配工作树副本。

Usually either all three copies match, or two copies match and one is different.通常要么所有三个副本都匹配,要么两个副本匹配并且一个不同。 You git add files to make the index copy match the working tree copy , so that now the two that match are index and working tree.git add文件以使索引副本与工作树副本匹配,因此现在匹配的两个是索引和工作树。 You run git commit to make a commit from the index copy.您运行git commit索引副本进行提交。

git commit --amend : a useful lie git commit --amend :一个有用的谎言

Method 1, way at the top of this answer, uses git commit --amend .方法1,在这个答案的顶部,使用git commit --amend The --amend flag sounds like Git is fixing up a commit. --amend标志听起来像 Git 正在修复提交。 This is a lie, but it's a useful lie.这是一个谎言,但它是一个有用的谎言。 Let's suppose we have:假设我们有:

...--G--H   <-- main (HEAD)

and we work on and make a new commit:我们继续努力并做出新的承诺:

...--G--H--I   <-- main (HEAD)

but then we notice a typo in the commit message .但随后我们注意到提交消息中有一个错字。 The snapshot—the files that were in the index, and still are in the index—is fine, but we want to fix our typo, so we run:快照——索引中的文件,仍然在索引中——很好,但我们想修正我们的错字,所以我们运行:

git commit --edit --amend

(there's --edit and --no-edit options as well as --amend for this case) and fix our typo, write out the updated commit message, and let Git get to it. (有--edit--no-edit选项以及--amend用于这种情况)并修复我们的错字,写出更新的提交消息,然后让 Git 得到它。

Commit I literally cannot be changed.提交I真的无法改变。 But Git can "boot it off the end" of the branch and make a new, improved I' , like this:但是 Git可以“从分支的末尾引导它”并创建一个新的、改进I' ,如下所示:

          I   [abandoned]
         /
...--G--H--I'  <-- main

Since there's no name by which to find commit I , we won't see it any more.由于没有可以找到提交I名称,我们将不再看到它。 If we don't pay close attention to hash IDs—and who does that?—it will look like Git changed commit I .如果我们不密切关注 hash ID——谁在做这件事?——看起来Git 更改了提交I

The --amend option works by giving the new commit the same parent(s) as the current commit. --amend选项通过为新提交提供与当前提交相同的父级来工作。 This lets you "amend" a merge commit (which I won't draw for space reasons, plus it's hard to draw well).这可以让你“修改”一个合并提交(由于篇幅原因我不会画出来,而且很难画好)。 But it also means that if you have:但这也意味着,如果您有:

A--B--C--D--E--F--G--H   <-- main

and make a new name new point directly to commit A (and switch to it):并直接为提交A命名new新名称(并切换到它):

A   <-- new (HEAD)
 \
  B--C--D--E--F--G--H   <-- main

and run git commit --amend , our new commit will have the same parents that A has.并运行git commit --amend ,我们的提交将具有与A相同的父级。 Since A has no parent, our new A' will have no parent too:由于A没有父级,我们的新A'也将没有父级:

A'  <-- new (HEAD)

A--B--C--D--E--F--G--H   <-- main

This is the trick that we'll use in method 1.这是我们将在方法 1 中使用的技巧。

Now, the problem with just checking out commit A directly is that Git would rip out all our working tree and index copies of all files, and replace them with the files from commit A .现在,直接检查提交A的问题是 Git 会删除我们所有文件的所有工作树和索引副本,并用来自提交A的文件替换它们。 So instead of using git checkout , we'll use git reset --soft .因此,我们将使用git reset --soft而不是使用git checkout

git reset

The git reset command is very big and complicated. git reset命令非常大且复杂。 It has a couple of major modes though, covered by the --hard , --mixed , and --soft options, along with its many other modes that we won't cover here for space reasons.不过,它有几个主要模式,包括--hard--mixed--soft选项,以及许多其他模式,由于篇幅原因,我们不会在这里介绍。 When used in these major modes, git reset does three things:在这些主要模式下使用时, git reset会做三件事:

  1. It moves the current branch name to point to some commit.它移动当前分支名称以指向某个提交。 You pick any existing commit, and the name now points there.您选择任何现有的提交,名称现在指向那里。

    If you used --soft , git reset stops here.如果你使用--softgit reset在这里停止。 Otherwise it goes on to step 2.否则进入第 2 步。

  2. It resets Git's index, making it match the commit you selected in step 1.它会重置 Git 的索引,使其与您在步骤 1 中选择的提交匹配。

    If you used --mixed (or no flag), git reset stops here.如果您使用--mixed (或没有标志), git reset在此处停止。 Otherwise ( --hard ), it goes on to step 3.否则( --hard ),它继续到第 3 步。

  3. It resets your working tree in the same way it reset Git's index, so that the files you have checked out are those from the commit you reset to.它以与重置 Git 索引相同的方式重置您的工作树,以便您签出的文件是您重置到的提交中的文件。

Note that when Git wipes out index and/or working tree copies of files, those may be the only copies of those files.请注意,当 Git 清除文件的索引和/或工作树副本时,这些可能是这些文件的唯一副本。 This is what makes a hard reset so particularly dangerous.这就是硬重置如此危险的原因。 However, sometimes that's just what we want: git reset --hard HEAD moves the current commit, in step 1, to the select commit, which is... the current commit .但是,有时这正是我们想要的: git reset --hard HEAD在步骤 1 中将当前提交移动到 select 提交,即...当前提交 So step 1 happens but the branch name continues to select the same commit as before.所以第 1 步发生,但分支名称继续 select 与以前相同的提交。 Then steps 2 and 3 wipe out the work we did, which is what we want from git reset --hard .然后步骤 2 和 3 清除了我们所做的工作,这就是我们想要git reset --hard的工作。

In our case, though, we want git reset --soft .但是,在我们的例子中,我们想要git reset --soft This will move the current branch name .这将移动当前分支名称 Since the current branch name in our setup is main , we:由于我们设置中的当前分支名称是main ,我们:

  1. create a new branch name, save , to remember commit H : git branch save ;创建一个新的分支名称save ,记住提交H : git branch save
  2. use git reset --soft to reset to the root commit: git rev-list finds that;使用git reset --soft重置到根提交: git rev-list发现;
  3. git commit --amend makes our new A' commit. git commit --amend使我们的新A'提交。

We end up with:我们最终得到:

A'  <-- main (HEAD)

A--B--C--D--E--F--G--H   <-- save

which is what option 1 does.这就是选项 1 的作用。

The git rev-list command is a way of finding commit hash IDs. git rev-list命令是一种查找提交 hash ID 的方法。 It walks through history the same way git log does—one commit at a time, backwards—and prints out selected commits;它以与git log相同的方式遍历历史记录——一次提交一次,向后提交——并打印出选定的提交; here, we have it print only those commits that have no parents.在这里,我们让它只打印那些没有父母的提交。 Only the root commit has no parents, so this prints out the hash ID of commit A .只有提交没有父提交,所以这会打印出提交A的 hash ID。

New empty repositories, and the --orphan flags新的空存储库和--orphan标志

Suppose we run:假设我们运行:

mkdir new-repo && cd new-repo && git init --initial-branch=main

This makes a shiny new repository: basically, two empty databases.这使得 shiny 成为新的存储库:基本上,两个数据库。 There are no commits, and there are no branch or tag or other names at all.没有提交,根本没有分支或标签或其他名称。

If you run git status in this new empty repository, you'll see something odd.如果您在这个新的空存储库中运行git status ,您会看到一些奇怪的东西。 You are "on" branch main or master or whatever you choose for your initial branch name.您处于“on”分支mainmaster或您为初始分支名称选择的任何内容。 And yet, git branch won't list any branch names, and git log can't show you any commits.然而, git branch不会列出任何分支名称,并且git log无法显示任何提交。 There literally are no commits as the objects database is empty (except, perhaps, for the empty tree and any other tricks Git might have up its sleeves, if Git has sleeves).实际上没有提交,因为对象数据库是空的(如果 Git 有袖子,可能除了空树和任何其他技巧 Git 可能袖手旁观)。

A branch name in Git is required to point to some commit. Git 中的分支名称需要指向某个提交。 You can't have the branch name if you don't have the commit for it.如果您没有提交,则不能拥有分支名称。 This is just a Rule of Git.这只是 Git 的规则。 Since there are no commits, there must be no branches .由于没有提交,因此必须没有分支 Yet you're still "on" some branch.然而你仍然“在”某个分支。 Git does this by attaching the name HEAD to the branch name, even though the branch name doesn't exist. Git 通过将名称HEAD附加到分支名称来执行此操作,即使分支名称不存在。 (Concretely, Git writes the branch name into the file .git/HEAD —but don't count on this, as added working trees are different.) (具体来说,Git 将分支名称写入文件.git/HEAD ——但不要指望这一点,因为添加的工作树是不同的。)

The way Git handles all this is to call this situation an orphan branch or an unborn branch . Git 处理这一切的方式是将这种情况称为孤儿分支或未出生分支 (Different bits of Git source use the two different terms, inconsistently, the way index / staging-area is.) When you make a commit while you are "on" an unborn branch, this makes a new root commit . (Git 源代码的不同位使用两个不同的术语,不一致的是,索引/暂存区域的方式。)当您在“打开”未出生的分支时进行提交这会生成一个新的根提交 So:所以:

<create some files>
<add the files>
git commit -m initial

creates commit A , the root commit, and creates the current branch name at the same time, and now you have one commit and one branch name and we're out of this squirrelly mode where Git acts kind of weird.创建提交A ,根提交,并同时创建当前分支名称,现在您有一个提交和一个分支名称,我们已经脱离了 Git 行为有点奇怪的这种松鼠模式。

But Git offers the ability to go back into this mode, using git checkout --orphan or git switch --orphan .但是 Git 提供了使go 回到此模式的能力,使用git checkout --orphangit switch --orphan . This is what we use for method 2.这就是我们用于方法 2 的内容。

If we want to use this mode, we want to use git checkout --orphan here, and there's a reason for that.如果我们想使用这种模式,我们想在这里使用git checkout --orphan ,这是有原因的。 Although git checkout and git switch mostly do the same thing, most of the time, for cases like these, the --orphan flag is very different in the two:尽管git checkoutgit switch大多做同样的事情,但大多数时候,对于这样的情况, --orphan标志在两者中是非常不同的:

  • git checkout --orphan newname leaves the index and working tree alone, putting you on a new unborn branch; git checkout --orphan newname单独离开索引和工作树,将您置于新的未出生分支上;
  • git switch --orphan newname empties the index (and updates the working tree to match) while putting you on a new unborn branch. git switch --orphan newname清空索引(并更新工作树以匹配),同时将您置于新的未出生分支上。

We want our new root commit to hold the same snapshot as the final commit on the existing main branch.我们希望我们的新根提交与现有main分支上的最终提交保持相同的快照 If we're "on" main , with everything all clean so that the HEAD commit, the index, and the working tree all match, and we git checkout --orphan new , we'll retain the desired snapshot.如果我们“打开” main ,一切都干净,以便HEAD提交、索引和工作树都匹配,并且我们git checkout --orphan new ,我们将保留所需的快照。 We can now simply git commit to create our new root commit.我们现在可以简单地git commit来创建我们的新根提交。

That's our method 2 above.这就是我们上面的方法2。

(If you accidentally use git switch --orphan , all is not lost: you can git read-tree -u main to refill Git's index and your working tree. But this command is even more obscure than the --orphan flag.) (如果您不小心使用git switch --orphan ,一切都不会丢失:您可以git read-tree -u main重新填充 Git 的索引和您的工作树。但是这个命令比--orphan标志更模糊。)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM