Git 是世界上最先进的分布式版本控制系统,学习并掌握 Git 的基本功能是代码人的基本素养(除非用其他的版本控制系统)。

Git 的安装

在 Linux 系统上安装 Git 比较方便,可以尝试输入 Git 命令,有一些系统是自带 Git,如果没有,大部分系统会提示你安装方法,比如 Ubuntu 中就可以使用命令 sudo apt-get install git 来进行安装。

而在 Mac 上,我没用过,所以请自行 Google。

在 Windows 上需要去 Git 的官网下载并安装,或者可以下载 GitHub Desktop 直接傻瓜式操作,不过不建议(因为有很多东西需要在 Terminal 调用 Git,而 GitHub Desktop 并不会安装 Git)。

Git 安装成功后,打开 Terminal 或者 Git Bash,输入 git -v 查看 Git 版本,如果出现版本号则说明 Git 安装成功。

Git 与 GitHub 的配置

Git 的配置

Git 安装完成后,要进行一定的配置才能够使用。

首先是设置名字和 Email 地址,在 Terminal 输入

git config --global user.name 'YourName'
git config --global user.email 'YourEmail'

这里的 --global 参数表示该配置用于本机器的所有仓库,当然也可以分仓库进行配置。

GitHub 的配置

GitHub 是一个免费的开源社区,可以将其看作一个免费的远程仓库,关于多人协作后面会提到。

使用 GitHub 需要先注册一个 GitHub 的账号,然后设置 SSH 将其与本地进行连接。

在 Terminal 输入命令 ssh-keygen -t ed25519 -C "[email protected]",之后会提示设置路径和密码,可以一路 Enter 跳过。之后 SSH key 就会在 ~/.ssh 文件夹中生成。该文件夹中有两个 Key,分别为 id_rsa 和 id_rsa.pub,前者是私钥,后者是公钥。私钥一定要自行保管,不能透露给他人,而公钥是我们进行连接所要使用的。

打开 GitHub 中的个人设置(点击主页右上角头像,出现的列表中的 Settings),其中有一个 SSH and GPG keys,选择该选项后点击 New SSH key 添加公钥即可。

添加完成后打开 Terminal ,输入命令 ssh -T [email protected],会出现类似如下的警告

> The authenticity of host 'github.com (IP ADDRESS)' can't be established.
> RSA key fingerprint is SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8.
> Are you sure you want to continue connecting (yes/no)?

输入 yes 后就可以看到如下输出

> Hi username! You've successfully authenticated, but GitHub does not
> provide shell access.

其中 username 是你在 GitHub 中的用户名。

版本库

版本库的创建

上面的配置完成后,就可以快乐地使用 Git 和 GitHub 了。首先我们需要创建一个版本库(repository,下称 repo 或仓库),一般来说我们倾向于在 GitHub 上创建 repo 并 pull 到本地。这里先介绍在本地创建 repo。

首先在 Terminal 打开要创建 repo 的文件夹,输入 git init 即可将该文件夹初始化为一个 repo,然后 git 就可以对该文件夹中的改动进行追踪了。

当然,也可以使用命令 git init dir 在当前目录创建一个名为 dir 的文件并将其初始化为一个 repo。

版本库的基本使用

创建好 repo 后使用 ls 命令来查看变化,会发现什么都没有。因为 git init 生成了一个名为 .git 的文件,而以 「.」开头的文件均为隐藏的,因此我们需要使用 ls -a 命令来显示所有文件。

使用命令 git add filename 将名为 filename 的文件添加到暂存区,当然,可以使用 git add -A 来添加所有文件到暂存区,这样使用命令 git status 就可以看到现在的状态是没有提交。使用命令 git commit -m 'msg' 进行提交,其中 msg 是描述此次提交的文字。然后就会看到这样的输出

[master (root-commit) df2d7bf] this is a msg
 1 file changed, 1 insertion(+), 0 deletions(-)
 create mode 100644 test.txt

显示了这次提交的改动——一个文件修改,一个文件内容添加,零个文件内容删除,并创建了一个 test.txt 的文件。而需要注意第一行的 df2d7bf,这是本次提交的 ID 的前面 7 位,其全长十分长,可以使用 git log 命令来查看。如果是删除则为 delete mode 100644 test.txt。之后新建、删除或修改一些文件,再输入命令 git status 就可以显示现在工作区和暂存区的状态,而 git diff 则可以显示工作区与暂存区的差异。

