Git 远程推送与同步指南

首次推送

当你第一次将本地代码推送到远程仓库时,使用以下命令:

1、初始化文件夹,添加git配置

1
git init

2、为git添加一个名为origin的远程地址(仓库)

1
git remote add origin https://github.com/你的用户名/仓库名.git

3、将本地缓存区的代码推送到origin这个远程仓库的main分支

1
git push -u origin main

-u等同于 --set-upstream,它将地址写入项目文件夹的.git/config,它告诉 Git,“以后如果不指定目标,默认就推送到 origin 的 main 分支”。

这样以后的推送只需要输入 git push 即可,无需再敲长命令。


首次拉取

1. 最常用的方式:克隆 (Clone)

这是将远程仓库“搬家”到你本地的最快方式。

1
git clone https://github.com/你的用户名/仓库名.git

git clone 是一个“打包套餐”,它一次性帮你做完了以下三件事:

  1. git init:创建了一个新的文件夹,并初始化了 Git。

  2. git remote add origin ...:自动把远程地址关联好了,并起名为 origin

  3. git pull:把远程所有代码下载下来,并自动切换到默认分支(通常是 main)。

2. 特殊情况:本地已有文件夹,想拉取指定地址

如果你本地已经建立了一个文件夹(甚至已经初始化了),现在想把它和某个特定的远程 URL 关联并拉取代码:

第一步:关联远程地址

1
git remote add origin https://github.com/你的用户名/仓库名.git
  • 注意:如果提示 error: remote origin already exists,说明你之前关联过别的地址。可以使用 git remote set-url origin 新地址 来修改。

第二步:拉取代码

关联后,将代码拉取到本地:

1
git pull origin main
  • 注意:如果本地文件夹里已经有文件,且和远程仓库不一致,可能会报错。此时请参考下文提到的 “报错:Refused / Fetch First” 章节,使用 --allow-unrelated-histories 参数解决。

报错:Refused / Fetch First

报错现象

当你执行 push 时,如果出现以下错误:

! [rejected] main -> main (fetch first)

hint: Updates were rejected because the remote contains work that you do not have locally.

原因

远程仓库比你本地多了一些文件(通常是因为你在 GitHub 建仓库时勾选了创建 README.md.gitignore),即远程仓库上的代码并非本地代码的历史提交。Git 为了防止你覆盖掉远程的内容,阻止了推送。

解决方案:合并(Merge)

我们需要先把远程的东西拿下来(Pull),合并到本地,然后再推上去。

步骤一:确保本地已提交

在拉取之前,必须保证本地所有修改都已保存到本地仓库:

1
2
git add .
git commit -m "保存本地代码"

步骤二:允许合并不相关的历史

先来解释一下git的历史提交机制:

什么是“历史”?

在 Git 眼里,你的项目不是一堆文件,而是一串提交链。

每一个Commit都包含两样东西:

  1. 快照:这一刻所有文件的样子。

  2. 父节点指针:指向它的前一个提交(我是从哪儿来的)。

1
A <-- B <-- C
  • C 知道它的父节点是 B

  • B 知道它的父节点是 A

  • 这就是“历史”。


什么是“共同历史”?(分叉路口)
假设你和你的同事小王,都从 C 这个节点开始工作:

  • 写了两个功能,提交了 DE

  • 小王改了别的地方,提交了 FG

现在的结构图是这样的:

1
2
3
4
5
6
7
          (你的分支)
D <--- E
/
A <-- B <-- C <--- 这就是【共同祖先 / 共同历史】
\
F <--- G
(小王的分支)

在这个图里,节点 C 就是你们的共同历史

  • 它是你们分道扬镳之前的最后一个“同步点”。

  • Git 极其依赖这个点,因为它是判断“谁改了什么”的基准(Base)


Git 的合并机制:三方合并

当你执行 git merge(或者 git pull)把小王的代码合并到你这里时,Git 实际上是在做一个三方对比

它会找来三个版本的代码:

  1. 你的最新版 (E)

  2. 小王的最新版 (G)

  3. 共同祖先 (C) —— 这最关键!

Git 会查看:“相对于祖先 C,你们俩分别改了什么?”

场景 1:C某个部分只有一人更改

  • 祖先 Ctitle = "Hello"

  • 你的 Etitle = "Hello"(没改)

  • 小王的 Gtitle = "Hello World"(改了)

Git 推理:既然你没动,小王改了,那结果肯定是用小王的。 👉 结果title = "Hello World"

