一、什么是 Git
1. 何为版本控制
版本控制是一种记录文件变化的系统,可以跟踪文件的修改历史,并允许用户在不同版本之间进行比较、恢复或合并。它主要用于软件开发过程中管理代码的变更,但也可以应用于任何需要跟踪文件变更的场景。
版本控制系统(VCS)可以帮助团队协作开发,并提供以下功能:
- 历史记录管理:版本控制系统会记录每个文件的修改历史,包括修改内容、时间和作者等信息。这使得用户可以查看文件的演变历程,了解每次修改的目的和影响。
- 并行开发支持:多个开发者可以同时修改同一个项目的不同部分,版本控制系统能够合并这些修改,并解决可能出现的冲突。
- 备份和恢复:通过版本控制系统,可以轻松地恢复到之前的任意版本,即使文件丢失或损坏也能够通过版本控制系统进行恢复。
- 分支管理:版本控制系统允许创建分支(Branch),开发者可以在分支上进行实验性的修改,而不会影响到主要的代码库,这使得并行开发和功能开发更加灵活。
- 版本标记:可以对重要的版本进行标记(Tag),例如发布版本或里程碑版本,方便日后查找和引用。
常见的版本控制系统包括 Git、Subversion(SVN)、Mercurial 等,它们都提供了类似的功能,但在实现方式和使用上有所不同。其中,Git 是目前最流行的版本控制系统之一,被广泛应用于软件开发领域。
2. Git 的诞生
Git 的开发由 Torvalds(林纳斯·托瓦兹,Linux 之父)于 2005 年 4 月启动,起因是 2002 年时,用于 Linux 内核开发的专有源代码控制管理(SCM)系统 BitKeeper 撤销了其 Linux 开发的免费许可证。BitKeeper 的版权所有者 Larry McVoy 声称,Andrew Tridgell(Linux 社区主要贡献者,Samba之父)通过逆向工程破解了 BitKeeper 的协议,并创建了SourcePuller。 同一事件还刺激了另一个版本控制系统 Mercurial 的创建。
引用:Git - Wikipedia
Git 的诞生是由于 Linux 内核社区与 BitKeeper 公司之间发生了一些纠纷,导致 Linux 内核社区无法继续免费使用 BitKeeper。
由于这个事件,Linus Torvalds 决定开始开发一个新的版本控制系统,这个系统要具有以下特点:
- 分布式:与传统的集中式版本控制系统不同,分布式版本控制系统允许每个开发者在本地完整地拷贝整个代码库,这样可以在没有网络连接的情况下继续工作,并且更容易支持分支和合并操作。
- 性能:由于 Linux 内核的规模巨大,所以版本控制系统必须具备高效的性能,能够快速处理大量的文件和提交。
- 简单易用:Git 设计的目标之一是让用户更容易理解和使用,尽管它提供了丰富的功能,但命令和概念相对简单,学习曲线较为平缓。
基于这些需求,Linus Torvalds 开始了 Git 的开发,并于 2005 年 4 月 3 日发布了第一个版本。随着时间的推移,Git 在开源社区中得到了广泛的认可和应用,逐渐成为了最流行和最常用的版本控制系统之一。
3. 集中式 VS 分布式
集中式版本控制系统(Centralized Version Control System,CVCS)和分布式版本控制系统(Distributed Version Control System,DVCS)是两种不同类型的版本控制系统,它们在数据存储、工作流程和协作模式等方面有所不同。
3-1. 集中式版本控制系统(CVCS)
CVCS 是一种传统的版本控制系统,所有的文件和版本历史都存储在中央服务器上。开发者需要通过从中央服务器检出代码(checkout)来获取项目的副本,在本地进行修改后再提交到中央服务器。典型的 CVCS 包括 CVS(Concurrent Versions System)和 SVN(Subversion)等。
主要特点包括:
- 协作依赖于中央服务器:开发者必须与中央服务器保持连接,才能进行代码的获取和提交。
- 危险点:如果中央服务器发生故障或者网络不稳定,那么开发者将无法工作。
- 分支和合并相对复杂:通常需要由中央服务器来执行分支和合并操作。
集中式版本控制系统最大的问题就是必须联网才能工作,遇到带宽不好的情况是,提交、更新较大文件可能会十分的缓慢。而且,集中式版本控制系统容灾性差,万一中心服务器硬盘出现的数据丢失,那后果是很严重的。
3-2. 分布式版本控制系统
DVCS 是一种新型的版本控制系统,每个开发者都拥有完整的代码仓库(包括完整的历史记录)的副本。开发者可以在本地进行大部分的操作,而不需要与中央服务器保持连接。典型的 DVCS 包括 Git、Mercurial 等。
主要特点包括:
- 分布式架构:每个开发者都可以在本地进行工作,不受中央服务器的限制。
- 离线工作:开发者可以在没有网络连接的情况下继续工作,而且操作效率更高。
- 分支和合并更灵活:由于每个开发者都拥有完整的历史记录,因此分支和合并操作更加轻松和快速。
分布式版本系统的优点显而易见,首先,它可以完全独立的工作,不需要服务器的参与;其次,它具有很高的安全性,某一台电脑的数据丢失后,可以通过其他电脑进行恢复。
现实情况下,分布式版本控制系统也需要一个“中心服务器”,但该服务器的作用仅限于为数据的交互提供便利性。
4. Git 的工作流程
Git 的命令大全和作用,可以从这个网站查阅:workspace :: Git Cheatsheet :: NDP Software
- 克隆仓库:在开始工作之前,首先需要将远程仓库克隆到本地机器上。使用
git clone
命令可以将远程仓库完整地复制到本地。 - 创建分支:为了并行开发和隔离不同的功能或修复,通常会创建新的分支。使用
git branch
命令可以创建一个新的分支,例如:git branch feature-branch
。 - 切换分支:使用
git checkout
命令可以切换到创建的分支,例如:git checkout feature-branch
。 - 添加和提交更改:在所选分支上进行工作后,可以使用
git add
命令将更改的文件添加到暂存区,然后使用git commit
命令将暂存区的更改提交到本地仓库, - 推送更改:如果要将本地分支的更改推送到远程仓库,可以使用
git push
命令,例如:git push origin feature-branch
。这将把本地分支的更改推送到远程仓库中对应的分支。 - 合并分支:当分支的工作完成后,可以将其合并到主分支(通常是
master
分支)或其他目标分支中。使用git merge
命令可以执行分支合并, - 解决冲突:如果在合并分支时发生冲突(即同一文件的不同部分具有不同的更改),需要手动解决冲突。在冲突解决后,再次执行
git add
和git commit
命令以完成合并。 - 拉取更新:在团队协作中,如果其他人推送了更改到远程仓库,可以使用
git pull
命令拉取更新到本地仓库,以确保与最新代码保持同步
二、Git 的下载与安装
1. Git 的下载
Git 的下载链接:Git - Downloads (git-scm.com)
点击进链接后,可以看到有四个版本,按操作系统位数可分为 32 位和 64 位,按使用版本可以分为 Standalone Installer(独立安装程序)和 Portable(便携式),可以简单的理解为前者需要安装,后者不需要安装可以直接使用(国内对这类不需要安装的软件也称为绿色版)。
本文使用的是 64 位的独立安装版本。
2. Git 的安装
以管理员身份运行 Git 安装程序。
直接点Next
。
我这里选择默认的安装路径,可以根据自己的喜好更改路径,然后点击Next
。
按默认的勾选安装即可。
直接点Next
。
选择文件的编辑器,一般都是用 VIM 作为默认的编辑器,直接点Next
。
决定初始化新项目(仓库)的主干名字,第一种是让 Git 自己选择,即默认名字是 master ,但是未来也有可能会改为其他名字;第二种是用户自行决定,默认是 main,当然,你也可以改为其他的名字。一般默认第一种,点击Next
。
[!NOTE]
这个安装流程在之前是没有的,第二个选项下面有个
NEW!
,说很多团队已经重命名他们的默认主干名为 main。起因是 2020 年非裔男子 George Floyd 因白人警察暴力执法惨死而掀起的 Black Lives Matter(黑人的命也是命)运动,很多人认为 master 不尊重黑人,呼吁改为 main。
接着是调整 PATH 环境变量,默认使用第二个。第三个只适合懂的人折腾。
[!NOTE]
翻译如下:
仅从 Git Bash 使用 Git
这是最谨慎的选择,因为您的 PATH 根本不会被修改。您将只能使用 Git Bash 中的 Git 命令行工具。
从命令行以及第三方软件进行 Git
(推荐)此选项仅将一些最小的 Git 包装器添加到PATH中,以避免使用可选的 Unix 工具使环境混乱。您将能够使用 Git Bash 中的 Git,命令提示符和 Windov PowerShell 以及在 PATH 中寻找 Git 的任何第三方软件。
使用命令提示符中的 Git 和可选的 Unix 工具
Git 和可选的 Unix 工具都将添加到您的 PATH 中。
警告:这将覆盖 Windows 工具,例如 “find” and “sort”. 仅在了解其含义后使用此选项。
选择 SSH 执行文件,按默认的即可。
[!NOTE]
以前的 Git 2.31 都没有这个界面,估计是 2.4 开始可以使用外部的 SSH 文件。
选择HTTPS后端传输,还是按默认的来,点击Next
。
[!NOTE]
作为普通用户,只是用 Git 来访问 Github、GitLab 等网站,选择前者就行了。如果在具有企业管理证书的组织中使用 Git,则将需要使用安全通道。如果你仅使用 Git 来访问公共存储库(例如 GitHub ),或者你的组织不管理自己的证书,那么使用 SSL 后端(它们只是同一协议的不同实现)就可以了。
两个选项的具体区别,感兴趣的可以去程序员社区 stack overflow 查看,链接:git - What’s the difference between OpenSSL and the native windows Secure Channel library - Stack Overflow
配置行尾符号转换, 这三种选择分别是: ① 签出 Windows 样式,提交 Unix 样式的行结尾;② 按原样签出,提交Unix样式的行结尾;③ 按原样签出,按原样提交。
那 Windows 样式和 Unix 样式到底有什么区别呢?
GitHub 中公开的代码大部分都是以 Mac 或 Linux 中的 LF(Line Feed)换行。然而,由于 Windows 中是以 CRLF(Carriage Return+ Line Feed)换行的,所以在非对应的编辑器中将不能正常显示。
Git 可以通过设置自动转换这些换行符。使用 Windows 环境的各位,请选择推荐的 “Checkout Windows-style,commit Unix-style line endings” 选项。换行符在签出时会自动转换为 CRLF,在提交时则会自动转换为 LF 。
引用:《GitHub 入门与实践》
上面说 Mac 、Linux、Unix 的 Line Feed ,翻译过来就是换行符,用 “\n” 表示,换行符 “\n” 的 ASCII 值为 0x0A。而 Windows 的是 Carriage Return + Line Feed(回车 + 换行),用 “\r\n” 表示,回车符 “\r” 的 ASCII 值为 0x0D。
我们现在的教程就是介绍怎么安装 Windows 版 Git,所以肯定选第一项啦。
至于 “回车”(carriage return)和 “换行”(line feed)这两个概念的来历和区别?
在计算机还没有出现之前,有一种叫做电传打字机(Teletype Model 33)的玩意,每秒钟可以打 10 个字符。但是它有一个问题,就是打字机打完一行换行的时候,要用去 0.2 秒,正好可以打两个字符。要是在这 0.2 秒里面,又有新的字符传过来,那么这个字符将丢失。
于是,研制人员想了个办法解决这个问题,就是在每行后面加两个表示结束的字符。一个叫做"回车",告诉打字机把打印头定位在左边界;另一个叫做"换行",告诉打字机把纸向下移一行。
参考程序员社区 stack overflow,链接:newline - What are carriage return, linefeed, and form feed? - Stack Overflow
接着来到配置终端模拟器与 Git Bash 一起使用,建议选择第一个选项,因为 MinTTY 3功能比 cmd 多,cmd 只不过比 MinTTY 更适合处理 Windows 的一些接口问题,这个对 Git 用处不大,除此之外 Windows 的默认控制台窗口有很多劣势,比如 cmd 具有非常有限的默认历史记录回滚堆栈和糟糕的字体编码等等。
相比之下,MinTTY 具有可调整大小的窗口和其他有用的可配置选项,可以通过右键单击的工具栏来打开它们 git-bash 。
接着来到选择默认的 “git pull” 行为的界面,先解释一下 git pull 是什么意思,git pull 就是获取最新的远程仓库分支到本地,并与本地分支合并。这里给出三个 git pull 的行为分别是:merge、rebase 和直接获取。
一般默认选择第一项,git rebase 绝大部分程序员都用不好或者不懂,而且风险很大,但是很多会用的人也很推崇,但是用不好就是灾难。git pull 只是拉取远程分支并与本地分支合并,而 git fetch 只是拉取远程分支,怎么合并,选择 merge 还是 rebase,可以再做选择。
更详细的解释,我给各位总结了几个比较好的说明,以下是链接:
git branch - Why does git perform fast-forward merges by default? - Stack Overflow
In git how is fetch different than pull and how is merge different than rebase? - Stack Overflow
Difference between git pull and git pull --rebase - Stack Overflow
接着是选择一个凭证帮助程序, 第一个选项是提供登录凭证帮助的,Git 有时需要用户的凭据才能执行操作;例如,可能需要输入用户名和密码才能通过 HTTP 访问远程存储库(GitHub,GItLab 等等)。第二个则是不使用凭证助手。
建议默认,点击Next
。
然后是配置额外的选项,分别是启用文件系统缓存和启用符号链接。启用文件系统缓存就是将批量读取文件系统数据并将其缓存在内存中以进行某些操作,可以显著提升性能。这个选项默认开启。启用符号链接 ,符号链接是一类特殊的文件, 其包含有一条以绝对路径或者相对路径的形式指向其它文件或者目录的引用,类似于 Windows 的快捷方式,不完全等同类 Unix(如 Linux) 下的符号链接。因为该功能的支持需要一些条件,所以默认不开启。
建议默认,点击Next
。
最后是配置实验性选项,这些还是实验性功能,可能会有一些小错误之类的,建议不用开启。
直接点Install
安装。
安装完成,默认勾选的选项都去掉,点击Finish
完成安装。
3. Git 各个软件的简单介绍
安装好后,可以在开始菜单中,找到 Git 的文件夹,里面包含了 Git Bash、Git CMD、Git FAQs、Git GUI、Git Release Note,下面我们就分别介绍一下这几个软件。
3-1. Git Bash
Git Bash 是基于CMD的,在CMD的基础上增添一些新的命令与功能,平时主要用这个,功能很丰富。
3-2. Git CMD
Git CMD 不能说和 cmd 完全一样,只能说一模一样,功能少得可怜。
3-3. Git FAQs
Git FAQs 就是 Git Frequently Asked Questions(常问问题),点击图标直接就可以访问地址:FAQ · git-for-windows/git Wiki (github.com)
3-4. Git GUI
Git GUI 就是 Git 的图形化界面,可以通过它快速创建新仓库(项目),克隆存在的仓库(项目),打开存在的仓库(仓库)。这个我基本没用过,建议还是用命令行学习Git。
3-5. Git Release Note
Git Release Note 就是版本说明,增加了什么功能,修复了什么 bug 之类的。一般使用浏览器打开。
三、Git 本地管理
1. 创建版本库
版本库(repository)又名仓库,本质上就是一个目录(文件夹),把需要控制管理的代码、文档放入到该目录下。在该目录下的所有文件的操作(添加、删除、修改、回退、查询等)都可以被管理起来。
为方便演示,我新建了一个文件来进行操作,而这个新建的文件夹就可以被称为版本库,但是此时还不是真正意义上的版本库,它跟普通的文件夹没有区别。
[!CAUTION]
为了避免遇到各种莫名其妙的问题,请确保目录名(包括父目录)不包含中文。
进入文件夹,右键弹出菜单,选择“Open Git Bash here”。
输入git init
并回车可初始化版本库,执行成功后,这个文件夹才是真正 Git 版本库。同时,在文件夹中会多出一个隐藏文件夹,名为“.git”。
当版本库创建好后,我们就可以用它来做项目的管理了。例如,在初始化版本库后,通常都会添加一个说明文件来描述这个版本库的作用,行业默认都是新建一个名为“README”的.txt
文件(普通文本文件)或者.md
文件(Markdown 文件)。
我这里直接就在 Git 的命令框中创建了,使用的是touch
命令创建,再用vim
命令进入编辑(如果你不会 VIM,可以用最简单的右键新建文本文件的方法新建,不过我还是建议学习一下 VIM)。
在里面我输入了“This is a version library for learning Git.”,然后保存退出。
可以使用cat
命令查看“README.md”。
根据 Git 的工作流程,此时的 README.md 还在工作区,可以通过git status
命令查看仓库的状态。下图中,红色字体的“README.md”就是新增或者被修改的文件,目前还在工作区,还未提交到暂存区,括号里也提示我们可以用git add
来添加到暂存区。
使用git add README.md
把 README.md 添加到 Git 的暂存区。
再次用git status
命令查看仓库的状态,就可以看到 README.md 变成了绿色,已经保存到暂存区了。
根据 Git 的工作流程,此时 README.md 也只是存在暂存区,还并没有提交到仓库,需要使用git commit
命令将 README.md 提交到仓库。
我在使用git commit
命令时,会加上参数-m
,用于表示本次提交的说明信息,说明信息最好统一格式,这样对于后续的 Bug 管理、版本信息追踪都会大有好处。
不过我们第一次使用时,会发现无法提交(如下图)。
这个问题是由于 Git 无法检测到用户身份信息而引起的,Git 在每次提交时都需要知道提交者的姓名和电子邮件地址。可以按照命令行中提供的提示来设置用户身份信息。如下:
git config --global user.email "you@example.com"
git config --global user.name "Your Name"
把双引号里面内容替换掉,我这里替换成我的 QQ 邮箱和英文名,如下图所示:
再次提交就可以了。
这时使用git status
命令查看仓库的状态,已经没有修改或新增的文件在暂存区或者工作区了。
2. 提交修改
提交修改的步骤和前面提到的一样,例如,我们给现有的 README.md 文件修改一些内容,如下图:
这时使用git status
命令查看仓库的状态:
上面的输出表示 README.md 已经修改,但是还未添加到暂存区。我们可以通过git diff
查看当前的修改内容,输出格式为 Linux 内核补丁所用的格式:
新增的内容会以绿色字体显示,并且在每行开头多一个+
,表示新增内容(如果是-
,则表示删除内容,且字体为红色)。
现在提交修改的内容,使用的命令还是git add
和git commit
。
可以通过git log
查看近期的提交记录,提交记录以时间倒叙输出。
如果执行git commit
之后,发现提交说明写的有问题,可以通过git commit --amend
命令,修改最近一次的提交说明。输入命令后,会用 VIM 编辑器打开提交信息(还是要会 VIM ,不会的先去查一下怎么用)。
修改一下提交记录,如下图(修改的内容没什么特殊含义,只是为了示范):
保存退出后,再次用git log
查看提交记录,可以看到提交信息已经更新了。
如果要添加代码文件,也是类似的操作,例如新增一个.c 文件。
随便写一些内容。
只要有新文件加进来,都可以用git status
命令看到。
顺便编译一下程序,多一个可执行文件。
用git status
命令查看仓库。
提交修改的内容,使用的命令还是git add
和git commit
,但是同时提交两个或两个以上的文件,有两种办法。
-
方法一:
git add
命令后面把要添加的文件全部写上,不同文件用空格隔开。 -
方法二:
如果要添加的文件是
git status
能看到的全部红色字体的文件,直接用git add .
,在原来的命令上加一个.
即可。
然后用git commit
提交到仓库即可。
这时用git log
查看提交记录,就有三段记录了。
3. 版本回退
在实际产品开发过程中,我们需要养成一个习惯,那就是代码或文档修改到一定程度时,要将修改的内容提交到版本库。这样,一旦后续代码或文档修改出了问题,或者误删了某些文件,还可以通过最近的一次 commit 进行恢复,这个恢复的过程就是版本回退。
在原来的 README.md 中,我重新做了修改,如下图:
并且做了提交,如下图:
可以通过git log
加上--pretty=oneline
参数,查看简化版的提交记录信息:
可以看到有四条提交记录,前面黄色的数字是 commit ID,是 Git 的提交的版本 ID(跟 SVN 递增的 ID 不一样),通常是 40 个字符的十六进制字符串,这是 SHA-1 哈希算法生成的。这个 ID 通常被称为"SHA-1 哈希"或"SHA-1"。SHA-1 是一种加密哈希函数,虽然在安全上已经存在一些漏洞,但在 Git 中仍然被广泛使用。
回到正题,由于某种原因,需要将版本库回退到“Added code and executable files.”这次的提交版本,可以使用git reset --hard
命令来回退。但是有个前提,就是要让 Git 知道当前是什么版本,而且还要知道回退到哪个版本。
在 Git 中,用 HEAD 来表示当前版本,HEAD^ 表示上一个,HEAD^^ 表示上上一个版本,HEAD~100 表示往上 100 个版本。由上图可知,当前的 HEAD 就指在最新提交的版本上,回退到“Added code and executable files.”这次的提交版本,就是当前版本的上一次,那么执行下面命令即可:
git reset --hard HEAD^
执行结果如下图:
打开 README.md 查看一下,果然回退了。
那么假设过了一段时间,需要返回之前的版本,也非常简单,只要能找到之前版本 commit ID,就可以通过git reset
命令回退就行。Git 提供了git reflog
命令用于记录每次的命令,那么就可以通过git reflog
找到之前版本的 commit ID。
上图红框标记的就是最后一次的版本提交记录,最开始的那串数字就是 commit ID,执行下面的命令即可。
git reset --hard 6353f69
执行结束,用git log
可以看到,版本已经复原了。
查看一下文件内容也是复原状态。
Git 的版本回退速度之所以非常快速,主要是 Git 在内部有个指向当前版本的 HEAD 指针,回退版本时,Git 仅仅只是把 HEAD 从指向需要回退的版本。
4. 理解工作区和暂存区
在使用 Git 管理版本时,经常会听到工作区和暂存区这两个概念,理解工作区和暂存区对于理解 Git 的很多操作十分有帮助。从前面提到的 Git 的工作流程,可以看出工作区和暂存区都包含在本地仓库里面,下面分别介绍一下这两个概念。
4-1. 工作区
当前开发程序所在目录称为工作区,比如,我前面演示用的文件夹“git_test”就是一个工作区,可以在这里新增或删除文件,也可以修改文件里面的内容。
这里特别强调一下,隐藏文件夹“.git”不属于工作区,因为里面的内容不可以被用户修改,用户也不能在这个文件夹里新增或删除。它跟接下来要讲的暂存区有很大的关系。
4-2. 暂存区
先着重讲一下隐藏文件夹“.git”,它是 Git 版本控制系统的核心所在,它包含了项目的版本控制信息,包括提交历史、分支、标签等。“.git”文件夹的作用如下:
- 版本控制信息存储:“.git”文件夹保存了项目的整个版本控制历史,包括每个提交的内容、作者、时间等信息。
- 分支和标签管理:Git 使用“.git”文件夹来存储和管理分支和标签信息,以便于在不同的分支之间切换,或者查看特定标签的快照。
- 配置信息:Git 的配置信息也存储在“.git”文件夹中,包括用户信息、远程仓库地址等。
- 暂存区:“.git”文件夹包含一个暂存区(stage,也叫 index),用于暂存待提交的修改,以便在提交时将其纳入版本控制。
- 工作树状态追踪:Git 通过“.git”文件夹来追踪工作树(working tree)中文件的状态,以便确定哪些文件已经修改、添加或删除。
综上所述,我们可以知道暂存区就在“.git”里面,还有 Git 为我们自动创建的第一个分支 master,以及指向 master 的一个指针 HEAD。
分支和 HEAD 的概念我们以后再讲,前面讲了我们把文件往 Git 版本库里添加的时候,分别用git add
和git commit
两步执行。其实本质上就是用git add
把文件修改添加到暂存区,然后用git commit
提交更改,把暂存区的所有内容提交到当前分支。因为我们创建 Git 版本库时,Git 自动为我们创建了唯一的 master 分支,所以git commit
就是往 master 分支上提交更改。
可以简单理解为,需要提交的文件修改通通放到暂存区,然后一次性提交暂存区的所有修改。
4-3. git diff 比较域
git diff
命令可以对比两个版本的差异,一般会结合一些常用的参数运行,如下图所示:
由上图可知,git diff
是只比较比较工作区和暂存区(最后一次 add)的区别;git diff --cached
是只比较暂存区和版本库的区别;git diff HEAD
是只比较工作区和版本库(最后一次 commit)的区别。
5. 管理修改
Git 之所以比其他版本控制系统性能优秀,主要就是它跟踪并管理的是修改,即每次版本之间的差异,而不是简单的文件。
例如,我现在重新修改一下 README.md 的内容,新增内容如下图:
通过git status
命令可以查看被修改的文件,再通过git diff
可以对比工作区和暂存区的不同。
然后git add
之后,再次查看仓库状态。
先暂时不提交到 master 分支,再次修改 README.md,修改如下图:
然后不进行git add
,直接用git commit
提交,此时可以发现提交是成功的,但是用git status
查看,还是存在工作区有被修改的文件。显然,第二次的修改并没有被提交。
那就证明了 Git 管理的是修改,当用git add
命令后,在工作区的第一次修改被放入暂存区并做好了被提交的准备,但是在工作区的第二次修改并没有放入暂存区,所以git commit
只负责把暂存区的修改提交了,也就是第一次的修改被提交了,第二次的修改不会被提交。
可以通过git diff HEAD
和git diff --cached
分别查看仓库的情况,可以看出工作区的文件和仓库的文件是有差别的,而暂存区和仓库的并没有区别。
6. 撤销修改
实际的项目开发过程中难免会犯错,比如 Bug 改错了位置、代码注释写错了等等。出现了错误的修改之后,就要及时的撤销修改,下面分别介绍不同场景下的撤销操作。
6-1. 撤销工作区的修改
假如因为某些原因,我们要放弃这一次工作区的修改,可以用git checkout
命令撤销修改。先使用git status
命令查看一下当前版本库的状态,当前有一个工作区的修改。
以前版本的 Git,第二行的括号里提示的是git checkout
,现在提示的是git restore
。这两个都是 Git 中用于撤销更改或者切换版本的命令,但是它们的使用方式和作用略有不同。
- git checkout:
- 用法:
git checkout <branch>
切换分支;git checkout <commit> <file>
恢复单个文件到指定提交版本。 - 主要功能:
- 切换分支:将工作区的内容切换到指定分支的最新状态,包括所有文件和文件夹。
- 恢复文件:将单个文件的状态恢复到指定提交版本的状态。
- 用法:
- git restore:
- 用法:
git restore <file>
恢复工作区中的文件到暂存区的状态;git restore --source=<commit> <file>
恢复单个文件到指定提交版本;git restore --staged <file>
将暂存区的文件恢复到工作区。 - 主要功能:
- 恢复工作区:将工作区中的文件恢复到暂存区或指定提交版本的状态。
- 将暂存区的文件恢复到工作区。
- 用法:
总的来说,git checkout
主要用于切换分支和恢复单个文件,而 git restore
则用于恢复工作区中的文件到指定状态,同时也可以用来操作暂存区的文件。两者的功能有交叉,但是在实际使用时需要根据具体的情况选择合适的命令。
这里我用git checkout -- README.md
来撤销修改,再次用git status
查看,工作区已经clean了,并且 README.md 已经恢复到了版本库的版本。
6-2. 撤销暂存区的提交
对 README.md 再修改一些内容,并用git add
提交到暂存区。
使用git status
查看当前版本库的状态,可以看到修改已经提交到了暂存区。
如果我们想撤销这次的提交,可以看到在上图中有个 git 命令已经给出了提示,可以用git restore
来撤销,之前的 Git 的版本是提示使用git reset
来撤销,我们还是用更广为人知的git reset
来撤销。
输入git reset HEAD README.md
,表示我们要从 HEAD 只向的分支恢复 README.md 文件。
6-3. 撤销本地版本库的提交
如果已经把修改后的文件提交到 master 分支上了,想要撤销是撤销不了的,只能通过回退版本(第 3 小节的内容)的方式来进行。现在还只是本地版本库,如果后续学到远程版本库,把修改提交到远程版本库,那可能就无力回天了,因为远程版本库不止一个人在用,可能会影响到其他人的工作。
因此,很多企业在合入远程仓库之前都会有一个环节叫代码审查(review),所谓的 review 就是指开发团队中的成员对其他成员编写的代码进行检查和评审的过程。代码审查是一种质量保证机制,通过让其他团队成员检查代码,可以发现潜在的问题、提供反馈和建议,以确保代码质量和项目整体的稳定性。
7. 删除文件
在实际工作中,删除文件也是常见的操作,在 Windows 系统常常使用手动删除,习惯使用命令行的同学会使用rm
。例如,我们现在就把其中一个文件删除,并查看当前版本库的状态。
Git 已经感知到了工作区的 Hello.exe 被删除,但版本库中的文件还未删除,这时我们有两种选择:
-
使用
git add/rm <file>
命令更新修改到暂存区以备 commit。这个操作目的是为了删除文件做准备,如果在执行
git rm
之前,突然不想删除了,那就是第二种选择了。 -
使用
git checkout -- <file>
丢弃工作区的修改,即还原被删除的文件。
8. 查看提交历史
在提交了若干更新,又或是克隆了某个项目之后,通常都会回顾下提交历史,看看项目都做了哪些修改。而查看历史提交记录最简单而又有效的工具是git log
命令了。
我们去嵌入式大神稚晖君的 GitHub 上找一个项目并克隆下来,这是他的 Github 链接:peng-zhihui (稚晖) (github.com)。
我这里克隆了他的 HelloWord-Keyboard 项目,使用了git clone
命令。
我这里换了一个文件夹,并且在这个文件夹下重新打开了 Git 终端,克隆项目完成后,运行 Git 的目录下会多出一个项目为名的文件夹。
用cd
命令进入文件夹。
在此项目中运行git log
命令时,可以看到很多历史记录。
历史记录甚至多到滚动不完,可以按q
键退出浏览历史记录。
不传入任何参数的默认情况下,git log
会按时间先后顺序列出所有的提交,最近的更新排在最上面。会包含每个提交的 commit ID、作者的名字、电子邮箱、提交时间和提交说明。
git log
有许多选项(或者叫参数)可以有效搜索信息,例如需要显示每次提交所引入的差异,就可以加入参数-p
或--patch
(patch 就是我们常说的补丁)。
如果要限制显示的日志条目数量,可以输入短杆后加上数量,例如git log -3
,就会显示最近三次的提交信息。
如果想看到每次提交的简略统计信息,可以使用–stat
选项。
–stat
选项在每次提交的下面列出所有被修改过的文件、有多少文件被修改了以及被修改过的文件的哪些行被移除或是添加了。 在每次提交的最后还有一个总结。
关于git log
还有很多选项,这里就不一一赘述了,感兴趣的直接在这个链接学习:Git - git-log Documentation (git-scm.com)。
四、代码托管平台
1. 常见的代码托管平台
-
GitHub: GitHub 是最大的开源代码托管平台之一,提供了强大的版本控制功能和协作工具,支持 Git。
-
GitLab: GitLab 是一个开源的代码托管平台,提供了类似于 GitHub 的功能,但还包含了一些额外的功能,例如持续集成、CI/CD 等。
-
Gitee: Gitee(码云)是开源中国(OSCHINA)的一个类似于 GitHub 的代码托管平台,支持 Git 和 SVN,提供 Git 仓库托管、团队协作、代码审查、问题跟踪、持续集成等功能。它是由中国的一家公司开发和运营,旨在为国内开发者提供一个方便、高效的代码托管平台。
-
Bitbucket: Bitbucket 是由 Atlassian 公司提供的代码托管平台,支持 Git 和 Mercurial,提供了私有仓库、团队协作、持续集成等功能。
-
SourceForge: SourceForge 是一个老牌的开源软件托管平台,提供了代码托管、下载、论坛等功能,支持 Git、SVN 和 Mercurial。
-
Microsoft Azure DevOps: Azure DevOps(之前称为 Visual Studio Team Services 或 VSTS)是微软提供的一套全面的软件开发工具,包括代码托管、CI/CD、项目管理等功能。
-
AWS CodeCommit: AWS CodeCommit 是亚马逊提供的托管 Git 存储库的服务,可以与其他 AWS 服务集成,例如 AWS CodePipeline 和 AWS CodeBuild。
-
Google Cloud Source Repositories: Google Cloud Source Repositories 是 Google Cloud Platform 提供的托管 Git 存储库的服务,可以与其他 Google Cloud 平台的服务集成。
2. 注册代码托管平台账号
代码托管平台较多,这里只介绍其中两个平台的账号申请,而且都是以官方的文档为主要参考教程,分别是 GitHub 和 Gitee。推荐 GitHub 的原因是大部分开源项目都在 GitHub 上(包括 Linux kernel)。但毕竟 GitHub 的服务器在国外,国内访问很不方便(无法访问和超慢的响应时间是家常便饭),要正常访问,还需要翻墙(这事违法哈)。所以这里补充一个类型 GitHub 的国内平台,就是开源中国的 Gitee,同样可以有很多开源项目,也可以在 Git 上配置账号,与 GitHub 的体验差别不大,主要对英语不好同学很友好,基本都是中文的。
2-1. GitHub
注册 GitHub 账号建议先翻墙,然后看官方文档。
官方注册文档链接:《在 GitHub 上创建帐户》
2-2. Gitee
官方注册文档链接:《注册 Gitee 账号》
3. 为 GitHub / Gitee 配置公钥
3-1. GitHub 配置 SSH Key
在开始菜单中打开 Git Bash。
-
设置用户信息:
git config --global user.name "your name" git config --global user.email "your e-mail"
把双引号里面的内容替换成自己的,如下图:
-
创建SSH密钥:
ssh-keygen -t rsa -b 4096 -C "your e-mail"
同样把双引号的内容替换成自己的邮箱,不过创建密钥时会遇到一个界面静止不动,如下图:
这是要我们设置一下密码,这个 Key 我们只是个人使用,只要不设计军事项目,不需要密码,直接按三次回车跳过。
看到这个画面就是 Key 创建成功了。
-
复制公钥,在当前窗口输入命令
cd .ssh
回车,再输入ls
可以看到“.ssh”文件夹下生成了 Key 的密钥对。其中,id_rsa 是私钥,不能泄露出去, 是公钥,可以放心地告诉任何人。输入
cat id_rsa.pub
查看公钥,并复制。 -
登录 GitHub 网站,点击网页右上角的头像,在弹出的菜单中选择
Setting
。在左边的选项卡中,选中
SSH and GPG keys
,然后点击右边绿色按钮的New SSH key
。然后输入 Title,再粘贴密钥,最后点击下面的按钮
Add SSH key
。有时候可能要验证一下是不是本人操作,输入 GitHub 的密码验证就好了。
成功添加 Key 如下图:
-
测试连接,输入下面的命令行:
ssh -T git@github.com
如果看到类似于 “Hi username! You’ve successfully authenticated, but GitHub does not provide shell access.” 的消息,则说明 SSH 连接已成功设置。如下图:
[!NOTE]
为什么 GitHub 需要 SSH Key 呢?
因为 GitHub 需要识别出你推送的提交确实是你推送的,而不是别人冒充的,而 Git 支持 SSH 协议,所以 GitHub 只要知道了你的公钥,就可以确认只有你自己才能推送。
当然,GitHub 允许你添加多个 Key。假定你有若干电脑,你一会儿在公司提交,一会儿在家里提交,只要把每台电脑的 Key 都添加到 GitHub,就可以在每台电脑上往 GitHub 推送了。
3-2. Gitee 配置 SSH Key
Git 配置 Gitee 和配置 GitHub 过程一样,但是不能使用同一个公钥,所以这里需要生成新的 Key 的密钥对。
为了区分之前的 Key,我们用基于ed25519
算法的 SSH 密钥。
ssh-keygen -t ed25519 -C "your e-mail"
生成的密钥对如下:
用同样的方法复制密钥,然后登录 Gitee 网站和账号,点击右上角头像,弹出菜单,选择“设置”。
在左边的选项卡中选择 SSH 公钥的选项卡,输入标题和公钥,点击确定。
然后输入密码验证一下。
公钥成功添加。
测试方法和 GitHub 差不多,后面部分改成 gitee.com 就行。中间停顿的时候,输入yes
回车即可,看到类似于 “Hi username! You’ve successfully authenticated, but GitHub does not provide shell access.” 的消息,则说明 SSH 连接已成功设置。
4. 添加远程仓库
到目前为止,我们已经学会了很多的关于 Git 的基本操作。但是这些功能与之前的 SVN 之类的集中式版本控制系统相比也没有太大的差别,因为这些仅仅只是基本操作,而 Git 真正意义上的杀手锏是远程仓库。
Git 是分布式版本控制系统,同一个 Git 仓库,可以分布到不同的机器上。那么是怎么分布的呢?最早,只有一台机器有一个原始版本库,此后,别的机器可以“克隆”这个原始版本库,而且每台机器的版本库其实都是一样的,并没有所谓的主次之分。
在实际工作场景往往是这样的,找一台电脑作为“服务器”,其他人都从这个服务器仓库“克隆”一份到自己的电脑上,并且各自把各自的提交推送到服务器仓库里,也从服务器仓库中拉取别人的提交。服务器在这个过程中充当数据转接的角色,仅仅作为数据共享的中介。
Git 服务器可以自己搭建,也可以使用代码托管平台,对于初学 Git 的小伙伴来说,搭建自己的服务器不太现实,所以直接注册 GitHub 或者 Gitee 的账号,使用外部的服务器来托管代码是最实际最高效的选择。前面三小节已经把账号注册完毕,并通过 SSH Key 加密连接了,现在我们就可以直接构建自己的远程仓库。
4-1. 构建 GitHub 的远程仓库
登录 GitHub 账号,先点击右上角的小三角形,在下拉菜单中选择New repository
,此时页面会刷新,在Repository name
下面的输入框中,输入仓库名,前面一直用git_test
这个仓库名,这里要使用一样的。最后点击下面的绿色按钮Create repository
创建仓库。要提醒一点,就是仓库名不能重复,如果你输入了仓库名是自己帐号里已存在的仓库,是无法创建成功的。
创建成功会刷新成如下图的界面。
回到本地,打开本地仓库git_test
的文件夹,在这个文件夹下打开 Git Bash,粘贴下面的命令(上图红框复制的)。
git remote add origin https://github.com/zhengxinyu13/git_test.git
远程库的名字就是origin
,这是 Git 默认的叫法,也可以改成其它的,不过origin
这个名字一看就知道是远程库,所以建议不改。
然后执行下面的命令:
git branch -M main
简单解释一下这个命令,这是用于将当前仓库的默认分支名称从旧名称(如master
)更改为main
。这在许多情况下是为了避免使用具有历史负担的术语,比如master/slave
或master
分支的历史含义。
具体来说,git branch -M main
命令执行以下操作:
-M
选项表示move
或rename
。它会重命名分支,如果分支已存在,则会强制覆盖。main
是新的分支名称。
这个命令将当前仓库的默认分支重命名为 main
,如果之前存在名为 main
的分支,则会覆盖它。
将本地库同步到远程库可以执行下面的命令:
git push -u origin main
由于远程仓库是空的,第一次推送main
分支时,加上了-u
参数,Git 不但会把本地的main
分支内容推送的远程新的main
分支,还会把本地的main
分支和远程的main
分支关联起来,在以后的推送或者拉取时就可以简化命令。执行命令后,会提示登录 GitHub 的账号,点击Sign in with your browser
。
然后会自动跳转到浏览器,点击Authorize git-ecosystem
。
然后输入密码,点击Confirm
。
如果看到这个界面,就表示配置成功。
Git 这边也会提示,表示已经成功把本地仓库推送到远程仓库了。
推送成功后,可以立刻在 GitHub 页面中看到远程库的内容已经和本地一模一样的文件了。
一旦有了远程仓库,本地仓库做出修改,需要同步到远程仓库的话,就可以通过命令:
$ git push origin main
这样就可以把本地的main
分支的最新修改推送到 GitHub 的远程仓库。
4-2. 构建 Gitee 的远程仓库
Gitee 创建远程仓库会 GitHub 基本上一模一样,而且界面是中文的,对英语不好的小伙伴很友好,操作过程我就不赘述了,登录 Gitee 后看图操作。
创建好后,在本地仓库的 Git Bash 上执行下图红框的命令。
如果前面已经把本地仓库和 GitHub 的远程仓库连接的话,再执行git remote add origin
是会失败的,如下图:
通常情况下,Git 不允许你添加同名的远程仓库,因为每个远程仓库都应该有一个唯一的名称。
如果你确定要将远程仓库更改为新的地址,你可以使用以下命令:
git remote set-url origin https://gitee.com/Grayson_Zheng/git_test.git
这将更新现有远程仓库 origin
的 URL 为指定的新地址。
不过这么做会使本地仓库与 GitHub 的远程仓库失去连接,后面推送只能推到 Gitee 上,这就需要后面的克隆仓库来解决了。
执行git push -u origin "master"
时会出错,这是因为本地仓库现在的分支名为main
,而不是master
,所以改成main
之后就可以了。
同样需要登录 Gitee 的账号,这一步比 GitHub 简单多了。
执行成功可以看到下图红框的内容。
Gitee 的远程仓库也同步完成。
5. 克隆远程仓库
由于有远程仓库,本地仓库即使不小心删除了,也可以通过克隆仓库找回。
克隆仓库需要用之前提到的命令git clone
,后面跟上远程仓库的地址就可以了。其实,不管是 GitHub 还是 Gitee ,它们给出的地址不止一个,实际上 Git 支持多种协议,比如 SSH、HTTPS 等。
以克隆git_test
这个项目为例,GitHub 的操作是先点击绿色按钮Code
,然后复制链接。
再git clone
后面粘贴链接就可以克隆了,如下所示:
git clone https://github.com/zhengxinyu13/git_test.git
而 Gitee 也是差不多的操作,在项目页面点击橘色按钮克隆/下载
。
弹窗中,可以直接复制链接,比 GitHub 跟人性化一些的是,git clone
已经包含在里面了,不需要额外再输入。
[!CAUTION]
需要注意的是,由于这个仓库虽然分别存放在 GitHub 和 Gitee 上,但是仓库的名字是一样,如果两个一起克隆就要注意,不能放在同一个文件夹下面,要分开存放,毕竟克隆下来就是一个本地仓库,本地仓库本身就是个文件夹,同一级目录下是不允许有两个同名文件或文件夹的。
五、分支管理
1. 为什么需要分支
分支是版本管理系统中的重要概念,一个版本库中的不同分支互不干扰、完全独立,直到分支合并那一天。那么实际工作中,分支的作用到底是什么呢?
[!NOTE]
假设你准备开发一个新功能,但是需要两周的时间才能完成,第一周你写了 50% 的代码,如果立刻提交,由于代码还没写完,不完整的代码库会导致别人不能干活了。如果等代码全部写完再一次提交,又存在丢失每天进度的巨大风险。但是有了分支,就不需要担心这种事情了。你可以创建一个属于你自己的分支,这样团队其他人是看不到的,而且还继续在原来的分支上正常工作。你在自己的分支上干活,想什么时候提交都可以,直到开发完毕后,再一次性合并到原来的分支上,这样即安全,又不影响别人工作。
Git 分支主要是为了实现并行开发和代码管理的灵活性和效率。以下是一些使用分支的主要原因:
-
并行开发: 分支允许团队成员在不影响主代码库的情况下独立地开发新功能或修复错误。每个分支都是代码库的一个拷贝,团队成员可以在自己的分支上进行修改,然后在开发完成后将其合并回主分支。
-
功能开发: 每个功能可以在自己的分支上进行开发,这样可以保持主分支的稳定性。一旦功能开发完成并通过测试,就可以将其合并到主分支中。
-
错误修复: 当发现主分支中的错误时,可以创建一个新分支来修复该错误,而不会中断其他正在进行的工作。修复后,可以将修复的代码合并回主分支中。
-
版本管理: 分支可以用来管理不同的版本。例如,可以创建一个用于发布的稳定分支,以及一个用于持续开发的开发分支。这样可以确保发布版本的稳定性,同时继续进行新功能的开发。
-
代码审查: 分支可以用于进行代码审查。开发人员可以在自己的分支上进行工作,并在完成后请求代码审查。这样可以确保代码的质量和一致性。
综上所述,分支是 Git 的重要特性,可以提高团队协作的效率,同时保持代码库的整洁和稳定。但 Git 的分支是与众不同的,无论创建、切换还是删除,Git 都能快速完成,无论你的版本库是 1 个文件还是 1 万个文件。
关于怎么学会使用 Git 的分支,这里推荐一个很不错的学习网站:Learn Git Branching。
这个网站也是 GitHub 上的一个开源项目pcottle/learnGitBranching,有兴趣的可以克隆下来看看。
2. 理解分支
在前面提到的回退版本中可以知道,每次提交到版本库, Git 都会把这些提交版本都串成一条时间线,这是比较抽象的概念,所以可以参考下图:
在本地仓库中,每一次的修改都会被 Git 记录在这条时间线上,这条时间线也是一个分支,而且是主分支,以前版本的 Git 叫master
分支,Gitee 仍然保持这个叫法,现在版本的 Git 已经开始改名叫main
分支了,而且 GitHub 也同步了这个叫法。
前面我们还提到有个 HEAD 指针指向最后一个修改,其实这不是一个严谨的说法。严格来讲, HEAD 指向的是当前分支(更专业的说法是对当前所在分支的符号引用),而master
(或main
)才是指向提交的节点。一开始时,HEAD 只指向master
(或main
),而master
(或main
)指向一个初始节点,这个节点是在仓库初始化之后就有了。之后每一次修改提交都会新增一个节点,同时也会使master
(或main
)指向新的节点。因此,随着版本不断地迭代修改,master
(或main
)这条分支也会越来越长。
[!NOTE]
从个人角度来讲,这块内容比较抽象,理解起来会比较困难,因为当时我学习 Git 的时候也是这样的状态。后面我把分支类比成数据结构里的链表,把分支类比成链表的头指针,把修改类比成链表的结点。每次有新的提交,就当作有新的结点插入了链表,并且让头指针(分支)指向新结点(最新的修改)。
3. 分支的创建与合并
前面我们提到了一个场景,就是新功能的开发可能需要很多时间,而在这段开发的时间里,如果功能还没做完就提交,可能会影响别人开发,如果等到功能开发好后在提交,那么中间要是出现电脑宕机等不可抗力的原因导致代码丢失,那也得不偿失。那么,既不影响别人开发,也不影响自己进度的办法就是创建一个自己的分支。
通常我们都会用dev
来命名这个分支,在 Git 中,dev
指的是“development”(开发)的缩写,它是一个常见的命名约定,用于表示开发阶段的分支,用于进行新功能的开发。
假设现在有个现在新建了版本库,里面写了初始版本的代码,当下如果用 Git 进行版本管理的话,可以用下图表示现在情况。
然后,对初始版本进行修改后,得到了C1
版本,提交后如下图:
这时,我需要添加一个新的功能到这个版本里面,但是又害怕破环了原来的代码,于是我就创建了一个新的分支进行开发。创建新分支并命名为dev
的命令如下:
git branch dev
此时有个需要注意地方,那就是 HEAD 指针。此时的 HEAD 还是指向主分支,如果我这时候修改代码并提交了,这个提交记录还是在主分支上,那我创建的新分支就没有任何意义了。因此,在创建新分支之后,要及时把 HEAD 指向新的分支,之后的提交记录才会记在新分支上。求换分支的命令如下:
git checkout dev
当然如果想在创建新分支后自动切换到新分支,可以用下面这个命令:
git checkout -b dev
这样就不需要用到git branch
命令了,可以省一条命令。
这时我写好了一部分的新功能,虽然没有完成全部功能,但是为了保险起见,我提交了这次的修改,于是有了C2
版本的代码,并且提交在了dev
分支上。
接着我有连续开发C3
版本和C4
版本,坏消息是,虽然我完成了新功能的开发,但是经过测试都没能达到预期,同时也影响到了原本好的功能。好消息是,主分支的代码还能正常运行。
终于,功夫不负有心人,当开发到C5
版本时,终于实现了功能,测试也没有发现 Bug,于是我提交了C5
版本。
然后再就把这次提交合入到主分支中去,这个过程需要将 HEAD 切回主分支,再进行合并。切换回主分支的命令是git checkout
,后面接上主分支名,如下:
git checkout main
切换主分支后,再用git merge
命令合并dev
分支,具体如下:
git merge dev
对于dev
分支来说,它看到了几个版本的迭代,但是对于主分支来说,只是C1
到C5
的迭代。合并之后,就可以放心的将dev
分支删除了。不过不需要担心曾经在dev
分支提交的各种记录会被删除,即使分支被删除,这个分支上的提交记录会被保留一段时间(直到 Git 触发了垃圾回收并删除了这些无用的提交)。删除分支的命令如下:
git branch -d dev
因为创建、合并和删除分支非常快,所以 Git 鼓励使用分支完成各种任务,合并后再删掉分支,这和直接在主分支上工作效果是一样的,但过程更安全。
4. 冲突解决
只要是团队开发项目,代码上的冲突是在所难免的,毕竟团队成员基本都在各自的分支里开发,那么合并分支就不可能一帆风顺,这时就要解决各种冲突了。
在 Git 中,所谓的冲突指的是当合并操作(merge)或重置操作(reset)尝试合并两个不同的修改时发生的情况。这种情况会导致 Git 无法自动解决修改之间的矛盾,需要人工介入解决。
常见的情况是,当你试图合并两个不同的分支,但这两个分支上的相同文件被修改了,且这些修改彼此之间存在冲突。例如,如果两个分支都修改了同一个文件的同一行代码,Git 就无法自动确定应该保留哪个版本的修改,因此会产生冲突。
当Git发现冲突时,会在受影响的文件中插入特殊标记来标识冲突的部分,例如:
<<<<<<< HEAD
这是当前分支(HEAD)的修改
=======
这是另一个分支的修改
>>>>>>> other_branch
冲突标记之间的内容就是冲突的部分。在解决冲突时,你需要手动编辑这些文件,选择要保留哪些修改,然后将冲突标记删除,以及合并双方的修改。一旦解决了所有冲突,你就可以继续完成合并操作。
下面举一个简单的例子,演示一下如何解决开发过程中遇到的版本库冲突的问题。
首先在现有的 Git 版本库创建一个新的分支feature
。
前面没有提到,这里补充一下,查看本地仓库有多少分支可以用git branch
命令,Git 会列出所有的本地分支,其中字体为绿色且前面带有星号的分支,就是当前被 HEAD 指向的分支。
此时我修改了 README.md 文件,并保存提交。
此时我切换回主分支,同样修改了 README.md 文件,并保存提交。修改的内容和上图一样,只是换了一个符号。
好了,现在两个分支都有对同一个文件的提交,如果这时候合并分支就会有冲突,Git 也会有提示合并存在冲突,需要手动解决,然后重新提交。
可以用git status
命令来查看存在冲突的文件。
这时我们再打开 README.md 文件,可以看到 Git 标记出不同分支的内容。
修改成下图所示的内容后保存,这个要修改成哪个分支的内容,全看实际的情况,我这里是随便选的。
解决冲突后,使用 git add
命令将解决冲突的文件标记为已解决,再用 git merge --continue
来继续合并进程。
用带参数的git log
也可以看到分支的合并情况(别慌,这些命令我也没记住,以前靠百度,现在靠 ChatGPT)。
git log --graph --pretty=oneline --abbrev-commit
5. BUG 分支
软件开发中,bug 就像家常便饭一样。有了 bug 就需要修复,在 Git 中,每个bug都可以通过一个新的临时分支来修复,修复后合并分支,然后将临时分支删除。
假设我现在接到一个修复代号为 101 的 bug 的任务时,而且这个 bug 相对比较紧急,所以我需要放下当前的工作优先去处理这个 bug,那么我就会创建一个名为 issue-101 的分支来修复它。不过,正在dev
分支上进行的工作还没有提交,只是添加到了暂存区而已。
此时我还不能提交,毕竟当前的进度还未完成,没办法提交。同时代号 101 的 bug 又需要在今天解决,那要怎么办呢?
在前面的 Git 工作流程中,有一个贮藏区(stash)一直没提到,现在就是它发挥作用的时候了。贮藏区可以把当前的工作现场“贮藏”起来,等以后需要恢复工作现场时,再把工作现场还原到“贮藏”前,进而可以继续进行未完成的工作。
现在用git status
查看版本库的状态,还存在未提交的内容。
用git stash
命令贮藏当前的工作状态,用git status
查看版本库的状态,可以看到工作区已经 clean 了,因此可以放心地创建分支来修复 bug 了。
这时就要注意了,先要确定以哪个分支的版本修复 bug,毕竟现在的 HEAD 指针还指向dev
分支,而且dev
分支上可能有很多提交是不完整的,这时就需要切换回主分支,以主分支的最后一个提交的版本为基准,去创建新的 bug 分支。
分支创建好后,就可以开始修复 bug 了。修复好后,提交修改,切回主分支,合并 bug 分支,最后删除 bug 分支。
这时候再回到dev
分支干活,但是工作区是干干净净的,需要去贮藏区恢复后才能继续。
用git stash list
命令可以查看当前贮藏区的所有贮藏记录,目前看来只有一条记录。
想要恢复成贮藏之前的状态,有两种情况:
-
恢复贮藏前的工作状态,不删除贮藏区的贮藏记录,命令如下:
git stash apply
后续想要删除要用
git stash drop
,而且还要指定删除哪条记录,不指定就是删除最新的,指定的话,命令如下,stash@{<index>}
里面的<index>
是记录编号,如上图的stash@{0}
。git stash drop stash@{<index>}
-
恢复贮藏前的工作状态,同时删除贮藏区的贮藏记录,命令如下:
git stash pop
我这里直接用git stash pop
,不需要用git status
,恢复的时候会直接显示版本库的状态。
6. 分支管理策略
在合并分支时,可以注意到有个“Fast-forward”的字样,这是 Git 中的一种合并模式。在 Fast-forward 模式(快进合并)下,Git 可以通过简单地将目标分支指向合并的分支的最新提交来完成合并,而无需创建新的合并提交。
在这种模式下,Git 只需将目标分支指向合并分支的最新提交,而不会创建新的提交来记录合并的过程。快进合并的好处是可以保持提交历史的线性和简洁,因为不会创建额外的合并提交。这在处理简单的合并情况时非常方便和高效。但这种模式有个缺点,那就是删除分支后,会丢掉分支信息。
所以有时候快进合并可能不太适用,比如当我想要保留合并操作的记录或者在合并过程中需要解决冲突时。在这种情况下,可以使用--no-ff
选项来禁用快进合并,强制 Git 创建一个新的合并提交。这样可以更清晰地记录分支的合并历史以及解决合并冲突所做的修改。
例如,我现在在dev
分支提交了一笔代码,准备合并dev
分支。如果想要保留提交信息,就要强制禁用 Fast forward 模式,Git 就会在 merge 时生成一个新的 commit,那么从分支历史上就可以看出分支信息了。
在dev
分支提交后,切回主分支,然后合并dev
分支时,带上--no-ff
和合并信息,命令如下(双引号是合并信息):
git merge --no-ff -m "merge with no-ff" dev
合并后看看分支历史,可以明显看到,前面演示的分支合并,和这次的禁用快进合并的分支合并有着明显的信息差别,禁用快进合并的分支合并操作,会记录下这次的提交是从dev
分支合并过来的。
在实际开发中,分支管理是一个至关重要的实践,它可以帮助团队更好地组织和协作。以下是一些基本原则和最佳实践,可以进行有效的分支管理:
- 主分支保持稳定性: 主分支(通常是 master 或 main)应该保持稳定,反映了生产环境中的代码状态。在主分支上应该只合并已经经过测试和验证的代码。
- 使用特性分支: 对于新功能或修复问题,应该创建专门的特性分支。每个特性分支都应该专注于解决一个特定的问题或实现一个特定的功能。这样可以使得代码更容易管理和测试。
- 及时合并主分支变更: 团队成员应该经常将主分支上的最新变更合并到自己的特性分支中,以避免特性分支与主分支之间的差异过大,导致合并冲突难以解决。
- 小步快走: 尽可能频繁地提交和合并代码变更,以减小每次合并的规模。这有助于降低合并冲突的发生率,并使得问题的定位和解决更加容易。
- Code Review: 所有代码变更都应该经过代码审查。通过 Code Review 可以发现潜在的问题并提高代码质量。通常,在代码审查通过后才允许将代码合并到主分支。
- 避免直接推送到主分支: 避免直接推送代码到主分支,除非是紧急修复或小的修改。这样可以确保所有的代码变更都经过了适当的审查和测试。
- 定期清理不需要的分支: 定期清理已经完成的特性分支或不再需要的分支,以保持仓库的清洁和可管理性。
- 合并策略选择: 根据团队的需求和项目的特点选择合适的合并策略,比如快进合并、非快进合并、rebase 等。
- 文档化: 在团队中建立清晰的分支管理规范和流程,并确保所有成员了解和遵守这些规范。文档化可以帮助新成员快速上手,并减少误解和混乱。
- 持续改进: 不断地评估和改进分支管理流程,以适应项目的发展和团队的需求。及时调整流程可以帮助团队更加高效地进行开发工作。
有了这些原则,实际的团队合作的分支其实就像下图这样(很多大型项目,上万条分支都是正常的)。
7. 特性分支
做软件开发总有无穷无尽的新功能要添加,从上图也可以知道,新功能一般都会在dev
分支上的基础上,构建一个feature
分支来开发。这也是多数团队默认的习惯,还是那个原则,不能让实验性代码破坏主分支。
假设我现在接到了 Leader 安排的新任务,要我开发一个新功能,任务的代号为 feat-013。
于是我在dev
分支的基础上,创建了feat-013
分支。
过程很顺利,我提交了新功能,并切回了dev
分支,准备把性能合并进来。
但就在这时,Leader 跟我说,预算不足,我负责的功能被砍掉了。然白干了,但是这个包含机密资料的分支还是必须就地销毁。但是用git branch -d
会发现删不掉这个分支,因为这个分支还没有合并。不过 Git 也会提示,可以用git branch -D
来删除分支。
8. 多人协作
从远程仓库克隆时,Git 会自动把本地的主分支和远程的主分支对应起来了,并且远程仓库的默认名称是origin
。要查看远程库的信息,用git remote
命令或者git remote -v
命令。
上图显示了可以抓取和推送的origin
地址。如果没有推送权限,就看不到push
的地址。
8-1. 推送分支
所谓推送分支,就是把该分支上的所有本地提交推送到远程库,推送时要指定本地分支。在 Git 中,要推送分支到远程仓库,可以使用 git push
命令。如下图:
在推送前,要确认推送的分支是哪一个,比如要推送主分支,那就先切换到主分支上,在执行git push
命令,后面的origin mian
表示远程仓库的main
分支,也就是我把本地主分支的提交,合并到远程仓库的主分支上。
同理,推送其他分支也是类似的操作,比如dev
分支,先切分支,再推送。
使用git branch -a
可以查看本地和远程仓库的所有分支信息。
不过要注意一点,不是本地所有的分支和提交都要往远程仓库推送。首先主分支,基本都是时刻与远程同步。其次是dev
分支,团队所有成员都想要在上面工作,所以也要与远程同步。最后,类似修复 bug 用的临时分支和用于开发新功能feature
分支,就看情况同步了。一般来说,本地用于修复 bug 的分支是不需要推送的(除非 Leader 要看你的进度),而feature
分支就取决于是单独开发新功能,还是团队协作开发了。
8-2. 抓取分支
多人协作时,团队成员都会往远程仓库推送各自的修改,以及向远程仓库抓取分支。不过要注意一点,抓取分支和克隆是 Git 中的两个不同的操作。克隆是指从远程仓库复制整个仓库的操作。当使用git clone
命令时,Git 会复制远程仓库的所有文件、提交历史、分支等信息到本地,并创建一个与远程仓库相同的本地仓库副本。
假设,我现在所处的团队中,我和另一个小伙伴被安排一起开发一个功能,我们一起克隆了同一个远程仓库,默认情况下,都是克隆远程仓库的主分支到本地。
但有时候开发需要克隆的是远程仓库的dev
分支(如上图,仓库远程仓库有两个分支),那么指定远程dev
分支可以用下面的命令:
git clone -b dev https://github.com/zhengxinyu13/git_test.git
如果一开始克隆时,没有加上-b
选项和指定分支,克隆的仓库则是默认克隆主分支,这时也不用着急把本地仓库删掉,可以创建远程 origin 的dev
分支到本地,如下命令:
git checkout -b dev origin/dev
现在我和小伙伴都用相同分支克隆下来的版本做开发,期间他把对某个文件的修改推送到origin/dev
分支,而碰巧我也对同个文件做出了修改,如果我也推送到origin/dev
分支,就会推送失败。因为小伙伴最新的提交和我试图推送的提交有冲突,解决办法也很简单,Git 也会提示用git pull
命令把最新的提交从origin/dev
分支抓下来,然后在本地合并,解决冲突后再推送。具体命令如下:
git pull origin dev
这就是多人协作的工作模式,一旦熟悉了,就非常简单。
9. 删除远程分支
要删除远程仓库中的分支,可以使用git push
命令来向远程仓库推送一个空的分支,实际上相当于删除远程分支。以下是具体步骤:
git push origin --delete branch_name
其中,origin
是远程仓库的名称,branch_name
是要删除的分支的名称。
例如,如果你要删除远程仓库origin
中的dev
分支,可以使用以下命令:
git push origin --delete dev
执行这个命令后,dev
分支将会被从远程仓库origin
中删除。
六、标签管理
1. 什么是标签
Git 标签(Tag)是用来标记某个特定提交的,通常用于标识版本发布、重要里程碑等。Git 的标签是版本库的快照,但其实它就是指向某个 commit ID 的指针,跟分支会像,但是分支可以移动,标签不能移动。
[!TIP]
那么为什么 Git 已经有 commit ID,却还要引入 tag 呢?
同样来假设一个场景,某个软件版本发布日,Leader 跟我说:“请把上周一的那个版本打包发布,commit ID 是 6a5819e…”,我心想:“一串乱七八糟的数字不好找!”
但是同样的场景,Leader 跟我说:“请把上周一的那个版本打包发布,版本号是 v1.2。”,我就会这么想:“按照 tag v1.2 查找 commit ID 就行了。”
可以这么理解,标签就是一个让人容易记住且有意义的名字。
2. 创建标签
在 Git 中打标签非常简单,先切换到需要打标签的分支上,然后用git tag
加上标签名称既可以了。
git tag
可以查看所有的标签,加上标签名则是给最新的提交打上标签。同时用git log
查看,也可以看到标签跟提交绑定在一起。
由于默认是最新的提交打标签,如果给之前的提交打标签,就需要找到对应的 commit ID 了。例如,我要给上图所示的“fix issues-101”打个标签,那我就先复制一下对应的 commit ID,再用git tag
打上标签。上图的 commit ID 太长了,可以用下面的命令获得简短 SHA-1 标识符(abbreviated commit):
git log --pretty=oneline --abbrev-commit
再用git tag
可以查看所有的标签,可以看多了个v0.9
的标签。注意,标签不是按时间顺序列出,而是按字母排序的。
标签还可以显示提交信息,用git show
命令加上标签名就可以了。
而且创建标签时,还可以顺带写上一些描述说明,用-a
指定标签名,-m
指定描述说明。
3. 管理标签
只要是人做的事都有可能犯错,因此标签也有可能会打错,但是没关系,Git 是可以删除标签的,同样是git tag
命令加上-d
选项就可以了。
目前所有的标签都只是在本地仓库,不会自动推送到远程仓库。如果要推送某个标签到远程仓库,可以用git push origin <tagname>
命令。
如果要一次性推送所有的标签,可以只用git push origin --tags
。
当把所有的标签都推送到远程仓库,发现有一个标签本来要删除,一时疏忽给忘了,那就需要先在本地把标签删了,然后用下面的命令删除远程仓库的标签,其中<tag_name>
是标签名。
git push origin :refs/tags/<tag_name>
这个命令的语法是通过将一个空的本地分支推送到远程仓库的标签位置来删除标签。如果要直接删除远程仓库中的标签,可以使用以下命令:
git push origin --delete <tag_name>
七、Git 自定义
1. 文件忽略
日常开发中,工作区可能有些文件是不需要提交的,例如一些测试功能时产生的 log,或者是数据库密码的配置文件等等。这些文件需要放在工作区,但是又不能提交,每次输入git status
时,又会显示 Untracked files,着实让强迫症患者抓狂。
好在 Torvalds 也是个强迫症晚期,在开发 Git 的时候也想到了这一点,可以通过.gitignore
文件来指定需要忽略的文件和文件夹。.gitignore
文件通常位于项目的根目录中,用于告诉 Git 哪些文件和文件夹不应该被纳入版本控制。
以下是一些常见的 .gitignore
文件的用法:
-
忽略特定文件或文件夹:
如果想忽略特定的文件或文件夹,只需在.gitignore
文件中添加它们的名称即可。例如:file_to_ignore.txt folder_to_ignore/
-
使用通配符:
可以使用通配符来匹配一类文件。例如,使用*
可以匹配任意字符序列,使用?
可以匹配单个字符。例如:*.log # 忽略所有 .log 文件 *.tmp # 忽略所有 .tmp 文件 secret_* # 忽略以 secret_ 开头的文件
-
注释:
可以在.gitignore
文件中使用#
符号添加注释。例如:# 这是一个注释
-
忽略整个文件夹:
如果想忽略整个文件夹及其所有内容,只需在.gitignore
文件中添加文件夹的名称即可。例如:node_modules/ # 忽略 node_modules 文件夹及其所有内容
[!CAUTION]
.gitignore
文件中的每一行都描述了一个忽略模式。模式可以是文件名、文件路径或者通配符,Git 会根据这些模式来确定哪些文件应该被忽略。
不过,现在也不需要从头写.gitignore
文件,这个 GitHub 上的项目《github/gitignore:》(点击跳转),为我们总结了几乎适用于所有软件开发的.gitignore
文件,可以根据自己开发项目所用的编程语言选择,例如,我常用 C/C++ 开发,我就下载对应的文件夹。
那么可能会有这样的问题,那就是有时候可能需要提交某个文件,刚好是被忽略的,那要怎么添加呢?
如果打开.gitignore
文件重新修改就很麻烦,也不高效,所以在把工作区提交到暂存区时,加上-f
选项,表示强制提交。具体如下:
git add -f <file_name>
2. 配置别名
在 Git 中,你可以通过配置别名来创建自定义的命令或简化常用的 Git 命令。可以在.gitconfig
文件中设置别名,也可以使用git config
命令来设置。
以下是一些常见的 Git 别名配置示例:
-
使用
git config
命令设置别名:git config --global alias.co checkout
这个命令会创建一个名为
co
的别名,用于执行git checkout
命令。 -
在
.gitconfig
文件中设置别名:
打开.gitconfig
文件,并添加类似以下内容的配置:[alias]co = checkout
这个配置会创建一个名为
co
的别名,用于执行git checkout
命令。 -
设置带参数的别名:
git config --global alias.br "branch -a"
这个命令会创建一个名为
br
的别名,用于执行git branch -a
命令。 -
设置自定义命令别名:
git config --global alias.history "log --pretty=format:'%h %ad | %s%d [%an]' --graph --date=short"
这个命令会创建一个名为
history
的别名,用于执行自定义的git log
命令,可以显示更简洁的提交历史。
设置好别名后,你可以直接使用别名来执行相应的 Git 命令,这样可以提高效率并减少输入量。
3. 配置文件
配置 Git 的时候,加上--global
是针对当前用户起作用的,如果不加,那只针对当前的仓库起作用。每个仓库本地的配置文件的位置都放在本仓库的.git/config
文件中。
Git 的配置文件包括三个级别:系统级别、全局级别和仓库级别。它们分别存储在不同的位置,作用范围也不同。
-
系统级别配置文件:
这个配置文件位于 Git 安装目录下的etc/gitconfig
文件中。它包含了对系统上所有用户都适用的配置,通常由系统管理员进行管理。你可以使用git config --system
命令来修改系统级别的配置,但可能需要管理员权限。 -
全局级别配置文件:
这个配置文件位于用户的主目录下的.gitconfig
或者.config/git/config
文件中(取决于操作系统和 Git 版本)。它包含了对当前用户所有仓库都适用的配置。你可以使用git config --global
命令来修改全局级别的配置。 -
仓库级别配置文件:
这个配置文件位于 Git 仓库的根目录下的.git/config
文件中。它包含了对当前仓库的特定配置。你可以使用git config
命令来修改仓库级别的配置,不需要任何特殊权限。
在配置文件中,你可以设置一些 Git 的行为选项、别名、用户信息等。以下是一个简单的示例:
[user]name = Your Nameemail = your.email@example.com[alias]co = checkoutci = commitst = status[core]editor = nano
这个配置文件设置了用户信息、一些常用的别名,以及指定了默认的文本编辑器。你可以根据需要在配置文件中添加、修改或删除配置项。
要查看当前的 Git 配置,可以使用git config --list
命令,它会列出当前生效的所有配置。
附录
1. 关于 commit 提交信息规范
编写规范的 Git commit 信息对于团队协作和项目维护非常重要。以下是一个适用于嵌入式开发的 Git commit 信息规范示例:
<type>(<scope>): <subject><description><footer>
<type>
:同样是描述 commit 的类型,可能包括:- feat:新功能
- fix:修复问题
- docs:文档修改
- refactor:重构代码
- test:增加或修改测试
- chore:构建过程或辅助工具的变动
<scope>
:描述 commit 影响的范围,可以是一个模块、驱动、功能名字等。<subject>
:简短描述 commit 的目的,用一句话概括。<description>
:详细描述 commit 的内容。对于嵌入式系统,可能需要说明与硬件相关的修改、驱动的更新、性能优化等。<footer>
:一些附加信息,比如关联的 issue、版本号、特定的硬件平台等。
例如:
feat(sensor): implement driver for temperature sensorAdded driver for XYZ temperature sensor to handle temperature readings.- Implemented initialization routine
- Added functions to read temperature in Celsius and Fahrenheit
- Tested on hardware version 2.1Fixes #321
在嵌入式开发中,还可能会有一些特定的标签或者约定,比如硬件版本、特定的编译器或工具链版本等信息,需要根据项目实际情况进行调整和添加。
参考资料
Git教程 - 廖雪峰的官方网站 (liaoxuefeng.com)
这个配置会创建一个名为 co
的别名,用于执行 git checkout
命令。
-
设置带参数的别名:
git config --global alias.br "branch -a"
这个命令会创建一个名为
br
的别名,用于执行git branch -a
命令。 -
设置自定义命令别名:
git config --global alias.history "log --pretty=format:'%h %ad | %s%d [%an]' --graph --date=short"
这个命令会创建一个名为
history
的别名,用于执行自定义的git log
命令,可以显示更简洁的提交历史。
设置好别名后,你可以直接使用别名来执行相应的 Git 命令,这样可以提高效率并减少输入量。
[外链图片转存中…(img-2u3qc6K0-1713703279247)]
3. 配置文件
配置 Git 的时候,加上--global
是针对当前用户起作用的,如果不加,那只针对当前的仓库起作用。每个仓库本地的配置文件的位置都放在本仓库的.git/config
文件中。
[外链图片转存中…(img-oEZbJta0-1713703279247)]
Git 的配置文件包括三个级别:系统级别、全局级别和仓库级别。它们分别存储在不同的位置,作用范围也不同。
-
系统级别配置文件:
这个配置文件位于 Git 安装目录下的etc/gitconfig
文件中。它包含了对系统上所有用户都适用的配置,通常由系统管理员进行管理。你可以使用git config --system
命令来修改系统级别的配置,但可能需要管理员权限。 -
全局级别配置文件:
这个配置文件位于用户的主目录下的.gitconfig
或者.config/git/config
文件中(取决于操作系统和 Git 版本)。它包含了对当前用户所有仓库都适用的配置。你可以使用git config --global
命令来修改全局级别的配置。 -
仓库级别配置文件:
这个配置文件位于 Git 仓库的根目录下的.git/config
文件中。它包含了对当前仓库的特定配置。你可以使用git config
命令来修改仓库级别的配置,不需要任何特殊权限。
在配置文件中,你可以设置一些 Git 的行为选项、别名、用户信息等。以下是一个简单的示例:
[user]name = Your Nameemail = your.email@example.com[alias]co = checkoutci = commitst = status[core]editor = nano
这个配置文件设置了用户信息、一些常用的别名,以及指定了默认的文本编辑器。你可以根据需要在配置文件中添加、修改或删除配置项。
要查看当前的 Git 配置,可以使用git config --list
命令,它会列出当前生效的所有配置。
参考资料
Git教程 - 廖雪峰的官方网站 (liaoxuefeng.com)
Git - Book (git-scm.com)