【问题标题】:A Unix 'rm' on a git repository project branch removes file from master branch directorygit 存储库项目分支上的 Unix 'rm' 从主分支目录中删除文件
【发布时间】:2020-03-17 16:51:26
【问题描述】:

寻找导致意外行为的以下情况的“为什么” - 特别是使用 unix 命令“rm”删除我的 git 存储库中项目分支上的文件也从主分支中删除了该文件。下面我给出命令的摘要,然后给出完整的控制台。

命令摘要:

  1. git 初始化
  2. 触摸文件1.txt文件2.txt
  3. git add *.txt
  4. git commit -m "添加file1.txt和file2.txt"
  5. git checkout -b myBranch
  6. rm file1.txt
  7. git status => 显示已删除的 file1.txt 未暂存以进行提交
  8. git checkout master
  9. git status => 显示已删除的 file1.txt 未暂存以进行提交
  10. ls => 显示 file1.txt 已从目录(主目录)中删除
  11. git checkout myBranch
  12. git rm file1.txt
  13. git commit -m "删除 file1.txt"
  14. git checkout master
  15. ls => 显示 file1.txt 在目录中(master)

上述总结中的关注点:第 9、10 行文件在 master 中被删除,但又回到第 15 行。

控制台详情(注意,可能有额外的显示条目)

ec2-user:~/environment/TestGit $ git init
Initialized empty Git repository in /home/ec2-user/environment/TestGit/.git/

ec2-user:~/environment/TestGit (master) $ touch file1.txt file2.txt
ec2-user:~/environment/TestGit (master) $ git add *.txt
ec2-user:~/environment/TestGit (master) $ git commit -m "Add file1.txt and file2.txt"
[master (root-commit) 531ed48] Add file1.txt and file2.txt
 Committer: EC2 Default User <ec2-user@ip-172-31-37-27.ec2.internal>
Your name and email address were configured automatically based
on your username and hostname. Please check that they are accurate.
You can suppress this message by setting them explicitly:

    git config --global user.name "Your Name"
    git config --global user.email you@example.com

After doing this, you may fix the identity used for this commit with:

    git commit --amend --reset-author

 2 files changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 file1.txt
 create mode 100644 file2.txt

ec2-user:~/environment/TestGit (master) $ git checkout -b myBranch
Switched to a new branch 'myBranch'

ec2-user:~/environment/TestGit (myBranch) $ ls
file1.txt  file2.txt

ec2-user:~/environment/TestGit (myBranch) $ rm file1.txt
ec2-user:~/environment/TestGit (myBranch) $ ls
file2.txt

ec2-user:~/environment/TestGit (myBranch) $ git status
On branch myBranch
Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        deleted:    file1.txt

no changes added to commit (use "git add" and/or "git commit -a")

ec2-user:~/environment/TestGit (myBranch) $ git checkout master
D       file1.txt
Switched to branch 'master'

ec2-user:~/environment/TestGit (master) $ ls
file2.txt

ec2-user:~/environment/TestGit (master) $ git status
On branch master
Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        deleted:    file1.txt

no changes added to commit (use "git add" and/or "git commit -a")

ec2-user:~/environment/TestGit (master) $ git checkout myBranch
D       file1.txt
Switched to branch 'myBranch'

ec2-user:~/environment/TestGit (myBranch) $ git status
On branch myBranch
Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        deleted:    file1.txt

no changes added to commit (use "git add" and/or "git commit -a")

ec2-user:~/environment/TestGit (myBranch) $ git rm file1.txt
rm 'file1.txt'

ec2-user:~/environment/TestGit (myBranch) $ git status
On branch myBranch
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        deleted:    file1.txt

ec2-user:~/environment/TestGit (myBranch) $ git commit -m "Remove file1.txt"
[myBranch 6585980] Remove file1.txt
 Committer: EC2 Default User <ec2-user@ip-172-31-37-27.ec2.internal>
Your name and email address were configured automatically based
on your username and hostname. Please check that they are accurate.
You can suppress this message by setting them explicitly:

    git config --global user.name "Your Name"
    git config --global user.email you@example.com

After doing this, you may fix the identity used for this commit with:

    git commit --amend --reset-author

 1 file changed, 0 insertions(+), 0 deletions(-)
 delete mode 100644 file1.txt

