在软件开发过程里,少不了版本控制,而主流的代码版本控制里,最耳熟能详的就是SVN和Git,本文将简单介绍一下git的使用。(待修改补充……)


一、 技术背景

在 Git 横空出世之前,市面上主导的是 SVN、CVS 这种集中式版本控制系统。集中式最大的痛点是:高度依赖中央服务器。一旦断网,所有人都无法提交代码;要是中央服务器磁盘坏了,整个项目的历史记录都有丢失的风险。

传说,在2005年以前,Linux内核代码当时是用的BitKeeper作为版本控制的,BitKeeper是商用软件,但是架不住Linux 之父 Linus Torvalds 面子大,BitKeeper就给免费提供给 Linux 开发者使用。结果,也是传说,因为社区中部分开发者对其协议进行了逆向工程研究,BitKeeper 公司认为这违反了使用条款,直接取消了免费授权。然后Linus亲自编写了开源的分布式版本控制系统 Git 来作为替代品。

💡 冷知识 (来自维基百科)
2005年,Linus Torvalds 为了代替 BitKeeper,仅仅闭关开发了约 10天,就写出了 Git 的第一个可用版本!不仅如此,“Git”在英国俚语里的意思是“饭桶、笨蛋”,Linus 曾自嘲说:“我是一个自大的混蛋,所以我把所有的项目都用自己的名字命名。首先是 Linux,然后是 Git。”

Git 的核心颠覆在于分布式。每个开发者的电脑上都有一个完整的代码库(包含完整的历史记录)。这意味着你可以在高铁上、飞机上随时提交代码到本地,等有了网络再把它们推送到远程服务器。


二、 基本使用

在 Git 中,数据流转需要经过四个核心区域:工作区 (Workspace)暂存区 (Index/Stage)本地仓库 (Repository)远程仓库 (Remote)
掌握了这四个区域的数据流向,你就能玩转最基础的命令行操作。

1. 从零开始:初始化与克隆

不管你是想新建一个项目,还是从 GitHub 偷拉别人的开源代码,这都是第一步。

# 场景A:你写了一个程序,需要增加版本控制的时候,敲下这个命令,可以让你的程序工作目录变成一个用Git管理的本地仓库(目录下有一个隐藏文件夹.git)
git init

# 场景B:从远程服务器克隆一个完整的项目到本地
git clone https://github.com/torvalds/linux.git

2. 核心三板斧:Add / Commit / Push

日常开发中最常用的流向,就是把工作区的改动一步步推送到远程。

# 1. 将所有改动(新增、修改、删除)扔进暂存区 (Workspace ➔ Stage)[^1]
git add .

# 2. 将暂存区的内容提交到本地仓库,并写下你的“作案动机” (Stage ➔ Repository)[^2]
git commit -m "完成用户登录界面的UI开发"

# 3. 将本地仓库的分支推送到远程仓库(如 GitHub/GitLab) (Repository ➔ Remote)
git push origin master

3. 数据拉取与更新:Pull 与 Fetch

当同事更新了代码,你需要把远端的改动同步到本地。

# 极简写法:直接拉取远程最新代码并与本地合并 (等同于 fetch + merge)[^3]
git pull

# 更安全的操作:只拉取远程最新提交,不自动合并,方便你先检查差异再决定如何处理
git fetch origin

三、 浅聊原理:Git 到底是怎么存数据的?

掌握了上面三板斧,在个人开发的情况下,基本是没有问题了,但是Git是为了团队合作而生的,所以避免不了合作开发。
是不是有很多人刚开始用Git的时候,会很怕数据丢失,会很怕代码冲突,或者是改掉了队友的代码又推上去了服务端。
这里我谈下我个人的一些经验,可能有帮助。
我先按照Duolingo的听力题的流程,把一些相关的知识点列举一下,让学习的同学熟悉理解一下。
1、Git和SVN在记录存储上有个很大的差异,可以说是完全不同,SVN的日志记录,是存储版本差异,类似“文件A在版本2增加了一行,删除了两行”,而Git不一样,Git 不是存差异(delta),而是存快照(snapshot)。 也就是说,你每次提交,它都会对当时的全部文件制作一个快照并保存这个快照的索引,如果某个文件没有修改,Git 不会重新存储该文件,而是只保留一个链接指向之前存储的文件。
2、Git底层主要靠三个核心对象(Object)来维持运转

  • Blob (数据对象):专门用来存文件内容,不存文件名。

  • Tree (树对象):类似操作系统的文件夹,用来存储文件名,以及包含哪些 Blob 和其他子 Tree。

  • Commit (提交对象):包含顶层 Tree 的哈希值、提交者信息、时间戳以及父提交(上一个版本的Commit)。

