零、引

很多时候,有人用网盘共享代码,或者直接打一个归档发送代码的时候,我都十分头疼,说,为什么你不用Git,就会有人问我,Git到底是什么,更有甚者完全不知道Git,GitHub,GitLab甚至Gitee之间的区别,中间都沾染了一个Git,便慌了手脚,不知其所以。

没耐心时,我便解释道,Git是存代码的网盘,显然,这么解释并不准确。然而,对于没接触过版本控制的人或初学者,Git和GitHub甚至是划等号的,理解为代码云盘,其实并不然。

一、何为版本控制

那么究竟什么是版本控制(VCS,Version Control System),Wiki百科的定义是“维护工程蓝图的标准作法,能追踪工程蓝图从诞生一直到定案的过程。此外,版本控制也是一种软件工程技巧,借此能在软件开发的过程中,确保由不同人所编辑的同一程序文件都得到同步”。

好家伙,我直接你说这个谁懂啊.jpg

所以,为了容易理解,我们想象这样一个场景。

当你在写毕业论文的时候,慷慨挥洒数万字,一交查重红一半。这时候我们就需要对我们的论文进行修改,将重复的部分修正掉,然而,这就面临着一个问题,我们究竟是在原来的文件上直接修改并覆盖,还是另存成另一个文件呢。

大概率,我们是舍不得我们死掉数亿脑细胞憋出来的奇怪文字的,于是乎,另存为,重命名一气呵成:毕业论文2.0.docx,毕业论文2.1.docx,毕业论文2.1最终版.docx,毕业论文2.1最终绝对不改版.docx,毕业论文2.1最终绝对不改版-副本.docx。

一同操作之下,我们的毕业论文终于写好了,而我们的文件夹里多出了一堆莫名其妙的中间状态的文件,你说删也不是,万一哪天需要回溯查一下以前的日志呢;不删吧,乱七八糟,什么名字都有。更可怕的是,这些论文不仅是你自己改的,甚至还有你的导师直接帮你修改的部分片段,这下你犯了难,到底哪些是教授改的,那些是自己改的,到底要留哪些文档?

于是乎,你想到了一个绝妙的办法,那我何不直接只留一份文档,然后另外建一个表格,只要记录每次的修改内容不就得了。这是一个好办法,这也就是系统化的版本控制系统的雏形,就像下面这张表格。

提交ID 修改的文件 修改内容 修改人 修改时间
f0cf2288 毕业论文.docx 新增:创建文件 xxx学生 2022-01-05 15:00:00 ( UTC +8)
7e59772a 开题报告.docx 新增:关于背景的研究 xxx学生 2022-01-05 15:00:00 ( UTC +8)
8d90d5f5 毕业论文.docx 修改:变更标题为xxx xxx教授 2022-01-05 16:00:00 ( UTC +8)
d271eceb 毕业论文.docx 新增:实验数据xxx xxx学生 2022-01-05 16:30:00 ( UTC +8)
c72f6412 毕业论文.docx 修改:实验数据xxx为xxx xxx教授 2022-01-05 17:30:00 ( UTC +8)
7c0e4c8d 开题报告.docx 修改:变更标题为xxx xxx教授 2022-01-05 18:00:00 ( UTC +8)

这样,我们只需要保存最新版本的文件,而期间的全部修改,将有版本控制系统进行,在上面这个例子中,这个系统便是这个记录变化的表格。

根据此,我们可以发现,版本控制系统首先它可以保存最新版本的文件,一般称这个保存文件的空间为仓库;它可以记录每次修改的内容,一般称为提交(commit);它可以多人协同工作,即多个人维护同一个仓库中的文件。

二、为什么选择Git

现在,我们已经了解了什么是版本控制,那么我们到底应该用哪个版本控制软件呢?根据上文,我们了解到,其实版本控制只是一个“标准工作流程”,或者说是一种方法,那么为了实现这种方法,我们自然需要一个软件的帮助,不然每个代码文件我们都用excel表格来记录变化那岂不是脑子有点毛病。