下面将区分 git 中的三个重要概念:工作区(Working Directory)、暂存区(Stage, Index)、版本库(Repository)。

工作区是 init 为 repo 的文件夹,就是我们进行作业的目录(不包括隐藏的文件),其与普通的资源管理器没有区别。

而暂存区则是储存我们的改动的区域。我们每次要提交的文件均记录在暂存区中,正如其名,暂存区是暂时储存文件版本的,你对文件的的下一次记录会覆盖这次记录,比如在 test.txt 文件中添加了一行文字后进行 add,然后再添加一行进行 add,这时不会显示你分两步添加了两行,而是一次添加了两行。想要把暂存区的改动保存为一个版本需要进行提交,在上一例中如果在添加第一行后进行提交,然后再进行添加第二行,就会发现改动是添加了一行。

版本库是储存我们的每个提交的版本的地方,它就是 .git 文件夹,不要对其进行任何改动,否则会出现不可挽回的错误(除非你想把这个版本库删除,直接删除 .git 文件夹即可)。版本库中包含了我们以往提交的所有版本信息,以及暂存区和一些分支。

下面是一些常用的 git 命令

  • git init:在当前目录初始化一个 repo;
  • git init dir:在当前目录下新建一个名为 dir 的文件夹并在其中初始化一个 repo;
  • git add file1 file2 …:将 file1, file2, … 添加到暂存区;
  • git add -A:将所有文件添加到暂存区;
  • git add .:将当前目录的所有文件添加到暂存区;
  • git rm file1 file2 …:删除工作区的 file1, file2, … 文件并添加此次删除到暂存区;
  • git rm –cached file:停止追踪该文件并将其在暂存区删除,但是不删除工作区的文件;
  • git mv file1, file2:将 file1 移动到 file2 的位置并添加到暂存区,常用该命令进行改名;
  • git commit -m ‘msg’:提交暂存区文件到 repo,其中 msg 为对提交的说明;
  • git commit file1 file2 … -m ‘msg’:提交暂存区的某些文件到 repo;
  • git commit -a -m ‘msg’:直接提交工作区已追踪的文件的变化到 repo(未追踪的不会提交);
  • git commit -am ‘msg’:同上;
  • git commit –amend -m ‘msg’:重做上次提交,即用新的提交覆盖上次提交,可用来更改 msg;
  • git commit –amend file1 file2 …:重做上次提交,并只包括指定文件的变化;
  • git log:显示历史提交;
  • git log -p:显示历史提交及内容变更;
  • git log -5:显示最近 5 次提交;
  • git diff:显示工作区与暂存区的区别,在后面加上 file 会显示该 file 的区别;
  • git diff –cached:显示暂存区与上一次提交的区别,在后面加上 file 会显示该 file 的区别;
  • git checkout – file:丢弃工作区文件 file 的修改

远程仓库的使用

在本地创建完 repo 后,一般为了团队协作我们需要将本地 repo 上传到 GitHub,然后让其他人进行拉取(pull)。为了将 repo 上传到 GitHub,我们需要在 GitHub 上创建一个远程 repo。首先打开 GitHub,然后在左边有一个绿色按钮(New),点击即可进行远程 repo 的建立。之后就是将本地 repo 和远程 repo 进行连接,打开 Terminal,输入如下命令

git remote add origin [email protected]:your_name/your_repo.git

命令中的 [email protected]:your_name/your_repo.git 在创建远程 repo 后在网页上有显示,进行更换即可。这里的 origin 可以换成其他的名称,不过一般都设置成 origin 表示是一个远程 repo。

一般来说,我们的本地 repo 默认分支为 master,远程仓库默认名为 origin,这样我们就可以使用下列命令进行推送(push)

git push -u origin master

第一次使用时,会出现一个 SSH 的警告,输入 yes 即可。

只有第一次推送需要使用参数 -u 来将本地的 master 分支和远程生成的 master 进行关联,之后只需要使用 git push origin master 进行 push。

而从远程拉取(pull)主要有两种方法。这里我们先介绍一种最简单的,就是使用 git pull 来将远程仓库的内容直接拉取到本地并进行合并,使用后工作区的文件会立刻变为远程仓库最新的文件(并不准确,分支部分会更加详细地介绍)。