ec2-user:~/environment/TestGit (myBranch) $ git status
On branch myBranch
nothing to commit, working tree clean

ec2-user:~/environment/TestGit (myBranch) $ ls
file2.txt

ec2-user:~/environment/TestGit (myBranch) $ git checkout master
Switched to branch 'master'

ec2-user:~/environment/TestGit (master) $ ls
file1.txt  file2.txt

【问题讨论】:

  • 怎么了?您描述的配方和您在控制台上显示的内容都符合预期。
  • 步骤 14 恢复工作目录以匹配 master。这些文件位于master,因此结帐会创建它们。这是意料之中的。
  • 第 9 行和第 10 行不“从主文件中删除文件”。他们从工作目录中删除文件。在第 13 行,您从 MyBranch 中删除了 file1。您永远不会将它们从 master 中删除。
  • 但文件最初是创建并提交给主服务器(2、3、4),然后创建了 MyBranch。这些文件已从 MyBranch (6) 中删除,然后我切换回 master (8)。 (9) 和 (10) 表明该文件不再是 Master 的一部分。这可以在提供的控制台中看到
  • @SParker 您提供的输出显示完全相反。当它显示deleted: file1.txt 时,它告诉您file1.txt 已从工作目录中删除,但它仍在主分支中。

标签: git


【解决方案1】:

好吧...我觉得我明白你不明白的地方。在第 9 点和第 10 点,您在 master.. 上玩弄。但是,您随后切换回 myBranch,这就是您提交的时候。所以... master 留在原处(第一次提交,有两个文件,它没有移动),你最终在 myBranch 上提交了文件删除。这就是为什么当你回到 master 时,两个文件都在那里。

【讨论】:

  • 是的,但是如果你按照控制台,你会看到我在 master 上创建了文件,创建了一个新分支,删除了新分支上的文件,然后在做任何其他事情之前我切换了返回主服务器并执行“git status”和“ls”,但主服务器上缺少文件。
  • 是的,但是 你没有在 master 上提交。修订是在 myBranch 上创建的,这就是文件被删除的地方。主人留在原来的地方,这就是为什么如果你切换到它,文件就在那里。
  • 但这就是我的意思。在 #9 和 #10 之后我再次检查了 master,我应该仍然看到 file1.txt,但它不存在。
  • 发生这种情况是因为您尚未提交。经常想要移动到不同的分支,周围有变化。 Git 尽其所能:检查在索引和工作树中修改的文件,以查看它们是否在 HEAD 中,因为它们在您要前往的修订版中。如果它们相同,它允许您在未提交更改的情况下执行此操作。这就是为什么该文件似乎不存在的原因(它已被删除......但在索引上)。如果 git 应该在没有任何更改的情况下进入 master,那将是一个更大的问题。它必须要求您存储或其他任何东西。
【解决方案2】:

您对 Git 工作方式的心智模型不正确。 (不用担心,我在十多年前开始使用 Git 时就这样做了。)要纠正你的思维模式,你需要知道这些事情:

  • Git 存储提交。它不存储文件——不管怎样,不是在你将使用它的级别——而是整个提交。

  • 提交自己做存储文件,这就是你获取文件的方式,但它处于提交级别:你要么有一个提交(及其所有文件),要么你没有(您没有任何文件)。每次提交都会存储所有文件的完整快照(嗯,所有其文件;见下文)。

  • 提交还存储一些关于提交的元数据:信息,例如提交人、时间和原因(日志消息)。每个提交中的一个关键元数据是提交的提交“编号”在此提交之前。

  • 提交“数字”是大而丑陋且随机的哈希 ID。每个提交都会获得一个唯一的哈希 ID。这就是你(或你的 Git)知道你是否有提交的方式。各地的每个 Git 都同意 那个特定的提交 获得 那个特定的哈希 ID,并且没有其他提交,过去或未来,可以拥有那个 ID。为了实现这一点,哈希 ID 是提交内容的加密校验和——这意味着任何现有提交的任何部分都永远可以更改。

  • 没有人能真正记住这些哈希 ID。幸运的是,我们不必这样做:我们有一台计算机可以为我们记住它们。

  • 分支名称,大多数人(包括我)通常缩写为“分支”,它只包含一个哈希 ID。像这样的 name 中的哈希 ID 是分支中 last 提交的 ID。这就是为什么每个提交都链接回其父提交或上一个提交:以便 Git 可以从最后开始并向后工作。

  • 一个提交的集合,你从最后开始并向后工作也被称为“一个分支”。因此,例如,当有人说 branch master 时,重要的是要考虑这是否意味着 master 中的最后一次提交以名称 master 或以master中的最后一次提交结束的一系列提交。