其实,版本控制软件有很多,比如Git的爹BitKeeper,CVS,以及后面的SVN。

然而,在一众版本控制软件中,Git最终脱颖而出,成为这个时代的主流,这不乏其中的历史原因(有兴趣可以了解一下当年BitKeeper与Linux与Linus与Git的历史)。然而除了历史原因之外,也因为Git的本身设计足够优秀,我们简单对比一下Git与风靡一时的SVN便可一窥究竟。

不同点 Git SVN
部署方式 分布式 集中式
数据备份 分布式备份 服务端全量备份
同步 可提交在本地后与其他服务器或用户同步 依赖部署SVN的服务器
分支 自行创建的分支不会影响其他用户且速度较快 创建分支将影响全体用户且每次变化将引起所有用户同步
冲突处理 不中断提交,标记冲突文件,解决冲突 中断提交,后提交者需要自行解决冲突后重新提交
托管平台 有,且很多,且有诸多大企业背书 有,但少,且可靠性差

我们可以看出来,Git的一大特点即分布式,即每个使用者在本地机器上安装Git后,创建或拉取仓库后,你的本地将拥有该仓库的全部信息,当其他用户下线或中央服务器宕机时不会影响本地仓库进程,可先在本地提交更改后待远程仓库恢复后向全部分布式网络中的仓库推送修改。而使用SVN则高度依赖部署SVN服务端的服务器,当失去与服务端的连接则无法提交自己的修改。而分布式还有一大有点,当远程数据丢失时,由于分布式特性,每一个本地仓库都有着全量的仓库部分,不会出现由于服务器故障导致全部文件丢失的情况。

三、Git的安装

1. Linux

由于最早Git便是在Linux上开发的(实际上是Linus为了维护Linux的代码而开发的,可以参考Linux社区与BitKeeper当年的纠纷),所以现在很多Linux镜像都会默认内置Git镜像,我们可以使用terminal试一下到底有没有安装Git,即

git

如果出现的不是usage: git后面接了一串指令的指令帮助,则当前系统没有安装Git,然而由于Git的知名度,大多数包管理软件都会包含Git,我们只需要通过相应的包管理软件进行安装即可,比如

# CentOS等其他使用YUM包管理器的Linux发行版yum -y install git 
yum -y install git

# Debian或Ubuntu等其他使用APT包管理器的Linux发行版
apt-get install git

2. Windows

虽然Git脱胎于Linux,然而其迅速走红和其优秀的开源性,很快就有人将其移植到Windows与Mac设备上,但是我手上没有Mac设备,因此只讲Windows设备。

首先,我们可以在官网下载最新版本的Git for Windows安装包,一般选择64位即可。

下载后,我们只需要双击运行,一步一步按照顺序点下一步安装即可(我一般就是全默认设置,如果了解具体的参数设置可以自行搜索Windows下Git安装详细教程)

3. 配置

当我们安装好了以后,最后需要简单配置一下Git的用户信息,我们可以用

git config --list

来列出全部的Git配置。

# 配置用户名,将User改成自己的名字
git config --global user.name "User"

# 配置用户邮箱,将email@example.com改为自己的邮箱
git config --global user.email "email@example.com"

# 注:以上两行命令中的“--global”为全局指令,即加上该参数则对全局账户进行设置,如果不加则仅对当前仓库进行配置

四、Git命令行

1. 创建代码仓库

现在,我们下载并安装好了git,终于可以拿来管理我们的代码了。首先,我们需要创建一个仓库。

首先,我们需要准备一个工作区用来存放我们的仓库文件,即一个文件夹。Windows下可以直接右键新建文件夹,或者使用命令行

mkdir git-test

当我们新建好了文件夹后,我们需要进入到文件夹中,在文件夹内右键选择“在Windows终端打开”(Windows11下),或使用cd指令进入该文件夹

cd /xxx/xxx/git-test    #/xxx/xxx为该目录上级目录的绝对路径

