Git:如何在我的项目中设置一个与远程分支相同的文件夹?
Posted
技术标签:
【中文标题】Git:如何在我的项目中设置一个与远程分支相同的文件夹?【英文标题】:Git: how do I set a folder in my project equal to that in a remote branch? 【发布时间】:2021-11-30 00:24:00 【问题描述】:我有一个feature/mine
分支,我想合并到feature/other
,但是在开发过程中的某个地方,对仍在feature/mine
中且不应在feature/other
中的特定文件夹进行了更改。因此,我的 PR 中的差异显示了 src/some-folder-I-dont-want-to-change
中的更改,但这些更改是在提交树的深处进行的。我不能简单地还原一些提交。
我想要的只是将我的src/some-folder-I-dont-want-to-change
设置为feature/other
的src/some-folder-I-dont-want-to-change
。有没有办法做到这一点?
我尝试了git checkout feature/other -- src/some-folder-I-dont-want-to-change
,但这只是从feature/other
添加文件,它不会删除我的分支上但不应该删除的文件。
【问题讨论】:
【参考方案1】:听起来您正在寻找git restore
,请参阅例如https://***.com/a/15404733/6060876.
我相信对于您的用例,您需要编写如下内容:
git restore --source=feature/other --staged --worktree -- src/some-folder-I-dont-want-to-change
【讨论】:
【参考方案2】:TL;DR
您可能只想从特定提交中检出一些文件,然后再次提交。然后,从合并基础到您的分支提示的差异将不包括对这些文件的任何更改,或者将对这些文件进行 same 更改,具体取决于您的确切需要。这可能会引发未来的问题,因此您需要注意这一点。
您可能希望使用git rebase
来替换您现有的提交,并使用一些新的和改进的提交。这比较复杂,可能会引发一些眼前的问题,但很可能会解决未来的问题。
长
您认为您的项目是一个简单的文件集合,但您使用的是 Git。 Git 不是关于文件; Git 是关于提交。一个提交包含个文件——事实上,每个提交都有一个每个文件的完整快照——但它是一个提交,而不是一些文件。然后有些人认为 Git 是关于分支的,但这也不对。分支帮助我们(和 Git)find 提交。但Git 是关于commits 的。所以你需要在这里考虑提交,而不是文件或文件夹。
这并不能解决您的问题,但您需要了解所有相关信息才能解决您的问题。所以请继续阅读。
关于提交的知识
在 Git 中,提交是一个编号实体。每个提交都有一个唯一的编号,Git 将其称为 hash ID,有时也称为 object ID。但是,这些并不是简单的计数:它们不会提交 #1,然后是 #2,#3,等等。相反,每个提交的唯一编号是巨大的——介于 1 和 2160-1 之间——而且显然是随机的,通常用 hexadecimal 表示,例如 cefe983a320c03d7843ac78e73bd513a27806845
子>.
这些数字对人类来说毫无用处(因此我们大多不使用它们——如果需要,您偶尔会使用带有剪切和粘贴功能的数字),但它们是 Git 实际查找提交的方式。所以 Git 需要它们。我们稍后会看到如何避免输入它们。
这些编号的实体中的每一个——这些提交——都包含两个东西:
每次提交都有每个文件的完整快照。此快照采用特殊的、只读的、仅限 Git 的、压缩和去重的形式。提交中的文件不能被任何东西使用但 Git 本身。
与快照不同,每个提交都有一些元数据,或者关于提交本身的信息:例如,谁做了它(姓名和电子邮件)以及什么时候。
对于 Git 本身来说至关重要的是,每个提交都包含一些较早提交的原始哈希 ID。大多数提交——Git 调用的那些普通提交——持有前一个提交的哈希 ID。
这一切对你来说意味着你只需要告诉 Git latest 提交的哈希 ID。假设我们画了这个,使用大写字母代表实际的提交哈希 ID,这太难看了。让我们用H
代替最新的H
ash ID:
H
在H
的元数据中,Git 存储了一些较早提交的原始哈希 ID。让我们称之为提交G
:
G <-H
通过读取H
,Git 将能够获得G
的哈希ID,从而能够读取G
。所以我们说H
指向 G
。
但G
本身也是一个提交,因此它具有一些较早提交 F
的哈希 ID,而后者又具有另一个更早提交的哈希 ID:
... <-F <-G <-H
因此,鉴于 最新 提交的哈希 ID,Git 可以轻松找到之前的每个提交,一直追溯到过去。 Git 只是简单地遵循向后的内部箭头,一次一步。
由于H
和G
都具有完整的快照——在Git 的特殊去重格式中,同样如此——Git 可以轻松地检索G
和H
并比较它们,查看更改了哪些文件(如果有)。这让 Git 知道你做了什么,如果你做了 H
,并向你展示更改。 Git 没有存储这些更改,它只是在您要求它显示H
时计算它们,理论上显示自G
以来的更改更有趣而不是显示H
的原始内容。
事实上,只读的不仅仅是快照:any 提交的所有部分都是完全只读的。因此,一旦提交,这些内部箭头就会永远设置为这种方式。
分支和其他名称
不过,要做到这一切,Git 需要知道那个 最新 提交的哈希 ID。这是分支名称进入图片的地方。我们有点懒得画从提交到提交的箭头,然后画成这样:
...--F--G--H <-- main
这里 name main
指向提交 H
,H
指向 G
。然而,与提交中的箭头不同,来自分支名称的箭头可以改变:如果我们真的想的话,我们可以让main
指向G
。 (这就是git reset
的大部分内容,虽然我不打算在这里介绍。)
我们可以制作更多的分支名称。 Git 对我们分支名称的唯一限制是它们必须指向实际提交。所以我们可以有两个名称都指向H
,像这样:
...--G--H <-- develop, main
现在我们有多个名称,我们需要一种方法来记住我们正在使用的名称。现在这两个名字都找到了提交H
,所以从某种意义上说,我们使用哪个名字并不重要,但我们即将改变它。因此,为了记住我们使用的名称,我们将在一个分支名称上附加特殊名称HEAD
,全大写,如下所示:
...--G--H <-- develop, main (HEAD)
如果我们现在使用git switch develop
或git checkout develop
,我们会得到:
...--G--H <-- develop (HEAD), main
我们仍在使用提交H
,我们现在只是通过名称develop
这样做。
除了 branch 名称,Git 还允许我们使用 tag 名称 和 remote-tracking 名称。这些通常也指向提交(尽管标签名称通常通过额外的 Git 对象这样做,以便能够存储注释)。过会儿我会再来讨论这个的。
添加新提交:Git 的索引或暂存区
当我们进行新的提交时,我们:
-
使用
git checkout
或git switch
选择起始提交;
修改一些文件并对它们运行git add
;
运行git commit
。
您可能想知道为什么我们必须一遍又一遍地运行git add
:Git 不是第一次得到关于这些文件的备忘录吗?
这里的答案与 Git 的 index 有关。 Git 的这一部分非常核心和重要,Git 迫使您了解它。其他版本控制系统可能有类似索引之类的东西,但是把它隐藏得很好,这样你就不用关心了,但是 Git 在这里非常坚持。 (它实际上有 三个 名称非常重要:它不仅仅是索引或暂存区域:Git 有时称它为 cache。这可能是三个名称中最糟糕的一个,现在大多只出现在 git rm --cached
这样的标志中。)
现在,我们已经知道提交内部的快照是只读的,实际上只有 Git 可以读取这些文件。因此,Git 将不得不将提交中的文件复制 成更可用的形式。这些可用的副本存在于 Git 所称的工作树中:你工作的地方。
这很简单:“签出”某些提交意味着提取其文件。不简单的是,Git 这样做的方式分为两个步骤:
首先,Git 将文件“复制”到其索引中,从其索引中删除先前签出的提交中的文件。
然后 Git 将文件从索引复制到您的工作树。
第一步中的“副本”一词用引号引起来,因为 Git 放入索引中的是 Git 的压缩和去重格式。由于这些文件都来自某个提交,因此它们会自动重复。这意味着它们不占用空间。1但是,它们仍然表现得像副本。
您工作树中的副本是普通文件,采用您计算机的普通格式,解压缩,因此它们确实是 副本。这就是为什么你必须经常运行git add
。
git add
所做的是读取工作树版本,对其进行压缩和 Git-ify,然后检查它是否重复。如果它是重复的,那么存储库中已经存在数据的副本,Git 会重新使用该副本。如果它不是重复的,则压缩数据现在已准备好成为第一个副本,Git 使用该副本。无论哪种方式,Git 现在更新文件的索引条目并且文件已准备好提交。
所以git add
真正的意思是更新我提议的下一次提交,而索引实际上是你提议的下一次 提交。它开始匹配 当前 提交,当您向它发送 git add
文件时,您会更新建议的提交。
这使git commit
的工作更快更轻松。当你运行git commit
:
Git 为新提交收集任何所需的元数据,例如您的姓名和电子邮件地址以及日志消息。 Git 使用 当前提交,通过读取 HEAD
来查看哪个分支名称是当前分支,并读取分支名称以找出提交的哈希 ID,作为 parent 用于新的提交。
Git 使用步骤 1 中的元数据将索引中的建议提交转换为实际提交。Git 将所有这些作为新的提交对象写入大提交对象-数据库。由于这是一个 new 提交,它会获得一个新的、唯一的、随机的(但不是随机的)哈希 ID:这是 Git 第一次真正找到哈希 ID .由于哈希 ID 取决于所有数据——不仅包括您的姓名和电子邮件地址,还包括您进行提交的确切 秒 - 没有办法提前知道哈希 ID 是什么。
作为最后一个技巧,git commit
将新提交的哈希 ID 写入当前分支名称。
结果是我们从:
...--G--H <-- develop (HEAD), main
到:
...--G--H <-- main
\
I <-- develop (HEAD)
commit I
是我们的新提交。
如果我们现在运行 git checkout main
,Git 将从它的索引和我们的工作树中删除提交 I
中的所有文件——它们将被安全地永久保存在其中2提交——并从提交H
中保存的文件中填写它的索引和我们的工作树。3
1每个索引条目都有一些空间用于文件名、模式、缓存数据和哈希 ID。实际数量有所不同,但在很多情况下平均每个文件不到 100 字节。
2永远,或者只要提交本身存在,就是这样。
3Git 在这里玩了一堆速度和聪明的技巧:它可以很容易地分辨出H
和I
中的哪些文件不同,并且可以跳过没有不同的任何文件的删除和替换。当在指向同一个提交的分支名称之间切换极端时,这使得git checkout
或git switch
非常快,因为实际上没有任何变化,除了HEAD
的绑定。这也意味着我们可以切换带有未提交工作的分支,因为 Git 不必换出文件。
树枝如何生长;合并
这向我们展示了树枝是如何生长的。我们可以从:
...--G--H <-- main
从这里,我们创建两个新的分支名称,feature1
和 feature2
或 feature/tall
和 feature/short
或其他。我将只使用br1
和br2
:
...--G--H <-- main, br1, br2
我们选择 br1
为“开启”,检查它,然后进行两次新的提交:
I--J <-- br1 (HEAD)
/
...--G--H <-- main, br2
现在我们检查 br2
并进行两个 other 新提交:
I--J <-- br1
/
...--G--H <-- main
\
K--L <-- br2 (HEAD)
稍后我们将不再费心使用名称main
:这并不重要,因为对 Git 来说重要的是提交。请注意,通过H
的提交都在所有三个分支上,而提交I-J
仅在br1
上,而提交K-L
仅在br2
上。
现在让我们查看br1
并运行git merge br2
,并快速了解git merge
的工作原理:
I--J <-- br1 (HEAD)
/
...--G--H
\
K--L <-- br2
git merge
命令需要结合工作。为此,它必须找到一个共同的起点: 两个 分支上的最佳提交。 Git 会做通常的从头开始的工作来做到这一点。 (从技术上讲,Git 使用 Lowest Common Ancestor 算法,使用 DAG 的扩展。)在这种情况下,最好的共享提交是显而易见的:它是提交 H
。
为了合并工作,Git 必须找出变化。为此,Git 将一次在两次提交上运行 git diff
。我们开始:
git diff --find-renames <hash-of-H> <hash-of-J> # what we changed
通过比较 H
和 J
中的快照,Git 可以找出我们修改了哪些文件,以及我们对这些文件做了什么。
接下来,Git 重复这个差异,这次是从 H
到 L
:
git diff --find-renames <hash-of-H> <hash-of-L> # what they changed
Git 再次获取已更改文件的列表,以及它们发生了什么。
git merge
现在的工作是合并这些更改。然后 Git 应该将合并的更改应用到 base 版本。
对于我们更改但他们没有更改的文件,这很容易:从我们的提交中获取我们的更改或该文件的版本。换句话说,“来自H
的文件,加上我们的更改,等于来自J
的文件”,因此Git 可以直接从J
获取文件。即使我们从整块布料创建一个新文件,这也适用,例如:如果我们添加了一个文件,而他们没有,Git 可以拿走我们的。
对于没有人更改的文件,Git 可以采用任何版本,因为它们都是相同的。
对于他们更改而我们没有更改的文件,Git 可以获取他们的版本。这也适用于他们删除的文件,我们没有删除,例如:Git 可以删除文件。
只有当我们俩都更改了 same 文件时,它才会变得有点棘手。 Git 现在确实必须将这两组更改结合起来。如果我们都接触了相同的原始行,这可能会产生合并冲突。 (我在这里也跳过了很多挑剔的细节。)
假设一切顺利,Git 只是简单地将我们的所有更改和他们的更改合并,将这些合并更改应用到来自H
的文件,并根据结果进行新的合并提交。这个新的合并提交有一个单一的快照——就像 any 提交一样——但是 Git 没有一个单一的父节点,而是向通常的单一父节点添加了一个 second 父节点。新的合并提交M
指向提交J
,就像常规提交一样,但也指向L
:
I--J
/ \
...--G--H M <-- br1 (HEAD)
\ /
K--L <-- br2
然后,写出合并提交M
,Git 将其哈希 ID 写入当前分支名称 - br1
,其中附加了 `HEAD - 像往常一样,我们的合并完成。
你的情况
我有一个
feature/mine
分支,我想合并到feature/other
,但是在开发过程中的某个地方,对仍在feature/mine
中且不应在feature/other
中的特定文件夹进行了更改。因此,我的 PR 中的差异显示了src/some-folder-I-dont-want-to-change
中的更改,但这些更改是在提交树的深处进行的。我不能简单地还原一些提交。
此时您谈论的是 GitHub 拉取请求,而不是 Git 合并。 GitHub PR 特定于 GitHub。 Bitbucket 也有拉取请求,我是在您的帖子中添加github 的人,所以也许您实际上是在谈论 Bitbucket 拉取请求。幸运的是,虽然有些细节有所不同,但这里的整体设置是相同的。 (GitLab 称他们为 merge requests,同样,在细节上存在一些差异,但总体思路是相同的。)
要制作 GitHub PR,您:
将您的新提交发送到 GitHub 上的某个存储库:GitHub 分支或共享存储库(哪个并不重要);那么 使用 GitHub 网页单击按钮或gh
CLI 脚本来创建拉取请求:这会在 GitHub 上生成测试合并以查看合并是否有效,如果无效,它们会告诉您合并冲突.
要进行该测试合并,GitHub:
找到一个合并基础提交; 做了一个测试合并,大概是有效的; 现在显示您更改了src/some-folder-I-dont-want-to-change
中的一些文件
假设 您 没有自己更改这些文件,这意味着 GitHub 在这里使用的 merge base 使得 common 开始点出现在其他人更改这些文件之前。
也就是说,假设你有:
J <-- feature/mine (HEAD)
/
I <-- feature/third
/
...--H
\
K--L <-- origin/feature/other
您在其中进行了一次提交 J
,该提交更改了不在此 src/some-folder-I-dont-want-to-change
位置的文件。 H
和 J
之间的比较,但是,包括提交 I
。
同时,H
与 L
的比较不包括对 src/some-folder-I-dont-want-to-change
的任何更改。
因此,您的 PR 显示更改为 src/some-folder-I-dont-want-to-change
。
如果您愿意,您可以获取提交 L
(这是他们最近的提交)并以该形式从中提取 src/some-folder-I-dont-want-to-change
中的所有文件,然后提交结果:
J--M <-- feature/mine (HEAD)
/
I <-- feature/third
/
...--H
\
K--L <-- origin/feature/other
现在,H
与 M
的比较显示 没有 更改为 src/some-folder-I-dont-want-to-change
。问题是,你的分支名称feature/mine
现在意味着commit M
,它——通过一次返回一跳——包括commit I
,这意味着您现在已经取消了对 feature/third
的更改。也就是说,您的提交 M
也可能是一个还原。
如果他们(无论他们是谁)接受您更新的 PR,并建议将 M
合并到 L
,结果将包括提交 I
,如果他们使用 MERGE 按钮。 GitHub 在这里有三个不同的可点击网页按钮:
MERGE:这是一个真正的 Git 合并。您现在已经进行了设置,以便 Git 相信提交 I
已正确合并。这意味着无论是谁做了 提交I
都必须重新做他们的工作。这就是未来的定时炸弹。
REBASE AND MERGE:这使得 GitHub 上的 Git 软件将每个提交 in 复制到 PR 到一个新的和假定改进的提交。这将产生类似的效果,尽管它改变了提交I
的人必须处理事情的方式。
SQUASH AND MERGE:这根本不会创建合并。这可以防止这种特殊的定时炸弹。它们用一个提交替换所有“你的”提交——包括你从别人那里继承的提交I
。效果是,无论谁提交了I
,都不必重新提交,因为最后没有人会看到你的提交J
和M
(你将不得不丢弃你的分支,因为任何南瓜)。
了解其运作方式和原因是这里的关键。如果您要设置未来的定时炸弹,那么无论谁做出了您实际上正在还原的提交,都需要知道这一点。如果您使用的是 squash-and-merge 方法(它还有其他缺点),则具有完全消除这种情况的有益副作用。
旁注:origin/feature/other
名称origin/feature/other
是一个远程跟踪名称。在查找提交方面,这与分支名称具有相同的效果。 branch 名称和 remote-tracking 名称之间的主要区别在于后者是您的 Git 为记住其他 Git 存储库的 branch 而创建的名称姓名。您不能切换到远程跟踪名称(git checkout
产生分离的 HEAD,git switch
产生错误)。
当您运行 git fetch
以从其他 Git 存储库获取提交时,您的 Git 会读取其他 Git 的分支名称。为了保留您的分支名称你的——避免用他们的覆盖你的——你的 Git 软件重命名他们的分支名称。然后,您的 Git 会创建或更新这些远程跟踪名称。远程跟踪名称,例如 origin/main
或 origin/develop
或其他名称,其前面有 remote origin
:因此术语 remote-tracking name 。 (Git 文档称这些 远程跟踪分支名称,但我发现这里的词 branch 具有负值:删除它会产生一个改进的术语。)
变基
假设这个图表,或类似的:
J--M--N <-- feature/mine (HEAD)
/
I <-- feature/third
/
...--H
\
K--L <-- origin/feature/other
抓住了真正的问题,你还有另一种选择。您可以将现有的提交 J-M-N
替换为从 H
开始的新的和改进的提交,而不是从 I
开始。 git rebase
命令可以帮助解决这个问题。
为此,您需要运行:
git switch feature/mine
git rebase --onto <hash-of-H> feature/third
如果名称main
指向提交H
,您可以使用:
git switch feature/mine
git rebase --onto main feature/third
您也可以在此处使用提交 I
的原始哈希 ID 而不是名称 feature/third
。总体思路是我们想告诉git rebase
两件事:
将副本放在提交H
之后:这就是为什么我们需要提交H
的哈希,或者名称main
。 找到正确的提交的任何东西都可以在这里工作。
使用当前分支复制提交,但不要复制提交I
或早于I
的任何内容。任何定位提交 I
的内容都足够了:名称 feature/third
可以做到这一点,但提交 I
本身的原始哈希 ID 也是如此。
rebase 命令将从当前提交开始,这里是 N
,然后向后工作,直到它到达要从复制中排除的提交:在这种情况下,提交 I
和更早的提交。这些是复制的候选对象。4 然后它将要复制的提交按正确的顺序排列,将它们的实际哈希 ID 保存在工作列表中。 (交互式 rebase,git rebase -i
,允许您编辑工作列表,但您可能不希望在这里。)
一旦列表准备好,git rebase
使用 Git 的 分离 HEAD 模式来复制每个要复制的提交,一次一个。与其详细说明它是如何工作的——尽管我会在这里说它使用 git cherry-pick
或等效的——我只会画出如果一切顺利的话你得到的最终结果:
J--M--N [abandoned]
/
I <-- feature/third
/
...--H--J'-M'-N' <-- feature/mine (HEAD)
\
K--L <-- origin/feature/other
这个最终结果有 您的提交的副本,但省略了 not-copied I
提交。因此,您不再对 您 未更改的文件进行任何更改:您的三个 - 或无论多少 - 提交都从与 origin/feature/other
相同的 基本提交 开始。因此,如果您现在使用 git push --force
更新您的 GitHub PR,对这些其他文件的明显更改将会消失。
无论某人使用何种合并策略,合并都不会包含对这些其他文件的任何更改,并且不会设置定时炸弹。所以这种方法往往更胜一筹。这就是 git rebase
的用途:设置新的和改进的提交,只做你想做的事。
4此候选人名单通常由以下人员进一步筛选:
删除任何合并提交; 从列表中删除 patch IDs 与上游匹配的提交;和 如果启用了--fork-point
,则使用分叉点技巧。
在这种情况下,这些都不应该有太大的影响。但是,如果您在列表中有合并提交,事情就会变得复杂得多。
【讨论】:
感谢您的来信。很有意思。也许回答这个特定问题的信息有点太多了;这使得在那里找到要点变得有些困难。我会考虑你的建议!以上是关于Git:如何在我的项目中设置一个与远程分支相同的文件夹?的主要内容,如果未能解决你的问题,请参考以下文章