现在,每个提交都是只读这一事实意味着我们对存储库所做的通常只是添加新提交。但是要进行新的提交,我们必须能够更改文件:在我们的编辑器中打开它们,对它们进行更改,然后将它们保存回来。提交中的文件无法更改。所以我们不会也不能处理提交的文件。保存所有文件快照的提交本身只是存档。

为了防止档案过快增长,Git 以一种特殊的、只读的、仅限 Git 的压缩格式存储提交的文件。只有 Git 本身才能真正使用这些。 (您当然可以编写自己的程序来阅读它们,但格式不止一种,而且已经有一个 Git plumbing 命令,即用户不应该使用的东西来阅读一个原始对象,使用git cat-file -p。这不仅可以读取文件,还可以读取提交中的文件。)新提交可以共享来自现有提交的文件——这显然是安全的,因为它们'都是只读的——事实上,这一切都是自动发生的。

无论如何,要在某个现有存储库中完成任何新工作,您必须首先选择一些现有提交并让 Git 将其提取到某个地方。那个“某处”是你的工作树(或工作树或这个名字的一些变体)。提取的工作树区域包含普通文件,采用普通日常格式。

您和您的计算机可以使用这些工作树文件。例如,这就是您在第 2 步和第 6 步中所做的。

Git 根本不使用这些工作树文件。它为您创建它们(通过从提交中提取它们),当你告诉它时它会查看它们,但它不会使用它们来进行提交。它们的存在是为了您 使用,以完成您的工作。您必须将它们复制到 Git 正在使用的文件中,这就是第 3 步的内容。 这就是一切变得有点复杂的地方。

索引

在第 1 步中,您创建了一个新的空 Git 存储库。此存储库还没有提交。它有一个空的工作树,您可以在其中处理您的文件。而且,它有一个空的索引。这个东西——这个索引——有点复杂,但你可以把它想象成你构建下一个提交的地方。您可以将其视为保存每个文件的副本。

您的第 2 步是:

touch file1.txt file2.txt

在您的工作树中创建了两个(空)文件。这些文件还没有在您的索引中。不过,您的第 3 步是:

git add file1.txt file2.txt

这具有将文件内容复制到索引中的效果。1Git 现在表示这些文件已暂存。这导致索引的另一个替代名称:它也称为暂存区。这些只是同义词:索引或暂存区只是一回事。2

最后,在第 4 步中,您运行了git commit。这从 索引 中的文件而不是工作树中的文件进行了新提交。这两个索引文件是工作树中的 副本。

此时,您现在有一个提交。这一次提交是存储库中的第一次提交,所以它有点特别:它不记录任何以前的提交。 (当然不能;没有以前的提交。)我不知道你的提交得到了什么哈希 ID:它不仅取决于提交中的文件(我知道)和你的日志消息(我在你的命令中看到的),还有你的姓名和电子邮件地址和在你的 Git 创建提交的那一秒(我不知道这些)。不过,我确实知道,它有一个唯一的哈希 ID,不同于您的存储库中的所有其他哈希 ID,或者您将来与您的存储库对话的任何其他 Git 存储库。3


1从技术上讲,索引保存文件的模式、它们的名称,以及(对于每个文件)对保存内容的内部 Git 对象的引用。这个blob 对象 有一个哈希ID,就像一个提交(虽然与提交不同,一个blob 对象可以重复使用)。空文件的hash ID是e69de29bb2d1d6434b8b29ae775ad8c2e48c5391,运行git hash-object -t blob --stdin &lt;/dev/null可以找到。如果当 Git 迁移到 SHA-2 而不是 SHA-1 时,每个对象的 ID 都会发生变化,这对 Git 来说将是一个非常有趣的时刻。我们可以希望 Git 为我们隐藏所有痛苦的部分。

