引言
最近我在学习 Git,其中有一章节讲到了当建好本地仓库后,如何创建远程仓库并让本地仓库与远程仓库建立连接。我利用这篇文章记录一下相关知识和实践。读完这篇文章,可以了解本地仓库与远程仓库建立连接的步骤。
背景
我在本地创建了一个代码仓库后,并完成了一些代码开发,现在想要建立一个远程仓库,把本地开发的代码推送到远程仓库,方便他人查看和与他人协作。在本地和远程建立连接的过程中,由于创建远程仓库时默认勾选了一些文件,所以推送时出现了一些报错,本文还会重点说明如何解决这类报错,并顺带介绍几种合并方式。
实战步骤
一、在本地初始化一个项目
# 进入项目目录
cd ~/projects/myproject
# 初始化 git 仓库
git init
# 添加所有文件到暂存区
git add .
# 提交到本地仓库
git commit -m "初次提交"
二、在 GitHub 上创建代码仓库
在 此网页 创建远程代码仓库
三、配置公私钥
具体步骤
- 首先检查是否本地已经有了公私钥
ls -al ~/.ssh
如果是 ls: /Users/用户名xxx/.ssh: No such file or directory 则证明还没有公私钥
- 生成公私钥
ssh-keygen -t ed25519 -C "xxx@qq.com"
接下来:
Enter file in which to save the key (/Users/用户名xxx/.ssh/id_ed25519): 这里告诉公私钥存放的位置,直接按空格即可
Enter passphrase for "/Users/xxx/.ssh/id_ed25519" (empty for no passphrase):
Enter same passphrase again:
以上两个是让输入密码
- 将公钥放入 github
cat id_ed25519.pub 得到:
ssh-ed25519 xxx xxx@qq.com
把得到的内容添加到 Github 的 此网页 中。
- 验证是否匹配成功
ssh -T git@github.com
提示 Hi xxx! You've successfully authenticated 则说明配置成功
核心知识
什么是公钥和私钥
公钥和私钥是基于非对称加密技术生成的一对数学上相关联的密钥。
非对称加密是指加密和解密使用的不是同一把钥匙。
- 非对称加密的两条核心性质:
- 使用公钥加密的内容只能由私钥去解密。
- 使用私钥去加密的内容只能由公钥去验证。
- 公私钥的两种用途:
根据非对称加密的两条核心性质也引出了公私钥的两种用途:
-
加密传输
-
场景:当 A 想给 B 发送一条只有 B 能看的消息时。
-
流程:
- B 生成一对密钥:公钥和私钥 。
- B 把 公钥 公开给所有人。
- A 使用 B 的 公钥 对消息加密。
- 只有 B 的 私钥 能解密这条消息。
-
效果:即使别人截获了密文,也无法解密,因为没有 B 的私钥,保证 保密性
-
-
身份验证 / 数字签名
-
场景:当 B 想让别人确认“这条消息确实是我发的,而且没被改动”时。
-
流程:
- B 使用 自己的 私钥 对消息的摘要(hash)进行加密,生成“数字签名”。
- A 收到消息和签名后,用 B 的 公钥 去验证。
- 如果能验证通过,就说明:
- 这条消息确实是由持有该私钥的人(B)发出的;
- 消息在传输过程中没有被篡改。
-
效果:保证真实性和完整性。
-
从感觉上去理解一下非对称加密的这两条性质并区分一下解密和验证
性质一是使用公钥加密的内容只能由私钥去解密。这是一种“公”向“私”发消息,谁都能够使用公钥对我发送消息,但只有我能够使用私钥解密消息。解密是指从密文中还原出原始数据。
性质二是使用私钥去加密的内容只能由公钥去验证。这是一种“私”向“公”发消息,我用私钥对消息的摘要进行签名,任何人都能够使用公钥对签名进行验证,验证通过就知道消息是我发的。
四、把本地的 Git 仓库和 GitHub 上的远程仓库建立连接
具体步骤
- 将本地的 git 仓库和远程的 git 仓库建立连接
语法:git remote add <远程名> <远程仓库地址>
解释:远程名就是为远程仓库起的一个本地的别名,一般取 origin,远程仓库地址就是远程仓库的 URL
示例:git remote add origin git@github.com:xxx/git_learning.git
- 检验当前项目是否关联上了远程仓库
语法:git remote -v
解释:-v 是 -verbose 的简写
- 尝试向远程仓库推送所有分支
语法:git push -u <远程仓库名> --all
示例:git push -u origin --all
解释:第一次推送使用 -u,效果是之后再次推送时直接使用 git push 即可,不用再加远程仓库名字。
-all 的意思是推送所有的本地分支
解决“向远程仓库推送所有分支”报错的问题
报错如下:
error: failed to push some refs to 'github.com:wdmlab/git_learning.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
这个错误的意思是本地的 main 分支并没有 LICENSE 文件的提交历史,但是远程 main 分支有 LICENSE 文件的提交历史,两条分支就像这样:
本地 main: (A_local)───B───C
远程 main: (A_remote)───D ← 这个D可能是LICENSE
本地 main 分支的提交历史 ≠ 远程 main 分支的提交历史,git 会认为我的提交会覆盖别人的提交,所以拒绝推送。
所以要让本地 main 分支也有远程 main 分支的这次提交,就要从远程拉取最新的 main 分支并且与本地的 main 分支合并。
git pull origin-git-learning main --allow-unrelated-histories
解释:两个分支之间没有任何交集,如果想让两个不相关的提交历史合并的话,就需要加上 --allow-unrelated-histories
执行完这条语句后还是会出现如下提示:
hint: You have divergent branches and need to specify how to reconcile them.
hint: You can do so by running one of the following commands sometime before
hint: your next pull:
hint:
hint: git config pull.rebase false # merge
hint: git config pull.rebase true # rebase
hint: git config pull.ff only # fast-forward only
hint:
hint: You can replace "git config" with "git config --global" to set a default
hint: preference for all repositories. You can also pass --rebase, --no-rebase,
hint: or --ff-only on the command line to override the configured default per
hint: invocation.
解释:
You have divergent branches:git 在比对两个分支时发现这是两个没有关系的分支,没有共同的祖先,因此 git 认为这是两个有分歧的分支。 need to specify how to reconcile them:git 在 git 的配置文件中 git 并不知道需要明确如何合并它们。 git 在这里提供了三种合并方式 merge、rebase、fast-forward,git 让我们选择一种合并方式。
接下来就来解释一下这三种合并方式
- merge
merge合并会创建一个新的“合并提交”把两个分支的修改整合到一起。如下方所示:
合并前:
A --- B --- C ← main
\
D --- E ← feature
合并后:
A---B---C---------M (main)
\ /
D-----------E (feature)
内部发生了什么:
- 找到两个分支的共同祖先
这里 main 分支和 feature 分支的共同祖先是 B(记为 base)
- 做三方合并
git 会使用一个虚拟工作区比较 base → main 和 base → feature 的差异,然后把这两组修改合并到一起,应用到工作区。如果发生冲突需要人工介入。
- 生成新的合并提交
合并成功后生成新的提交,提交的内容是基于共同祖先,对比不同节点的差异形成的新版本的文件。
- rebase
rebase 是把本地分支的提交重新放在新的基底上。示意图如下:
rebase 之前
A --- B --- C ← main
\
D --- E ← feature
如果在 feature 执行 git rebase main
则 feature会变成:
A --- B --- C --- D' --- E' ← feature
内部发生了什么?
- 找到两个分支的共同祖先
这里 main 分支和 feature 分支的共同祖先是 B(记为 base)
- 提取 feature 分支相对于 base 的差异补丁
取到 D 和 E 相对于 B 的修改
- 切换到 main 分支的最新节点,依次重放这些补丁
把 D 的修改重新用到 C 上,生成新的提交 D';把 E 的修改重新应用到 D'上,生成新的提交 E'。
- 更新指针和清理
让 feature 分支指向最新的提交 E',原先的 D、E 提交会被清理。
注意:
如果 feature 分支已经推送到远程了,那么对 feature 分支执行 rebase 之后,是需要再次推送到远程的,而且这次推送需要强制推送。
在远程,origin/feature 分支还指向的是旧的提交 E,而本地是新的:
远程:
A --- B --- C
\
D --- E ← origin/feature
本地:
A --- B --- C --- D' --- E' ← feature
如果直接执行 git push origin feature,Git 会拒绝推送,因为本地的 feature 分支和远程的 feature 并不兼容,本地并不是远程的延续,所以必须执行 git push -f,让远程的 feature 分支强制的指向 E',即让本地的历史覆盖远程的历史。
执行 git push -f 之后,远程的情况:
A --- B --- C --- D' --- E' ← origin/feature
- fast forward
fast forward 是指当目标分支的所有提交都已经包含在源分支中时,Git 不需要生成新的合并提交,而是直接把分支指针向前移动。
远程仓库:
A---B---C (origin/main)
本地仓库:
A---B (main)
如果执行 git pull --ff-only
Git 发现本地分支(目标分支)是落后于远程分支(源分支)的,但没有额外的提交,所以 pull 之后可以直接把指针从 B 移动到 C。
结果:
A---B---C (main, origin/main)
但是当源分支和目标分支有着“非子集”的提交历史,就不能使用 --ff-only 来合并了,比如:
远程仓库:
A---B---D (远程提交)
本地仓库:
A---B---C (本地提交)
本地分支并不是远程分支的提交历史的子集,所以不能直接快进指针
那回到最开始的问题,我要让 git 选择 merge、rebase、fast forward 哪种合并方式呢?
- --merge:会创建一个新的 commit 提交,把两边的历史都保留下来。
- --rebase:把本地提交“重放”到远程的历史之后生成新的提交。(这样会重写本地的历史)
- --ff-only:仅本地分支是远程分支的提交历史的子集时才能适用。(现在两个分支并不是这样的)
所以使用 --merge。
接下来的操作步骤:
设置合并策略为 merge
git config pull.rebase false
把远程分支的代码拉取下来并与本地的分支合并
git pull origin-git-learning main --allow-unrelated-histories
然后再把 main 分支推送到远程
git push origin-git-learning main
总结与复盘
在建好本地仓库和远程仓库后建立连接的思路
- 配置公私钥,让远程仓库知道代码是从“我的”的本地仓库推送上去的。
- 通过 git remote add <远程名> <远程仓库地址> 命令将本地仓库与远程仓库建立连接。
- 通过 git push -u <远程仓库名> --all 命令把本地仓库的所有分支推送到远程仓库。
推送过程中发生错误的处理思路
在推送的过程中可能会发生错误,可能是由于在建远程仓库时在 main 分支上勾选了自动创建某些文件,而这些文件在本地没有,因此本地 main 分支的提交历史和远程 main 分支的提交历史不一样,这里的提交历史不一样是指没有共同祖先,Git 默认不会让不兼容的提交历史强行覆盖到远程分支上,解决方法是先拉取远程分支到本地,与本地的代码合并,然后做到与远程分支有相同的分支历史,然后就可以推送到远程了。
在拉取远程分支到本地分支的过程中包含了远程分支与本地分支代码合并的过程,可以选择 merge、rebase 和 fast-forward,文中详细介绍了三者的特点,最终我选择了 merge。而且需要注意本地 main 分支和远程 main 分支完全不相干,所以拉取时需要加上 --allow-unrelated-histories。
整个过程的图示
- 初始状态
本地 main: B1 -- B2
远程 main: A1
- 拉取远程分支到本地
本地:
A1 (origin/main) 本地的远程跟踪分支指向 A1
B1 -- B2 (main)
注:
origin/main 是 Git 自动维护的一个本地引用,用于记录上次你 fetch 时远程 main 所在的位置。
它不会自己移动,只有在下次执行 fetch 时才会更新。
可以理解为它是上次看到远程分支时的“快照”。
- 拉取到的远程分支的内容和本地分支的内容合并
本地
A1 (origin/main)
\
\
B1 -- B2 -- M (main)
此时本地分支就包含远程分支的所有提交节点了,说明本地同步到了远程的最新状态。
- 但是远程还并不知道本地有一个合并提交 M
因此再将本地的代码 push 到远程,让远程同步到本地的状态
本地
A1
\
\
B1 -- B2 -- M (main)(origin/main)