下面是一些常用的远程仓库命令

  • git fetch origin:将远程仓库 origin 的内容拉取到本地远程仓库;
  • git remote -v:查看所有远程仓库;
  • git remote show origin:查看远程仓库 origin 的信息;
  • git remote add origin url:从 url 添加新的远程仓库 origin;
  • git pull origin master:拉取远程仓库 origin 的内容并与本地分支 master 合并;
  • git push origin master:上传本地分支 master 到远程仓库 origin;
  • git push origin master –force:强行上传,即使有冲突;
  • git push origin:将所有本地分支上传到远程仓库。

分支管理

分支的基本操作

在 Git 中,我们的每次提交都会使版本向前移动一次,而这些版本构成了一条时间线,我们称之为「主分支」,即 master 分支。在分支中存在一个指针指向本分支的最新文件,如 mater 分支的指针为 master。

如果我们想对 master 分支文件进行更改,但是又害怕更改会破坏 master 分支的文件结构,如多人协作的时候更改一个文件可能会导致其他人的某些相关联的文件出问题;又或者是仅仅用不同分支来储存不同的文件。两个分支之间互不干扰。

在 Git 中还有一个指针 HEAD,指向当前工作的分支。因此我们主要有两类指针,一是指向分支提交的指针,另一个是指向当前分支指针的指针。在理解了两个指针后,我们就可以进行分支的操作了。

在 master 分支中,如果我们想要更改某个文件而不影响该分支,我们可以使用命令 git switch -c newbranch 来创建一个名为 newbranch 的分支。该分支的内容与 master 的内容完全一致。因此,新分支只是一个指针,和 master 分支同样指向最新的提交,因此我们可以十分快速地进行分支的切换,即只需将 HEAD 指针从 master 指针指向 newbranch 指针。

而我们在 newbranch 分支中可以进行和 master 分支中一样的修改、提交,在修改提交后 master 分支的指向不变,而 newbranch 分支将指向最新的提交。因此我们使用命令 git switch master 来切换到 master 分支,这时我们就会发现之前的更改都不见了。 上文中 git switch -c newbranch 命令其实是 git branch newbranchgit switch newbranch 两个命令的结合,前者是用于创建新的分支,而后者是切换分支。其中 -c--create 的简写。我们也可以使用 git checkout -b newbranch 来创建并切换分支,用 git checkout newbranch 来切换分支。

在 newbranch 分支中,我们做完需要做的工作后,可能需要将 newbranch 分支的更改合并到 master 中去。这时就需要用到 Git 的合并:首先切换到 master 分支,然后使用命令 git merge newbranch 进行合并。这会使 newbranch 分支变更的内容合并到 master 中,当然可能出现冲突问题,这一点我们下面再讲。

在合并了分支后,我们能够使用命令 git branch -d newbranch 来删除分支,这样就完成了简单的分支工作流程。如果我们在合并之前就需要将该分支删除,上面的命令是行不通的,这时需要使用命令 git branch -D newbranch 来进行删除。

而如果我们在某分支上的工作并未完成,却需要切换到另一个分支上进行工作,这时就需要暂时将工作区储存起来,因为 Git 无法在切换分支时保存当前分支的工作区和暂存区。使用命令 git stash 即可将现有的工作区暂时储存,下次需要使用的时候用命令 git stash list 可以查看暂存区的文件,然后使用 git stash applygit stash pop 进行恢复,前者恢复后不会删除 stash 记录,而后者会进行删除。如果想要直接删除可使用命令 git stash drop。当然,也可以进行多次 stash,如果想要将上面的命令运用在某个 stash 上,只需输入 git stash apply stash@{0} 即可,其中 apply 和 0 也可换成别的命令或参数。

冲突解决

如果我们在 master 和 newbranch 分支同时进行了提交,也就是说 newbranch 分支并非领先 master 分支一个(或数个)版本,那么我们就可能出现合并冲突

Auto-merging file
CONFLICT (content): Merge conflict in file
Automatic merge failed; fix conflicts and then commit the result.

这样 git 就会将修改合并,显示在产生冲突的文件中。使用 git status 也可以查看冲突文件。

我们打开冲突文件后,就会发现 Git 使用 <<<<<<<=======>>>>>>> 这三行来标记不同分支的内容,我们修改保存后提交即可完成分支合并。

分支的合并模式

