先分清 Git 和 GitHub
| Git | GitHub | |
|---|---|---|
| 本质 | 分布式版本控制系统 | 托管 Git 仓库的协作平台 |
| 在哪里运行 | 主要在本地,也能与任意 Git 服务器通信 | 云端网站与服务 |
| 解决什么 | 版本、分支、合并、历史 | 权限、Pull Request、Issue、Review、Actions |
| 没有网络能否工作 | 大部分操作可以 | 不能访问云端协作功能 |
GitHub 不是 Git 的必需部分,Git 也不只服务于 GitHub。把二者分开之后,许多命令会突然变得清楚:git commit 保存到本地;git push 才把本地历史发送到远端。
Git 保存的不是“修改记录”,而是一串快照
理解 Git 最有效的模型,是把每次提交看成项目在某一时刻的完整快照。内部实现会复用没有变化的对象,所以它不需要机械地复制所有文件;但从使用者视角,每个 commit 都能还原出完整目录状态。
一次提交包含项目树的指针、作者、时间、说明文字和父提交。父指针把提交连成历史。合并提交可能有两个父节点,所以 Git 历史更准确地说是一张有向无环图,而不是永远笔直的一条线。
分支不是另一份代码。它只是一个会随提交向前移动的名字。HEAD 则告诉 Git:我现在站在哪个分支,或者直接站在哪个提交上。
本地的三个区域
.git 中的对象、引用与历史。commit 真正写入这里。暂存区不是多余的一步。它回答的是:“我这次究竟想提交哪一组变化?”一个文件可以同时存在已暂存和未暂存的修改,因此提交前最值得养成的习惯不是盲目 git add .,而是查看:
git status
git diff
git diff --staged
一次最小而完整的本地循环
git init # 让当前目录成为仓库
git status # 看清当前状态
git add README.md # 选择进入下次提交的内容
git diff --staged # 审阅将要提交的快照
git commit -m "Document setup" # 生成本地提交
git log --oneline --decorate # 查看历史和分支指针
好的 commit 应该是一个可以解释、可以审查、也可以单独撤销的逻辑单元。提交信息写“做了什么以及为什么”,而不是笼统的 update 或 fix。
分支、切换与 worktree
新任务通常从新分支开始:
git switch -c docs/git-guide
# 修改、暂存、提交
git switch main
git switch 改变 HEAD 指向的分支,并让工作目录呈现该分支的快照。旧教程常用 git checkout,它仍然存在,但同时承担切分支和恢复文件等多种职责;新手用 switch 与 restore 更容易看清意图。
Working tree 与 git worktree
working tree 是当前检出的文件目录;git worktree 是一组命令,可以让同一个仓库同时拥有多个工作目录。这样能在不反复切换分支的情况下,一边维护主线,一边开发功能或修复紧急问题。
合并与变基:都在整合历史,但表达不同
| Merge | Rebase | |
|---|---|---|
| 动作 | 把两条历史汇合,必要时生成合并提交 | 把一组提交重新放到新的基点上 |
| 历史 | 保留真实分叉结构 | 得到更线性的历史 |
| 风险 | 通常不会改写现有提交 | 会生成新的提交 ID,等于改写历史 |
| 建议 | 共享分支上的安全默认项 | 清理自己尚未共享的功能分支 |
最重要的原则是:不要随意 rebase 已经被别人依赖的共享历史。历史是否漂亮,永远不该优先于协作者能否安全同步。
远端并不是“云端文件夹”
远端仓库是另一份 Git 仓库。origin 只是克隆时默认创建的远端名字,并不具有特殊权限。常见对象关系是:
main:当前本地分支;origin/main:本地对远端main最近一次已知位置的记录;- GitHub 上的
main:服务器此刻真正的分支位置。
| 命令 | 真正发生的事 |
|---|---|
git clone | 复制仓库、历史和默认分支,并设置远端 origin。 |
git fetch | 下载远端新对象并更新远端跟踪分支,不改当前工作文件。 |
git pull | 先 fetch,再把远端变化 merge 或 rebase 到当前分支。 |
git push | 发送本地对象,并请求远端把某个分支指针向前移动。 |
这也解释了为什么 push 会被拒绝:如果远端已经有你本地不知道的新提交,Git 不愿直接让远端分支跳过别人的历史。先 fetch,理解差异,再合并或变基,才是安全做法。
GitHub 协作的两种基本模型
共享仓库:branch → Pull Request
适合已有写入权限的团队成员。每个人从默认分支创建功能分支,推送到同一个仓库,通过 Pull Request 讨论、运行检查和合并。
开源贡献:fork → branch → Pull Request
适合没有原仓库写入权限的外部贡献者。fork 是自己账号下的一份仓库;origin 通常指向自己的 fork,upstream 指向原项目。Pull Request 把自己分支上的提交提议给上游维护者。
Pull Request 不是 Git 命令
PR 是 GitHub 提供的协作对象:它把提交差异、说明、讨论、审查、自动测试和合并决策放在一起。Git 负责历史,GitHub 负责围绕历史协作。
一次规范的开源贡献
- 先读规则。查看 README、CONTRIBUTING、许可证、行为准则、测试要求和安全报告方式。
- 确认问题值得做。优先选择
good first issue或help wanted;需求不明确时先与维护者沟通。 - Fork 并克隆。自己的 fork 用作可推送的远端,同时把原项目添加为
upstream。 - 创建主题分支。让每个 PR 聚焦一个问题,不在默认分支直接堆改动。
- 做小而完整的提交。运行项目要求的格式化、测试和构建,提交前审阅 diff。
- 推送并打开 PR。说明背景、改动、验证方式和关联 Issue;未完成时可以先开 Draft PR。
- 响应审查。继续向同一分支提交即可更新 PR,不需要因修改意见重开一个 PR。
git clone https://github.com/YOU/PROJECT.git
cd PROJECT
git remote add upstream https://github.com/OWNER/PROJECT.git
git switch -c docs/clarify-installation
# 修改并验证
git add docs/install.md
git commit -m "Clarify Windows installation"
git push -u origin docs/clarify-installation
# 然后在 GitHub 上从该分支创建 Pull Request
冲突不是事故,而是 Git 无法替人做决定
当两条历史修改了同一位置,Git 没有足够信息判断哪个意图更正确,于是把决定交给人。可靠的处理顺序是:
- 先用
git status确认冲突文件和当前操作; - 阅读两边内容,理解业务意图,而不是机械删除冲突标记;
- 编辑成最终版本,运行测试;
git add标记该文件已解决;- 继续 merge / rebase,或在判断错误时使用对应的
--abort回到操作前。
比背命令更重要的安全习惯
- 操作前后都看
git status; - 提交前分别查看
git diff和git diff --staged; - 不把密钥、密码、个人令牌和本地环境文件提交进仓库;
- 不了解后果时,不使用
git reset --hard、git clean -fd或强制推送; - 确需改写自己远端分支时,优先理解并使用
--force-with-lease,避免覆盖别人的新提交; - 主分支的重要变更通过 PR、检查和审查进入,而不是直接推送。
最后的判断
Git 真正难学的地方,不是命令多,而是它同时展示了几个不同状态:硬盘里的文件、暂存区里的下次快照、本地历史、远端历史,以及指向这些提交的各种分支。只要先问“我现在在移动哪个对象”,大多数命令就不再神秘。
我会把整套系统压缩成一句话:在本地用小而清晰的提交保存意图,用分支隔离工作,用远端交换历史,再用 Pull Request 让变化经过讨论、验证与授权。
资料与延伸阅读
- Pro Git 中文版:Git 的工作区、暂存区、对象模型、分支与远端。
- GitHub Docs:About Git:分布式仓库与协作模型。
- GitHub Docs:Writing code for a project:branch、fork、commit 与 Pull Request 的关系。
- GitHub Docs:Contributing to open source:开源贡献的完整流程与注意事项。
- 《Git+Github 核心概念大串讲》(BV1ySLc6QEcB):概念讲解与实战演示来源。