场景 2:两个人都改了同一行(冲突 Conflict

  • 祖先 Ccolor = "red"

  • 你的 Ecolor = "blue"

  • 小王的 Gcolor = "green"

Git产生疑问:

“祖先是红色。你把它改成了蓝,他把它改成了绿。相对于祖先C,你们都动了这一行。我不知道听谁的。”

👉 结果合并冲突(Merge Conflict)。Git 停下来,让你手动去选蓝还是绿。


回到一开始的问题:如果没有共同历史,怎么办呢?

输入以下指令:

1
git pull origin main --allow-unrelated-histories

--allow-unrelated-histories 做了什么? 它告诉 Git我们需要合并不相干的历史。

我们可以从三个层面来看看发生那一瞬间以及之后会发生什么:

文件层面:简单的“物理堆叠”

既然没有共同祖先可以用来对比差异,Git 就会采取最笨也最直接的办法:把两边的文件都倒进同一个文件夹里。

  • 场景 A(平安无事):

    • 你本地有:main.cpp, project.pro

    • 远程有:README.md, .gitignore

    • 结果:你的文件夹里现在有这 4 个文件。大家和平共处。

  • 场景 B(发生撞车):

    • 你本地有:README.md (内容:这是我的代码)

    • 远程也有:README.md (内容:Github 自动生成)

    • 结果报冲突(Conflict)

    • 因为没有祖先,Git 无法判断是“谁改了谁”,它只知道都有这个文件且内容不一样。它会把文件标记为冲突状态,强迫你手动打开文件,决定保留哪一段文字。

历史层面:强行认亲

这是最有趣的部分。执行这个命令后,会生成一个新的 “合并提交”(Merge Commit)

这个提交非常特殊,它有两个父节点,而且这两个父节点来自两个原本不相干的世界。

图示如下:

1
2
3
4
5
(本地的历史)
A --- B --- C ┐
├── M <-- 这个是合并产生的新节点
X --- Y ────┘
(远程的历史)
  • 在那一瞬间:节点 M 诞生了。它就像一个“混血儿”,左手拉着你本地的爸爸 C,右手拉着远程的爸爸 Y

  • 从此以后:因为 M 连接了这两条线,Git 就会认为这两条线归一了。下次你再合并,Git 就能顺着线找到 M 作为共同祖先。

步骤三:再次推送

合并成功后,本地就拥有了远程的文件,此时再推送即可成功:

1
git push -u origin main

工具技巧:如何退出 Vim 编辑器

在执行 git pull 合并时,Git 经常会自动打开 Vim 编辑器让你确认“合并日志(Merge Message)”。对于新手来说,这是一个常见的“卡住”点。

操作口诀

当你看到满屏波浪号 ~ 且无法用鼠标点击时:

  1. Esc:确保退出输入模式。

  2. 输入 :wq

    • : (冒号,进入命令模式)

    • w (write,写入保存)

    • q (quit,退出)

  3. Enter:执行。


Git 反馈

当显示以下信息时,代表操作成功:

关键词 含义
Enumerating objects Git 正在清点要上传的文件数量。
Writing objects 100% 文件上传传输完成。
main -> main 本地 main 分支已更新到远程 main 分支。
branch ‘main’ set up to track 关联成功!以后可以直接用 git pushgit pull

💡 给开发者的建议

  • 先 Pull 后 Push:养成好习惯,每次准备上传代码前,先运行 git pull 拉取队友或远程的最新修改,解决冲突后再 git push

  • 慎用 -fgit push -f 是强制覆盖,除非你非常确定要删除远程的所有历史,否则在团队开发中严禁使用

后续提交完整流程

检查状态

养成好习惯,先看一眼哪些文件变红了(修改了但没暂存)。

1
git status

添加文件入缓存区

把所有修改过的文件放入缓存区。

1
git add .

(注:. 代表当前目录下的所有文件。如果你只想提交特定文件,把 . 换成文件名)

提交保存

把暂存区的文件生成一个新的版本记录。引号里写清楚你干了什么。

1
git commit -m "这里写你的修改备注,例如:修复了登录按钮的bug"

这里相当于提交了一份新版本的代码到本地

拉取更新

在上传到远程仓库之前,必须先检查远程有没有别人提交的新代码。
这一步能避免 90% 的推送报错。

1
git pull

(如果没有更新,它会提示 Already up to date,这很好;如果有更新,它会自动合并)

上传到远程仓库

把你的新版本上传到 GitHub。

1
git push