在上一节中我们使用的合并方式被称为 Fast Forward 模式,这种模式下我们会直接将 master 分支的指针指向 newbranch,这个合并是十分迅速的,但是这种合并方式是不会记录分支合并的操作的。

如果我们想要保留合并记录,我们需要添加参数 --no-ff-m,即使用命令 git merge --no-ff -m 'your msg' newbranch,其中 'your msg' 是合并时的描述,因为该合并模式会在合并时进行一次 commit,需要 -m 参数。

使用命令 git log --graph --pretty=oneline --abbrev-commit 可以以图的形式展示提交和分支处理。

还有一个命令 git rebase,可以将看起来混乱的分支提交合并等转换成一条直线,我也不太理解有什么用。这样虽然会使 commit 的历史变得更为美观,但是会导致原始数据的丢失,因此不要在远程仓库已存在且有其他人在其上工作的时候进行此操作。如果只是私人仓库,或者第一次提交给远程仓库,可以为了美观而使用这个命令。

分支的基本使用方法

一般来说,我们使用分支管理是为了保证代码的稳定性,即 master 分支的稳定。而我们的每次修改都应在分支上进行,这样就会保证 master 分支不被影响,开发出现错误的话也不会出现太大的损失。而如果我们是进行多人协作的话,我们需要一个 dev 分支来保存总的开发,在每一个大版本开发完成后将 dev 分支 merge 到 master 分支中。而每个人在进行开发时就从 dev 分支上创建个人的分支,以防止互相影响,在工作中 merge 到 dev 分支上即可。

同时,对于每一个新功能,或者修改 Bug,都建议在分支上进行工作,然后 merge 到 master 上。

Bug 修复

如果在 master 中发现 Bug,我们可以创建一个基于 master 的分支然后在其上进行修改,之后 merge 一下即可。但是在 master 中存在的 Bug 也会在 dev 和个人分支中存在,因此我们需要在修复完 Bug 后将 master 上的修改应用到 dev 上。我们只需要记下 master 中修改 Bug 的那次提交的 ID,比如 df2d7bf,然后在 dev 分支使用命令 git cherry-pick df2d7bf 即可将 master 中的修改应用到 dev 分支。注意,该命令只是修改 master df2d7bf 提交中修改过的文件,而其他文件并不会受影响。

远程协作

当其他人想要参与我们的项目的时候,我们一是可以将对方的 SSH key 添加到我们 GitHub 上,但是这样就将自己 GitHub 上所有 repo 的读写权力都分享出去了,因此不建议这样做。安全的方法是在 repo 的设置中的 Collaborators 添加对方,这样对方就可以对该 repo 进行修改了。

当对方 clone 远程仓库时,只会 clone master 分支,如果我们需要同时在 dev 分支上进行修改,那么就需要使用命令 git switch -c dev origin/dev 来将远程仓库(origin)的 dev 分支 clone 到本地的 dev 分支。如果我们的本地仓库并未与远程仓库进行链接,需要使用 git branch --set-upstream-to=origin/dev dev 来将 origin/dev 和 dev 进行链接。

如果一个人修改了 dev 分支并 push 到了远程仓库,同时我们这边也进行了修改,但是还未进行 push。此时我们使用 git push origin dev 可能是行不通的,此时我们需要先将远程仓库 pull 下来,然后进行冲突处理,之后即可成功 push。

在远程仓库一节,我们说 pull 和 fetch 有区别,在这里就可以详细解释了:fetch 只是将远程仓库 clone 到本地远程仓库,而工作区并未发生变化。此时我们就需要将远程仓库与本地仓库进行合并,例如对 dev 分支进行合并,需要切换到 dev 分支并使用命令 git merge origin/dev 将本地远程仓库的 dev 分支与本地的 dev 分支进行合并。而 pull 则将这两个过程一步完成了。如果远程仓库与本地仓库的冲突较多,推荐使用 fetch + merge 的方式处理。

版本回滚

版本回滚本来应该在版本库就介绍的,因为这是一个十分常用的功能。但是在分支管理后再进行介绍有助于理解这个过程。版本回滚有许多情况和实现方式,下面将分开介绍。

版本回退(reset)