2从技术上讲,索引主要是.git 中名为.git/index 的文件。 “大部分”在这里只是因为 Git 有一种称为 split index 的模式。然而,所有这些都是可能改变的内部细节。一个外部承诺是您可以设置一个名为 GIT_INDEX_FILE 的环境变量,以使 Git 使用不同的索引。一些 Git 程序出于特殊目的这样做:例如,git stash,当它是一个 shell 脚本时,在进行一些 stash 提交时这样做,以避免覆盖正常索引。

3这取决于哈希 ID 的唯一性。在存在恶意行为者的情况下,这又部分取决于密码学的强度。见How does the newly found SHA-1 collision affect Git?


更多关于分支名称

我们已经提到分支名称,例如 master,包含提交的哈希 ID。在您拥有一些哈希 ID 之前,您不能拥有任何分支名称。所以创建这个初始提交就是创建 name master。该名称包含实际的哈希 ID,无论它是什么。当某物持有哈希 ID 时,我们说这东西 指向 提交。所以此时——在第 4 步创建了第一个提交之后——你有一个带有一些大而丑陋的哈希 ID 的提交,但我们称之为“commit A”,并像这样绘制它:

A   <-- master

名称master指向(包含)提交A的哈希ID。

现在我们进入第 5 步:

git checkout -b myBranch

这会创建一个新的名称,myBranch,还包含现有提交 A 的哈希 ID。让我们更新我们的绘图:

A   <-- master, myBranch

Git 还需要知道我们正在使用哪个分支名称,所以让我们将名称HEAD(全部大写)附加到这两个分支名称之一。我们要使用的分支名称(由 git checkout -b 创建)是新分支名称,因此是:

A   <-- master, myBranch (HEAD)

两个名称都指向同一个提交。这在 Git 中是完全正常的:commit A 现在在两个分支上。 当前名称是myBranch,当前提交是提交A。

现在让我们看看步骤 6、7 和 8 中发生了什么:

  1. rm file1.txt

    这会从您的工作树中删除文件。 Git 的 index 仍然与 commit A 匹配——Git 将提交 A 来自索引——其中仍然有两个文件。

  2. git status

    这会运行两个单独的比较。一个比较当前提交,提交A,与索引。这些具有相同内容的相同文件,因此git status 的这一部分什么也没说。第二个比较是 index-vs-work-tree。在这里,索引有file1.txt 而工作树没有,所以这个比较表明file1.txt 是从工作树中删除的,而不是从索引中删除的,也就是说这个删除是not staged for commit。

  3. git checkout master

    这告诉 Git 你想要更改当前的提交和/或分支。当前分支是myBranch,当前提交是A。选定的分支名称为master,其提交为A。所以 Git 可以跳过更改 commits,同时将特殊名称 HEAD 粘贴到名称 master 现在:4

    A   <-- master (HEAD), myBranch
    

其他任何地方都没有发生任何事情:索引仍然有两个文件,当前提交仍然是提交A,工作树仍然缺少一个文件。第 9 步 — 另一个 git status — 将告诉您当前分支现在是 master,但会进行相同的比较:提交 A 与索引,以及索引与工作树。这里的结果是一样的。第 10 步只查看工作树,我们知道它缺少 file1.txt。

步骤 11 要求 Git 再次将 HEAD 附加到 master。没有其他任何变化:索引没有改变,工作树也没有改变。

不过,在第 12 步中,您运行:

git rm file1.txt

这会改变索引。 git rm 命令从索引和工作树中删除文件。它已经从工作树中消失了,所以这并没有真正改变任何东西,但是现在 index 中不再有 file1.txt。

在第 13 步中,您再次运行 git commit。这会根据索引中的内容创建一个 new 提交:也就是说,一个只有空的 file2.txt 的提交。您还可以获得所有常见的元数据:您的姓名和电子邮件地址,以及您提交此提交的原因的日志消息。这个新提交的父是现有提交A,我们称之为B,而不是尝试猜测哈希ID:新提交B指向现有提交A .

git commit 的最后一步是让 Git 将新提交的哈希 ID 写入 HEAD 所附加的名称中。由于第 11 步将HEAD 附加到myBranch,结果是这样的:

A   <-- master
 \
  B   <-- myBranch (HEAD)

