我正在开发一个小的网络应用程序来组织讲义。该应用程序和一些虚拟预览内容托管在Gitlab上,可通过Gitlab页面访问。它看起来像这样:
project-name/web <- the actual code
project-name/tex <- dummy content
在我的本地机器上,有适当的内容,以及更多的内容文件夹,所有这些都是未跟踪的,因此不存在于 Gitlab 存储库中,因为这些是不应该公开访问的讲义。它看起来像这样:
project-name/web
project-name/tex <- dummy and proper content
project-name/folder1 <- further content
project-name/folder2 <- further content
现在我想在我的Raspi上托管具有适当内容的应用程序(使用nginx)。我在 raspi 上创建了一个(裸)git 存储库,将完整的项目文件(包括适当的内容(所有文件夹))添加到该存储库中,并设置了一个 git 钩子以将其部署到 nginx 服务器,即将文件复制到/var/www/html 并运行一些 PHP 脚本,这也是必要的。
但是现在我有两个存储库,Gitlab和Raspi,并且需要对代码进行两次所有更改。我研究了如何组合两个存储库,并得到提示,可以将两个存储库通用的"web"文件夹添加为 Raspi 存储库的子模块,然后对 Gitlab 存储库进行代码更改并将它们拉入 Raspi 存储库的子模块。但它并不完全有效,因为"web"是 Gitlab 存储库的子文件夹,而不是整个存储库。所以人们向我指出稀疏提交以只挑选一个子文件夹,但这保留了文件夹结构,因此也无法正常工作。
我对 git 不是很有经验,只知道非常基本的命令,那些子模块和稀疏提交的事情对我来说似乎相当复杂,我无法判断它们是否适合解决问题。
我很确定我的情况并不少见,但我仍然未能找到合适的解决方案,因此非常感谢任何阅读提示!
Git 不存储文件夹。
从某种意义上说,Git 甚至不存储文件。 Git 存储的——无论如何,在你将使用它的级别——是一个大的提交数据库,1加上一个较小的名称数据库。 存储文件的是提交。 这似乎是一个微不足道的差异,但它确实是差异,也是整个事情的关键。
合并两个 Git 存储库包括获取两个原始存储库中的所有提交并将它们放入一个大的组合堆中。 为生成的扩展数据库构建所需的名称集通常是主要问题,但是您跳过了它,转到了自己发明的第二个问题。 正如我们将在最后看到的那样,这可能毕竟不是您想要的。
无论如何,这里你需要知道的第一件事是提交是什么和做什么,因为这是你可以实际使用Git 本身的级别。 让我们从一个简单但烦人的事实开始,即每个提交都有一个唯一的哈希ID,一大串丑陋的字母和数字,如9fadedd637b312089337d73c3ed8447e9f0aa775
。 实际上,这是提交的真实名称:这是 Git 在其大数据库中查找对象的方式。
每个提交都会存储一组文件:没有文件夹,只有文件。 与提交一起存储的文件(可以说是提交的主要数据)采用特殊的只读、仅 Git 压缩格式。阿拉伯数字提交及其文件始终被冻结,因此为了使用或更改它们,Git 必须提取它们(我们稍后会谈到)。 这些在时间上形成了一个快照,就像它一样:在你提交这个文件时,你的文件看起来像这样。
除了此快照之外,每个提交还包含一些元数据,例如谁制作了它,何时以及为什么。 这些元数据大部分供人类使用,但有一部分是供 Git 本身使用的:每个提交都存储其直接父提交的原始哈希 ID 列表。 大多数提交只有一个父级。 当我们有这样的单父提交时,它们会形成一个向后看的提交链:
... <-F <-G <-H
这条链最终以最后一次(最近的)提交结束(在右边,这里)。 它有一些丑陋的哈希ID,但我只是用字母H
来代替那个哈希ID。 提交位于 Git 的大数据库中,可通过该哈希 ID 检索。 提交里面是其父G
的哈希ID,所以给定提交H
,Git可以找到并检索G
。G
当然有父F
,所以现在 Git 可以得到F
,它有一个父级,依此类推。 这可以追溯到时间,最终回到第一次提交,这是第一次提交,它只是没有父级。
分支名称仅保存上次提交的(单个)哈希 ID。 因此,如果此存储库中只有八个提交A
到H
,并且只有一个分支名称master
,我们有:
A--...--G--H <-- master
作为整个存储库。 这八个提交中的每一个都有其所有文件的快照。 Git 将向你展示任何一对提交之间的更改,方法是将提交及其父文件提取到临时区域(在内存中),并查看哪些文件是相同的——哪些 Git 根本不会说什么——哪些是不同的。 对于那些不同的,Git 会给你一个配方,你可以通过它修改早期的提交,将其转换为后期提交。
要添加新提交,您将:
让 Git 将分支的最后一次提交提取到工作区:这是您的工作树或工作树。此时,Git 还将冻结格式、压缩和 Git 化文件的副本放入 Git 的索引中。3最后一次提交现在是当前提交,您使用的分支名称(例如
git checkout master
中的master
)是当前分支。
随心所欲地使用工作树副本。
使用
git add
将更新的工作树文件复制回 Git 的索引。运行
git commit
. 这会从您和您的设置、当前日期和时间等中收集一些元数据;使用当前提交作为新提交的父提交;使用 Git 索引中现在的任何内容作为新的永久冻结文件,并写出一个新的提交。 新提交的写入为其提供了新的唯一哈希 ID。
Git 现在将新提交的哈希 ID 存储到当前分支名称中。 所以master
过去指向H
的地方,现在它指向一个新的提交,我们称之为I
,它指向H
:
...--G--H--I <-- master
这就是树枝的生长方式。
请注意,I
具有每个文件的完整快照,就像H
一样。 这些是你稍后将在工作树中获得的文件,如果你签出提交I
。
1从技术上讲,这是 Git 的对象数据库,如果您使用带注释的标签,有时您也可以直接与标签对象进行交互。
2从技术上讲,Git 存储在提交中的是树对象的哈希 ID。 树对象具有条目,每个条目提供文件名或文件的一部分、其模式以及保存文件内容的blob 对象的哈希 ID。 树对象可以允许 Git 存储文件夹,但 Git 通过 Git 的索引构建和使用这些树对象,它只允许文件条目,因此 Git 最终永远不会存储文件夹。
3脚注 2 中提到的索引是 Git 构建下一次提交的方式。 它有一些额外的用途,我们不会在这里详细介绍。 它并不真正存储文件的副本:它存储模式、文件名(完整路径,如path/to/file
)和 Git blob 对象哈希 ID。 但是,在这个级别,您可以将索引视为保存冻结格式的文件副本,准备进入下一次提交。
合并存储库
如果要将两个存储库合并为一个大型存储库,请:
也许,从克隆两个存储库之一开始,以便您正在使用副本以防万一搞砸。 这会为您提供所有提交的副本。 作为一个克隆,这个副本有自己的分支名称:原来的分支名称都改名了,现在
origin/master
、origin/dev
等,而不是master
和dev
等等。克隆过程采用一个名称(
git clone -bbranch
)作为它应该为您创建的名称。 如果你不给它一个,它会询问origin
Git 它推荐哪个分支。 通常它建议master
. 因此,您的克隆通常以一个master
分支结束,您的 Git 将其设置为指向您的 Git 根据其master
设置您的origin/master
的相同提交。(回顾上面的图纸,看看这如何使您的
master
等于他们的master
。让 Git 将第二个存储库中的所有提交添加到此副本中。 和以前一样,让您的 Git重命名其所有分支。 我们稍后将看到它是如何工作的。
分支名称以及 Git 的其他名称到哈希 ID 映射条目构成了 Git 存储库中的另一个数据库。 我们在上面看到了分支名称如何选择提交链中的最后一个提交,以及克隆如何重命名其他 Git 的分支名称。 这些origin/*
名称是远程跟踪名称,4它们只是记住其他 Git 的分支名称指向的位置,上次我与其他 Git 交谈并获取其分支名称指向的提交列表。
要从另一个 Git 获取提交,您需要一个 URL(或者有时是计算机上的路径名,但我们只是在这里假装这是一个 URL)。 当你克隆一个 Git 仓库时,你给 Git 一个 URL:例如git clone ssh://git@github.com/user/repo
。 你的 Git:
- 创建一个新的空目录(通常 - 您可以将其指向现有的空目录)并输入该目录以执行其余步骤;
git init
:在此处创建一个新的空 Git 存储库;git remote add ...
:添加一个远程名称,默认origin
,存储URL;- 您是否要求的任何额外配置; 在新
- 遥控器上运行
git fetch
;和 - 最后,运行
git checkout
以创建和签出master
或您选择的任何名称。
第 5 步让你的 Git 使用存储的 URL 调用另一个 Git。 另一个 Git 在列出他们的所有分支名称和提示提交哈希 ID(以及标签名称和其他名称,但我们将忽略这种复杂性)后,移交它拥有的任何提交,而你的 Git 没有 - 这是他们的所有提交。
此步骤将复制其所有提交并创建或更新您的远程跟踪名称。 因此,如果我们想添加来自另一个Git 的所有提交,我们只需要运行:
git remote add <name> <url>
你选择一些名字——second
、another
、随心所欲——和网址。 您的 Git 会添加一个新的远程,存储此 URL。 然后,您可以运行:
git fetch <name>
这让你的 Git 调用另一个 Git。 它们列出了它们的分支名称(以及我们忽略的其他名称)和最后提交的哈希值,您的 Git 会要求这些提交以及这些提交作为父级的所有其他提交,递归地,一直回到该存储库中的第一次提交。
假设你为第二个 Git 使用了名称two
。 您现在有了表单two/*
的远程跟踪名称,例如two/master
和two/develop
等,以查找该 Git 中每个分支名称中的最后一个提交。
现在由您进行新的提交,以组合这两个存储库中每个存储库中您喜欢的任何文件。
4Git 调用这些远程跟踪分支名称,人们通常将其缩短为远程跟踪分支。但是,它们根本不是分支名称,因为如果您将它们提供给git checkout
或git switch
,您最终会进入 Git 所谓的分离 HEAD模式:不在分支上。 我发现只称它们为远程跟踪名称不那么令人困惑:它们为您跟踪远程的分支名称,所以它们是名称,它们做远程跟踪的事情,所以这就是我们应该称呼它们。
插曲
请注意,存储库中的提交是历史记录。 没有文件历史记录,因为实际上没有任何文件。 只有提交,它们存储快照并具有链接。 稍后的提交指向较早的提交。 历史记录之所以存在,是因为后面的提交指向较早的提交。 Git 可以从末尾开始,然后向后工作,这就是历史。
名称查找提交。 每个名称都会找到一个特定的提交。 如果你从那里向后工作,你会得到历史。 如果你只是呆在那里,那么,你有一个提交,提交有文件,你可以提取文件并使用它们。
进行组合提交
给出两个这样的分支提示:
...--o--J <-- branch1
...--o--L <-- branch2
您可以选择这两个提交之一,例如J
,按其分支名称git checkout branch1
并运行git merge branch2
。
理想情况下,这两个分支实际上从一个共同的起点开始:共享提交,位于两个分支上。 也就是说,这真的看起来像:
I--J <-- branch1 (HEAD)
/
...--G--H
K--L <-- branch2
其中提交H
是两个分支上明显的最佳共享提交。
我在这里引用HEAD
是 Git 如何记住你对哪个分支名称进行了git checkout
:Git 将特殊名称HEAD
附加到一个分支。 这也是 Git 提取到 Git 索引和您的工作树中的文件,即,这些是您现在可以从提交J
实际查看和使用的文件。 该名称HEAD
同时提供当前分支名称,并通过指向提交的分支名称间接提供当前提交。
您现在运行:
git merge branch2
和 Git 定位提交L
,branch2
指向。 合并代码现在从这两个提交(J
和L
)向后工作,以自行查找提交H
。 此提交H
是两个分支的合并基础。
为了完成合并操作(合并为动词,我喜欢这样称呼它),Git 现在运行两个比较,从提交中的快照开始H
两次。git diff
命令让我们运行相同的比较,从而考虑 Git 看到的内容:
git diff --find-renameshash-of-Hhash-of-J
找到我们在branch1
上更改的内容;git diff --find-renameshash-of-Hhash-of-L
找到了他们在branch2
上更改的内容。
合并操作现在合并了两组更改。 无论我们对H
中的文件做了什么,Git 都可以再次执行此操作,还可以添加他们对H
中同一文件所做的任何操作。 对每个文件执行此操作,并执行任何整个文件更改(例如添加全新的文件,如果我们或他们这样做了),则会将H
中的快照修改为新快照,随时可用。
如果一切顺利,Git 现在将进行一个新的合并提交,我们可以将其绘制为提交M
:
I--J
/
...--G--H M <-- branch1 (HEAD)
/
K--L <-- branch2
Git 像往常一样branch1
调整名称,以指向新的合并提交M
,它像往常一样具有快照。 唯一不"像往常一样"的是,新提交M
有两个父母,J
和L
。
这意味着,如果我们尝试查看M
以查看更改的内容,通常的技巧 - 比较M
与其父级 - 不起作用。没有父母;有父母,复数。 Git 为此做什么取决于您使用什么命令来查看M
,但很多时候,它只是放弃并且根本不显示任何差异! 通常很难看到合并。 从技术上讲,合并也可以有两个以上的父项。
在遍历历史记录时,Git 通常会沿着合并的一条"腿"或"侧"向下,或者向下移动所有"腿"。 同样,我们不会在这里讨论所有细节:它变得有点复杂,非常快。 不过,一个简单的git log
会沿着两条腿向下,按某种顺序,一次一个提交。
无论如何,这里真正的重点是合并提交M
将两个历史重新绑定为一个。 从branch1
,我们访问提交M
;然后按某种顺序提交J
和L
,并I
和K
。 通常我们会在返回提交H
之前先击中所有这些,在那里事情变得简单,然后我们像往常一样继续访问提交G
、F
等。 所以所有这些提交现在都在branch1
. 我们甚至不再需要branch2
这个名字:它标识提交L
,但是如果我们沿着它的第二条腿走下去,M
会达到L
。 如果需要,我们可以立即删除branch2
名称。5
5如果我们不删除branch2
,我们可以在branch2
上进行更多的提交,而这些提交不会在branch1
上。 稍后,我们可以再次git checkout branch1
git merge branch2
。 这一次,最好的共享提交将是提交L
。 这就是长时间运行的重复合并操作的工作方式:合并会更改一个分支上可访问的提交集,从而使将来合并到该分支中的工作效果更好。 至少,我们希望它更好:有时它只是不同。
您的情况有点不同
此时您可能想要使用:
git checkout master
git merge two/master
例如,进行组合提交。 但是在现代 Git 中,你会得到一个错误:
fatal: refusing to merge unrelated histories
这里的问题是没有共享提交。 旧版本的 Git 无论如何都会进行合并,或者至少尝试合并,使用一个没有文件的假提交:Git 是空树。
您可以自己启用此功能,就像您有一个旧的 Git 一样:
git merge --allow-unrelated-histories two/master
Git 现在将使用假空提交作为公共起点。 两个分支提示提交中的每个文件都将是"新添加的"。 如果所有文件名都不同,则合并将通过将所有文件放入新提交中自行成功。
如果这不是你想要的 -它不是 - 你可能希望确保 Git不会自行提交,方法是使用:
git merge --allow-unrelated-histories --no-commit two/master
这可确保 Git 在合并未完成的情况下停止,就像 Git 自行合并两个提交时出现任何问题一样。
但是,如果任何文件名发生冲突,无论如何您都会得到"添加/添加冲突",Git 将停止。 这里的问题是 Git 不知道要使用哪个文件。 它应该使用通过HEAD
/master
选择的当前提交中的那个吗? 还是应该使用通过two/master
选择的另一个提交中的一个?
现在,您的工作是为合并快照提供正确的文件集。 你可以在工作树和 Git 的索引中执行此操作,在那里你可以查看和处理文件,在 Git 的索引中(你不太清楚:git status
告诉你 Git 索引中的不同之处,而不是 Git 索引中的内容,因此它将文件的索引副本与其他副本进行比较)。
你可能想从 Git 的索引中git rm
或git rm --cached
一些特定的文件(我们在这里不担心这个问题),但大多数情况下,你需要修复工作树副本,然后git add
工作树副本,让 Git 将正确的文件复制到它的索引中。 当你这样做时,Git 会将每个冲突的文件标记为已解决:git status
会将它们移出特殊(仅合并)冲突部分。
您应该知道,git status
会告诉您将通过以下方式提交的内容("暂存提交"):
- 将当前(
HEAD
)提交的冻结文件与索引中的冻结格式文件进行比较 - 对于每个相同的文件,什么都不说
- 对于每个不同的文件,请提及文件名
因此,如果HEAD
master
也是origin/master
,您可以通过查看您拥有的另一个克隆(这只是您的第一个原始存储库)来了解这些文件,并查看哪些文件被签出那里。
解决所有合并冲突后,git status
还会告诉您工作树中的内容与 Git 索引中的内容不同。 这些是不是为提交而暂存的更改。
要完成合并并进行新的合并提交,将两个历史记录绑定在一起,您现在只需运行:
git merge --continue
或:
git commit
(merge --continue
只是检查是否有合并完成,然后运行git commit
,所以它们在这种情况下做同样的事情)。
新合并提交快照中的文件是此时 Git 索引中的文件。 因此,所有这些工作只是将正确的文件放入索引中。 这就是它的全部意义所在。 Git 存储提交,而不是文件;提交包含由 Git 索引中的任何内容组成的快照文件;您使用的命令操作索引,并进行新的提交。
如果您不想合并存储库,则不必合并存储库
如果您只想从某个地方获取一堆文件,并将它们添加到某个现有或新克隆中的新提交中,只需尽一切努力获取文件即可。 如果需要,克隆存储库,或切换到现有克隆。 使用您喜欢的任何命令将文件复制到适当的位置。 使用git add
将这些文件复制到 Git 的索引中,其中它们的路径名为folder1/file
,因为在您的工作树中,您有一个包含名为file
的文件的folder1
。
索引中包含正确的文件集后,运行git commit
以在当前分支上进行新提交。 Git 将收集元数据,使用新快照写出新提交,并将新提交的哈希 ID 存储在当前分支名称中。 新提交将指向上一个提交。 这就是 Git 的全部意义所在:添加新提交。 我们通过分支名称找到它们;我们通过git diff
-ing来比较它们;我们做其他更花哨的 Git 命令,用它们做其他事情。 但重要的是提交。
重要的是提交
请注意,由于提交很重要,因此如果您愿意,可以使用git merge
将两个历史记录绑定在一起,而不必担心合并中的快照。 然后,您可以进行第二次提交,以修复合并中出现的问题。
例如,如果 Git 可以自行合并两个原本不相关的历史记录(也许使用--allow-unrelated-histories
),但这会保存太多文件,那又怎样? 你可以让 Git 这样做,然后删除不需要的文件并进行第二次提交。
Git 提交共享他们的文件。 每次提交都是完全只读的,永远冻结。 你要么有提交,要么没有,如果你有提交,它有它的所有文件。 如果其文件与上一次提交的文件匹配,Git 知道在两次提交之间共享文件是安全的。 只有一个实际的冻结副本。
因此,如果您采用两个不同的存储库并将它们的提交合并到一个存储库中,则您已经拥有所有提交和所有文件。 进行合并提交,如果您签出它,则会得到太多文件,不会占用额外的空间 - 好吧,只有一点点空间用于合并提交本身。 删除一堆文件的后续提交会占用一点空间,以记录新提交,该提交仅重用文件的某些子集。
签出合并后提交的提交,仅提取到您的工作区中,仅显示该提交中的那些文件,因此无论如何您都不会看到额外的文件。 它们将出现在您的历史记录中,但无论它们是否在您的合并中,它们都会在那里。
选择权在你:Git 会存储你告诉它的任何内容。 您将拥有您拥有的提交,无论这些提交是什么,并且您无法更改任何现有提交,但您可以选择哪一个是您的最后一次提交。 您甚至可以创建一个新历史记录,其中包含一个包含正确文件的提交:
...--W--X--Y <-- master
Z <-- new-history (HEAD)
Z
没有父级的地方。 如果现在删除找到所有其他提交的所有名称,例如master
:
git branch -D master
给:
...--W--X--Y ??? [can't find Y any more!]
Z <-- new-history (HEAD)
Git 最终会删除所有其他提交。
为了使此操作更快,git clone
此存储库; 您的克隆不会有origin/master
,只有一个origin/new-history
。 您现在可以在新克隆中调用该master
,该克隆仅包含一个具有正确文件的提交。 但是,它的历史记录不能与原始存储库的历史记录相关联。
若要实现此状态,如果需要,请参阅git checkout --orphan
。 您可以运行:
git checkout master
git checkout --orphan new-history
git commit
您将获得这个没有父级的新Z
提交,其快照与 Git 的当前提示提交相同master
. 索引没有改变:git checkout master
填充了它,但git checkout --orphan new-history
没有清空它。
这通常不是正确的做法,但如果你了解它是如何以及为什么工作的,你现在就会得到很多 Git 的内容。