如果我们想回退版本到上个版本,可以使用命令 git reset 来进行版本回退,其有三种不同的模式:

  1. mixed:git reset --mixed HEAD^ 命令可以将版本回退到上个版本,重置暂存区的文件与上一次提交的一致,工作区内容不变,此为默认参数,可不加 --mixed
  2. soft:git reset --soft HEAD^ 命令可以将版本回退到上个版本,暂存区和工作区不变,并将由于回退导致的变化储存到暂存区;
  3. hard:git reset --hard HEAD^ 命令可以将版本回退到上个版本,工作区和暂存区均回到上一版本。

reset 的实质是移动分支的指针以及 HEAD 指针,因此使用 hard 模式时可以做到分支之间的转移git reset --hard dev 将 master 的指针指向 dev 指针所指向的版本。

下面将区分这三种模式的使用场景:

  1. hard:需要放弃本地的所有改动时可以使用该参数;需要舍弃某次提交之后的所有提交后可以使用;
  2. soft:如果我们在两个大阶段间频繁进行多次没有多少意义的 commit,那么可以使用 soft 模式将某版本后的所有提交集成为一个提交,达到简化版本提交历史的目的;
  3. mixed:类似 soft,可以在回退后进行 git add -A 来达到简化提交历史的目的;某次提交了错误的文件,但不能全部舍弃。

而上文中出现的 HEAD^ 代表上一个版本,而 HEAD 代表这一个版本,HEAD^^ 代表往前两个版本,以此类推……若版本数过多,可以使用 HEAD~100 来代替往前 100 个版本。同时也可以使用提交的 ID 进行回退。

如果不小心错误地回退,可以使用 git reflog 来查看历史命令找到版本 ID。

版本还原(revert)

版本回退会将版本直接「退」到指定版本,而还原则可以将某版本的提交取消而不影响后续提交,即如果上上个版本添加了字母 A,上个版本添加了字母 B,则 revert 到上上个版本时只会删除 A 而不删除 B(当然,这只是个例子,Git 是按照行来进行追踪的)。只需要使用命令 git revert ID 即可实现版本还原。

标签管理

使用命令 git tag yourtag 即可将标签 yourtag 打在最新的提交上,如果需要指定某提交,只需要在最后加上版本号。使用命令 git tag 即可列出所有标签。

如果想要创建有具体说明的标签,可以使用 git tag -a yourtag -m 'msg' ID 来指定标签名和说明,而使用 git show yourtag 可以看到说明文字。

使用命令 git tag -d yourtag 即可删除某个指定的标签。

标签只存在于本地而不会上传到远程,如果想要上传需要使用命令 git push origin yourtag 推送某一标签或 git push origin --tags 来推送所有标签。而删除远程标签需要使用 git push origin :refs/tags/yourtaggit push origin -d tag yourtag

设置忽略文件

有时候需要在工作目录放一些不能提交的文件,如果每次都要注意 add 的时候不把它加进去未免过于繁琐、简陋,我们码代码也需要矜持优雅。因此我们可以在 Git 工作区的根目录创建一个文件 .gitignore 然后添加指定文件或文件类型即可,具体可参考这里。如我们不需要提交所有的隐藏文件和 txt 文件,只需要在 .gitignore 中添加两行

.*
*.txt

即可,这里我们要注意,.* 将忽略 .gitignore 文件,因此我们还需要加入一行

!.gitignore

来防止 .gitignore 文件不被追踪。也可以使用命令 git add -f .gitignore 进行添加。

如果我们发现某文件无法被 Git 追踪,那么可以使用 git check-ignore -v file 对 file 进行检查,这会传出 .gitignore 中忽略该文件的规则。

后记

终于把 Git 的笔记写完了,这里就推荐一下一些好用的工具吧。

首先是 GitHub 官方的客户端 GitHub Desktop,我在数学建模中和队员们就是使用这个进行文件同步,对于计算机小白也十分友好,只需要按个按钮 commit,push 和 pull 就行了。

其次是 SourceTree,这是一个免费的 Git 图形界面工具,GitHub Desktop 只支持本地 repo 和托管到 GitHub 上的 repo,如果你是使用别的托管平台或者自己搭建 Git 服务器,就可以使用 SourceTree 进行图形化管理。

然后就是我最喜欢的 VS Code 插件组合,GitLens 和 Git Graph,两个完全足够使用,配合 Power Shell 和 VS Code 其他的插件,能让写代码变得「一体化」:不需要再打开其他软件进行 Git 的操作,只需要在内置的 Power Shell 或插件进行使用即可。