现有名称master 完全没有改变。 HEAD 仍附加到 myBranch,但名称 myBranch 现在指向新提交 B。该索引仍然具有运行 git commit 之前的所有内容:即,它只有空的 file2.txt。提交B 有一个向后箭头指向——或者实际上,包含提交A 的哈希ID,所以如果你现在运行git log,你的Git 将从HEAD 开始,找到myBranch,找到B,显示提交B,按照箭头提交A,显示提交A。


4从技术上讲,Git 通过将分支名称 master 写入 .git 中名为 .git/HEAD 的文件中来实现这一点。你可以查看这个文件,但是当你想更新它时,你应该使用各种Git工具,因为在各种情况下,Git可能会使用一些other文件。特别是,从 Git 2.5 开始,Git 现在有了 git worktree add,它添加了一个新的 index-and-work-tree 对。每个添加的工作树也必须有自己单独的HEAD,所以一旦你添加了一些工作树,索引不再总是.git/index,HEAD 不再总是.git/HEAD .


总结

始终牢记以下几点:

  • Git 就是关于提交。分支名称——以及其他名称,一旦你到达那个点——只是为了find提交。

  • 每个提交都有一个唯一的哈希 ID,除了一些未完成的新功能(“部分克隆”)之外,您总是要么有完整的提交,要么没有提交。

  • 每个提交都链接回一个或多个前任或 父 提交,但某些存储库中的第一次提交等特殊情况除外。这些链接或提交链形成了人们所说的分支(“分支”一词的几种含义之一)。

  • 要进行新的提交,您需要更新 Git 的 index。当你第一次git checkout 一些你还没有完成的提交时,Git 会从那个提交中填充索引——当然还有你的工作树。 您使用工作树中的文件,而 Git 使用它的索引。

  • 索引和您的工作树不会被复制:当您git clone、git fetch 或git push 时,您将传输提交。索引和工作树在这里无关紧要(嗯,git push 有一些条件,在 other Git 中,接收你的 git push)。

  • 提交一直被冻结(而且大部分是永久的——它们有点难以摆脱,即使你有时想摆脱)。索引和工作树中的文件副本是临时的。

  • 添加 新提交 会更新您的分支名称。更新的分支名称是您附加 HEAD 的分支名称。

  • 在 Git 2.23 或更高版本中,您可以使用 git switch 选择 HEAD 去哪里和/或创建新的分支名称,并使用 git restore 从特定提交中提取特定文件;在早期版本的 Git 中,这两个作业都被困在一个 git checkout 命令中。

  • 当您使用第二个 Git 存储库时,请记住,在您 git push 那些提交到另一个存储库之前,您的Git 是唯一的有你的 new 提交。这使得通过用一些新的和改进的版本(例如,git rebase -i 或git commit --amend)替换一些提交来“重写历史”变得容易(并且可以)。一旦你将提交发送到别处,你仍然可以用新的和改进的版本替换提交,它只是 other Git 现在有你之前发送的提交,所以这些事情变得更难——有时更难。

【讨论】:

  • 感谢您的长时间回复。值得赞赏。但是在第 10 步中,我又回到了主服务器上,我没有看到 file1.txt。我想,当我回到主分支时,我会看到主分支上的所有内容。从未删除过 file1.txt 或将其合并回 master,因此任何从 Master 签出的操作仍应看到 file1.txt。
  • 您的第 10 步是运行 ls。这不是 Git 命令,所以它不会做任何 Git 式的操作,它只是向您显示工作树中的内容。由于第 8 步和第 9 步对您的工作树没有任何影响,因此此后没有任何变化。
  • 这里的关键特性(或者,对你来说,bug)是git checkout 附加 HEAD 并更新索引但是如果不需要修改 索引中的文件副本,它单独保留索引和工作树副本。您可以深入探索这个奇怪之处here。由于master 和myBranch,在这个特定的时刻,都指向same 提交,索引中的文件不需要更新。您可以随意更改分支名称:Git 将 nothing 对其索引执行任何操作,因此不会对您的工作树执行任何操作。
猜你喜欢
  • 2023-03-02
  • 1970-01-01
  • 1970-01-01
  • 2013-02-21
  • 2017-05-29
  • 2014-09-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多