当然,不事先新建文件夹也可以,可以在已经有代码文件的文件夹下执行下面的命令,或者由Git创建仓库工作区。

当我们进到这个文件夹后,在终端(也就是CMD)或者Git Bash中输入git指令。输入指令后,文件夹中讲初始化出一个.git的隐藏文件夹。

# 在本文件夹内初始化一个Git仓库
git init        

# 如果使用这个方式,则不需要先创建空文件夹,git会自动创建一个叫xxx的文件夹并将仓库初始化
git init xxx    

2. 添加/删除/修改文件

这个时候,我们可以在这个仓库文件夹中创建我们的文件,这个文件可以是任何格式,为了方便,我们C语言源码作为例子。创建一个hello.c的文件,在文件中写入如下代码:

int main(){
    printf("Hello World");
}

现在,我们保存文件。注意,此时的文件仅存在于我们的本地工作区,并未包含在Git仓库的暂存区中,我们需要使用git指令将该文件添加到仓库的暂存区

# 查询工作区与暂存区内的变化,每次变化都会记录,可以使用这个指令查看变化,无论是添加还是删除都会显示,以后不再赘述
git status

# 将hello.c存入暂存区
git add hello.c

# 如果创建了一个目录(文件夹)也可以使用add指令添加到暂存区(会将其内部子文件夹也进行添加)
git add folder-name

# 如果要提交的目录和文件太多,可以直接用.提交全部仓库根目录下的文件
git add .

# 如果对文件多处进行了修改,可以在后面加上-p参数对每处变化进行缺人
git add xxx -p

现在,我们已经将文件成功添加到Git的暂存区里了(注意,这里是暂存区,而不是已经提交到了git。为了保证事务的原子性,需要后面统一commit提交到git中)。

那么,当我们对文件进行删除的时候,我们也需要进行相关的操作:

# 将文件夹中的文件删除,并提交到暂存区。注意,当文件的修改或新增没有提交的情况下是不允许进行该操作的
git rm hello.c

# 将通过文件恢复,可以恢复rm及其他操作的更改
git restore .               # 恢复根目录
git restore folder-name     # 恢复指定目录
git restore hello.c         # 恢复指定文件

# 在Git中删除该文件,但是在本地工作区内不删除
git rm --cached hello.c

# 撤销操作,可以撤销上一步,具体的后面再提
git reset

除了删除,我们还可以进行重命名操作:

# 更改后将直接提交至暂存区
git mv 原文件名 新文件名

最后,对文件的修改,我们只需要对文件内容进行修改即可,修改后的文件我们依旧使用add添加指令提交到暂存区

3. 提交变化

现在,我们已经对文件进行了新建删除修改操作并已经提交到了暂存区,然而暂存区还并不是我们Git的仓库存储的位置,所以我们还需要将我们已经提交到暂存区的变化再次提交到Git仓库中。

# 将暂存区提交至仓库,每次提交需要写明提交信息,信息写在 -m 参数后
git commit -m "提交信息"

# 仅将暂存区的指定文件提交至仓库,多个文件用空格隔开
git commit hello.c -m "提交信息"

# 跳过暂存区,直接将工作区提交至仓库
git commit -a -m "提交信息"

# 重新提交一次并取代上一次的提交,如果提交内容没有变化,则只修改提交信息
git commit --amend -m "提交信息"

# 重新提交,并指定重新提交的文件
git commit --amend hello.c

4. 分支

当我们开发时,大多项目是分模块,由不同的开发人员开发完成的。对于不同的开发模块,我们希望开发该模块时不影响到其他的部分,如果我们所有人调试所有模块都在一起进行,那么如果有一个人出现问题所有人都要受到波及,这时候我们就要引入分支,当我们使用分支的时候,我们仅对当前分支进行修改调试,当调试通过后,我们通过分支合并进行对分支的整合以达到隔离开发与最终整合的目的,保证了开发互不影响。


# 列出所有本地分支
git branch

# 列出所有远程分支
git branch -r