3、Git的 分支(Branch)和SVN的分支概念有些许不一样,按照Git的设计哲学,Git是没有绝对意义的主干(SVN里的master是主干,别的是分支),哪怕是默认的master,那也是被叫做master分支,只是这个分支,通常会按照语义去设计成生产分支(可以直接发布到线上的代码,区别于一些比如开发分支、issue分支等)。Git就是鼓励创建分支的,从设计上来说,每个分支本质上就是一个指向某个Commit对象的可移动的轻量级指针而已。创建一个新分支,其实只是创建了一个 41 字节的文件(包含了 40 个字符的哈希值和一个换行符),所以Git创建分支都是瞬间完成的。
4、合并(merge/或者git pull会自动执行merge)的时候,有时候会出现一行字,里面有个关键字Fast-forward,这个中文翻译好像叫快进合并,也就是当Git发现,需要合并进来的分支(remote的分支和本地分支可以理解成是两条分支)在新建或者同步(merge或者rebase)后,当前分支没有发生更新,就直接把指针指向了需要合并进来的分支的最新Commit,这个有点绕,可能我的表述不够好,暂且先这么表达,如果完整流程碰到过一次会更好理解一点。

好,介绍了这些,开始说一下实际使用过程中会遇到的问题。
先说一下最重要的,也是很多新手最担心的,不知道Git一行代码下去操作了什么,担心数据丢失。三板斧里,每个操作,对应的都是一次数据保存。Add过的数据,之后的修改都能被还原回上一次add的状态;
commit过的数据,都会永久保存在本地记录里(被delete或者手动删除掉.git等操作外),实际上哪怕是被reset --hard这种操作,还是会保留30天(默认配置),如果想要恢复,也可以找到commit的SHA去恢复数据;
至于push后的数据,更是直接在服务端保留了一份记录,随时可以reset回自己想要的数据。
然后就到了merge的环节,不只是新手,所有人都讨厌合并,但是这个是可以通过业务和架构设计尽量避免的,这个后面说。新手主要害怕的是,一旦发生冲突(conflict),不知道该如何办,满脑子都是回滚,这里我先简单介绍一下,为什么会有冲突。
首先,正常的情况,是我在master分支上,commit是A的基础上开了一个分支branchX,然后把A改成了B,生成一个α的commit,再切换回master,执行“git merge branchX”操作,git就会认为是修改,按照默认设置,直接fast-forward,在master上增加一个α的commit,当前的版本直接显示为B,这个很好理解吧,就是一个正常版本的递进,前面说了一堆比较绕的操作,直接理解成我在生产分支上开了一个bug修复分支,然后修复了一个bug,再合并回来,差不多的操作。这部分是所有人所期待的使用节奏,git只记录每次修改点。但是实际工作中,项目是多人合作的,就会出现这种情况,我在master分支上,commit是A的基础上开了一个分支branchX,然后把A改成了B,生成一个α的commit,再切换回master,打算执行“git merge branchX”操作时,发现remote上有更新的master的commit,原来是同事,额,姑且就叫小王吧,同事小王也从master分支上开了一条新分支branchY,改了一个内容是把A改成C的叫β的commit,然后先我一步合并回了master分支,并且推到了远端,这时候,不管我先从远端拉下来把branchX合并进来,还是先合并branchX,再fetch远端的master,把local的master合并进去,都会出现conflict,这时候需要合并人(我)去确认,到底应该选择B还是选择C,git会在差异部分同时显示两个版本的内容(比如这里的例子就是同时显示B和C),用分割符(>>>>/====)隔开,并附上commit的SHA(合并进来的版本)以及HEAD(当前版本)。
所以实际上,conflict没有那么恐怖,只是确实比较麻烦,举的例子比较简单,直接保留一个B或者C,然后把分隔符删掉以后执行三板斧就好了,但是实际情况往往可能会很复杂,一个新需求可能开发周期已经很长才能合并回生产分支,同一个项目同时开发的人也会很多,很多大一点的项目,都得单独拿出一天来做合码日,来保证合并后的代码没有差池,所以这也是很多人都讨厌merge。