# 列出所有本地分支和远程分支
git branch -a

# 新建一个分支,但并不切换进新分支
git branch name

# 新建一个分支,并切换到该分支
git checkout -b name

# 新建一个分支,并将指定提交id的commit拉取进该分支
git branch name commit-id

# 新建一个分支,与指定的远程分支建立追踪关系
git branch --track name remote-name

# 切换到指定分支,并更新工作区
git checkout name

# 切换到上一个分支
git checkout -

# 在现有分支与指定的远程分支之间建立追踪关系
git branch --set-upstream name remote-name

# 合并指定分支到当前分支
git merge name

# 选择一个commit,合并进当前分支
git cherry-pick commit-id

# 删除分支
git branch -d name

# 删除远程分支
git push origin --delete name
git branch -dr name

5. 撤销

当我们对文件进行了修改或删除,但是我们发现出了错误,此时我们需要对我们的操作进行撤销

# 将暂存区的文件恢复到工作区
git checkout hello.c

# 将指定提交id的指定文件恢复至暂存区与工作区
git checkout commit-id hello.c

# 将暂存区的所有文件恢复到工作区
git checkout .

# 重置暂存区的指定文件,与上一次提交保持一致,但工作区不变
git reset hello.c

# 重置暂存区与工作区,与上一次commit保持一致
git reset --hard

# 重置当前分支的指针为指定提交id,同时重置暂存区,但工作区不变
git reset commit-id

# 重置当前分支的指针指向指定commit,同时重置暂存区和工作区,与指定commit一致
git reset --hard commit-id

# 重置当前分支的指针为指定commit,但保持暂存区和工作区不变
git reset --keep commit-id

# 新建一个commit,用来撤销指定commit
# 后者的所有变化都将被前者抵消,并且应用到当前分支
git revert commit-id

# 暂时将未提交的变化移除,稍后再移入
git stash
git stash pop

6.标签与版本发布

当我们的开发告一段落,我们发布了一个正式的版本,这时候,我们可以通过标签系统将我们此时发布的版本进行标记发行

# 列出所有tag
git tag

# 新建一个tag在当前commit
git tag name

# 新建一个tag在指定commit
git tag name commit-id

# 删除本地tag
git tag -d name

# 删除远程tag
git push origin :refs/tags/name

# 查看tag信息
git show name

# 提交指定tag至远程
git push 地址 name

# 提交所有tag至远程
git push 地址 --tags

# 新建一个分支,指向某个tag
git checkout -b branch-name tag-name

# 将仓库所有文件归档以备发布,gzip为Linux下的归档工具,可指定格式为zip或者tar
git archive --format=zip master | gzip > file.zip

7. 查看仓库变化信息

# 显示有变更的文件
git status

# 显示当前分支的版本历史
git log

# 显示commit历史,以及每次commit发生变更的文件
git log --stat

# 搜索提交历史,根据关键词
git log -S keyword

# 查询全部符合筛选条件的提交描述的提交
git log HEAD --grep "关键字"

# 显示某个文件的版本历史,包括文件改名
git log --follow filename
git whatchanged filename

#查看某文件的全部提交及变化内容
git log -p filename

# 显示所有提交过的用户,按提交次数排序
git shortlog -sn

# 显示指定文件是什么人在什么时间修改过
git blame filename

# 显示暂存区和工作区的差异
git diff

# 显示暂存区和上一个commit的差异
git diff --cached filename

# 显示工作区与当前分支最新commit之间的差异
git diff HEAD

# 显示两次提交之间的差异
git diff commit-id1 commit-id2

# 显示某次提交的元数据和内容变化
git show commit-id

# 显示某次提交发生变化的文件
git show --name-only commit-id

# 显示某次提交时,某个文件的内容
git show commit-id:filename

# 显示当前分支的最近几次提交
git reflog

五、远程托管

现在,我们已经在本地完成了全部Git的操作,我们终于可以愉快的在本地使用版本控制了,再也不用担心哪次手残删了重要的文件了。

然而,现在还有另一个问题,那就是我们现在也只能在本地进行仓库的操作,那么怎么进行团队间的合作呢?

我们可以通过HTTP协议或者SSH连接到其他人电脑上IP上的Git,这样,我们就可以用远程指令在多个不同合作用户之间进行仓库同步了。然而,就像电话一样,如果两两都需要接一条线的话,那么我们本地需要维护太多个远程仓库了,在这种场景下,远程仓库托管就应运而生了。

我们可以将我们的仓库同步到一个开放的远程的仓库上,这个远程的仓库有着完整的域名配置,Git系统等等,以它作为中转,我们所有同步开发这只需要都连接到该远程仓库就可以实现同步了。

这个流程大概就是我在本地创建了一个仓库,然后申请了一个类似于GitHub的托管平台的账户,我将我的本地仓库推送到GitHub上面,如果是公开仓库的话所有人都可以看到,这时我拉来了另一个开发者一起开发这个项目,他从GitHub上拉取了我的仓库到他的本地,这样就存在了三个仓库,这也符合了Git分布式的概念。我在本地仓库进行工作,他在他的仓库上工作,当我们的工作都进行的差不多了,统一提交到这个远程的仓库托管平台就可以了。同时,我们也可以获得到该远程平台上最新的代码进度到本地仓库。

现在,全世界最大的代码托管平台当然是GitHub,在微软的钞能力加持下,GitHub已经成为了世界第一大的源代码托管平台。出了托管源代码,GitHub也发展出了很多新的与Git无关的功能,比如Release,Issue等等。因此,当我们学会使用Git后,自然需要学习如何使用GitHub,这篇文章就不再延申。

然而,由于GitHub的全英文,加之国内种种原因导致访问GitHub及其缓慢与困难,大大提高了GitHub的使用门槛,在这种环境下,Gitee应运而生。然而,虽然开源中国打着开源的旗号,实则做的是垄断的生意,以开源之名四处对开发者相逼。而且由于某种不可名状的不可抗力,Gitee的监管力度与开放程度堪称灾难(我之前有个很正常的没涉黄没侵权没有任何问题的仓库就给我封了)。

因此,我在此呼吁各位,千万不要使用Gitee,那玩意连注销账户都注销不了,被封的仓库删也删不要掉,发邮件根本没人理,所以懂得都懂。

除了垃圾到了极点的Gitee和访问极其困难的GitHub之外,还有一个托管平台大放异彩,那就是GitLab。

GitHub虽然“不作恶”,但是毕竟代码不是放在自己手上,不放心,而且以前也出过仓库被黑客勒索和GitHub用私有仓库训练AI的事情,保密性高的国内项目是不会放在上面的。而GitLab支持本地化部署,即部署在自己的服务器上,这就有了相当强的优势了(我上家公司用的就是私有化部署的GitLab),因此我前段时间也在自己的一个闲置服务器上部署了一个,相关详情教程见上篇博客。

说了这么多,言归正传,我们继续讲Git的远程操作指令。

# 下载远程仓库的所有变动
git fetch 远程仓库名

# 显示所有远程仓库
git remote -v

# 显示某个远程仓库的信息
git remote show 远程仓库名

# 增加一个新的远程仓库,并命名
git remote add 远程仓库名 远程仓库地址

# 取回远程仓库的变化,并与本地分支合并
git pull 远程仓库名 分支名

# 上传本地指定分支到远程仓库
git push 远程仓库名 分支名

# 强行推送当前分支到远程仓库,即使有冲突
git push 远程仓库名 --force

# 推送所有分支到远程仓库
git push 远程仓库名 --all

六、参考与感谢

[1] 维基百科.版本控制

[2] 维基百科.Git

[3] 廖雪峰.Git教程

[4] 阮一峰.常用 Git 命令清单

[5] Git文档

本篇内容为原创内容,采用CC BY-NC-SA 4.0协议许可
2022-01-05 22:47
UtopiaXC
于大连


尽管如此,世界依旧美丽