💡 底层大揭秘
每次执行 git commit,实际上就是生成了一个 Commit 对象,这个对象用一根无形的“线”牵着一个 Tree(你的整个项目目录),Tree 里面又牵着一堆 Blob(你的代码文件)。
而我们常说的 分支(Branch),本质上仅仅是一个指向某个 Commit 对象的可移动的轻量级指针而已。创建一个新分支,其实只是创建了一个 41 字节的文件(包含了 40 个字符的哈希值和一个换行符),这也是为什么 Git 创建分支永远是瞬间完成的原因!


四、 合理使用分支 (Git Flow)

既然 Git 创建分支性能极高,那么日常开发中就绝对不能让所有人都在 main 主分支上硬刚。合理的分支策略能让团队协作井井有条。

常见的工作流模式(Git Flow)往往会包含以下几种分支:

分支类型 (Branch)

描述与使用场景

master / main

主分支(线上稳定版)。只包含随时可发布到生产环境的代码。严禁直接在此分支进行开发。

develop / dev

开发分支。所有特性的汇总中心。日常开发的最新代码都在这里,测试无误后再合并到主分支发布。

feature/*

特性分支。用于开发某个具体的新功能(如 feature/login)。从 dev 检出,开发完成后合并回 dev。

hotfix/*

热修复分支。线上突然出现严重 Bug!直接从 main 检出修复分支(如 hotfix/bug-001),修复后紧急合并回 main 和 dev。

代码示例:

# 创建并直接切换到一个名为 feature/payment 的新特性分支
git checkout -b feature/payment

# (开发中... 一顿写代码、add、commit 操作...)

# 开发完毕,切回 dev 分支
git checkout dev

# 将 payment 分支的改动合并到当前的 dev 分支
git merge feature/payment

五、 配套食用:提交规范 (Commit Convention)

如果在 git log 里总是看到 git commit -m "更新""修复bug""又改了一下" 这种毫无营养的信息,等后续需要排查历史问题或者自动生成变更日志(Changelog)时,团队绝对会抓狂。

目前开源界最流行的是 Angular 团队的规范,它不仅让代码历史清晰易读,还能配合工具链实现自动化。
标准的提交格式通常为:<type>(<scope>): <subject>

Type 枚举值

描述与使用场景

feat

新功能 (Feature)。项目中增加了一个全新的功能。

fix

修复 Bug。解决了一个线上或测试环境的 bug。

docs

文档更新。仅仅修改了 README、注释等,没有改变核心代码逻辑。

style

代码格式。不影响代码运行的变动(如空格、格式化、去掉多余分号等)。

refactor

重构。既不是新增功能,也不是修改 bug 的代码变动(如优化一段核心算法,抽离公用组件等)。

test

测试。增加缺失的测试用例或修正现有的测试。

chore

构建/工具变动。改变构建流程或辅助工具(如更新依赖包、修改 Webpack/Vite 配置)。

代码示例:

# 规范的提交写法,让人一眼看出改了哪里、干了什么
git commit -m "feat(user): 新增用户微信扫码登录功能"

git commit -m "fix(payment): 修复支付宝回调失败时订单状态未重置的bug"

结语

Git 的机制极为强大且灵活,尤其是在那个 SVN 称霸的年代横空出世,绝对是降维打击。理解了 工作区域流向基础指令的作用底层的快照机制,你就掌握了控制代码版本的“三板斧”。再配合合理的分支策略和 Angular 提交规范配套食用,你将能编写出历史记录清晰、易于追溯的高质量代码库,从容融入任何现代化的开源项目。

希望这篇文章能帮初学者的你入门,后续还会补充一些更细节一点的内容(比如 Rebase 和 Merge 的恩怨情仇,以及如何用 Reset 和 Revert 强行吃下“后悔药”)!如果你有任何疑问,欢迎在评论区探讨交流。