Git 的基本使用
什么是 Git
在使用 Git 前,了解 Git 对于之后的学习会有很大的帮助。
Git 是一个分布式的版本控制软件,最初的目的是为了更好的管理 Linux 内核的开发。相比较于其它的软件版本控制系统,虽然总体上用起来与它们十分的相似,但是在对于信息的存储和认知上有很大的差异。主要体现在:
-
直接记录每个文件的快照,而不是进行差异的比较
- 对于其它的软件版本控制系统,如 SVN,在每个软件版本中都是通过记录与最初文件的差异来进行版本控制的。而 Git 则不同,每个版本都是通过创建之前版本文件的一个快照,这有点类似与一个小型的文件系统,当然,为了效率,Git 在每个版本中对于没有修改的文件使用一个链接指向之前的文件。因此当每次切换版本时,你都会感觉到非常快。
-
几乎所有的操作都在本地执行
- Git 是一个分布式的软件版本控制软件,在你自己本地也可以存储对应的文件,这样极大地提高了系统的容错性和容灾性。在你每次提交时,都是只会访问自己本地的文件系统,只有当你希望将自己的软件版本推送到远程服务器时,此时才需要访问互联网。
-
Git 可以保证完整性
-
Git 中的所有数据在存储前都会计算校验和,然后通过校验和来引用对应的版本。因此在文件有任何改动的情况下,Git 都能立即发现。
-
在每次提交后,Git 都会有一个校验和来引用对应的版本,因此在你执行提交之后,很难导致数据的丢失。
-
Git 的几种状态
理解 Git 的几种状态对于学习是极其重要的,Git 总共分为三种状态:***已修改(modified)***、***已暂存(staged)***、***已提交(committed)***。
- 已修改(modified) 表示已经修改了 工作区 的文件,但是还没有提交。
- 已暂存(staged)表示已经将修改的文件放入了 暂存区,使之包含在下次提交的快照中
- 已提交(committed)表示已经将暂存区的快照文件放入了 本地仓库 中
使用图来表示可能会直观一些:
请牢记这三种状态和这三个区,这是 Git 的核心。
一般的 Git 流程如下:
- 在工作区内修改了文件,此时 Git 的状态更新为 已修改
- 将修改后的文件加入暂存区,此时 Git 的状态为 已提交
- 提交更新,将暂存区内的快照文件存入本地数据库,此时 Git 的状态为 已提交
Git 的使用
运行前的一些配置
在使用 Git 前,需要配置一些相关的变量,如用户名、邮箱、默认文本编辑器等,只有配置了这些必须的属性,Git 才能正常使用。当然,Git 还有一些其它的配置信息,可以通过 git config --list --show-origin
来查看相关的配置属性以及所在的位置,但是 用户名、邮箱是必须配置的,因为 Git 在每次提交时都会将这些信息写入,并且这是不可更改的。
有两种方式可以设置这些属性:一是通过 git config --global user.name xxxx
和 git config --global user.emal xxxx@xx.com
;二是通过直接修改对应的配置文件。
这里以命令的方式进行配置为例:
# 配置用户名
git config --global user.name FatalFlower
# 配置用户邮箱
git config --global user.email GuiHuaLinked@gmail.com
默认文本编辑器的设置,当 Git 需要输入信息时将会调用它,如果没有配置的话,Git 会调用默认的文本编辑器。
# 设置 Git 的文本编辑器为 emacs
git config --global core.editor emacs
当在 Windows 上修改 Git 默认的文本编辑器时,需要指定执行程序的全路径。
基本使用
-
初始化 或
clone
通过初始化的方式或者
clone
的方式都可以得到一个 Git 仓库。-
初始化一个 Git 仓库(此时你应当已经进入了对应的项目目录)
# 初始化一个本地 Git 仓库 git init
这会在你的项目目录下创建一个
.git
文件夹,这是一个隐藏的文件夹,里面包含了 Git 仓库所必须的一些文件。 -
clone
一个 Git 仓库git clone https://github.com/LiuXianghai-coder/tourism.git
这会将整个 tourism 仓库克隆到本地,同时还会将该仓库带有的
.git
文件夹clone
下来,得到整个项目的提交历史和文件等其它的信息。
-
-
修改工作区的内容
在工作区(即你能够看到的项目目录中),对其中任意几个文件进行修改,此时 Git 会检测到文件的修改。如果在修改完几个文件之后运行
git status
可以看到当前的状态。看起来可能像下图这样:
此时的修改后的文件依旧存储在 工作区
-
将工作区的内容添加到暂存区
此时可以通过
git add *.txt
命令将当前工作目录下所有的 txt 文件都放入暂存区。再通过git status
命令查看 Git 当前的状态。可能与下图类似: 注意此时修改后的文件处于 暂存区
-
提交暂存区内的文件快照
此时暂存区内有修改后的文件的快照,在未提交之前,这些快照依旧时不稳定的,因为它依旧有可能会丢失,因此最好的做法是将它们提交到本地仓库。
使用
git commit
命令可以将暂存区内的快照提交到本地仓库,此时提交将会强制要求输入本次提交的信息,也可以在命令后加上-m
选项来输入本次的提交信息。但是一般来讲,还是建议使用git commit
不带上-m
选项,因为这会使得你能够更加详细地说明本次提交地内容。 执行完
git commit
命令,并且输入对应的提交信息后,再次运行git status
命令查看 Git 的状态,可以看到工作区是干净的。 -
推送到远程服务器
之前讲过,Git 是一个分布式的软件版本控制工具,它的分布式就体现在这,客户端和服务端以及拉取这个仓库的客户端都会有这个仓库的
.git
文件夹。也就是说,以上这几个端都有对应的副本,因此对于软件版本控制来讲是极其安全的。只要即使拉取和推送了对应的仓库。 要推送本地的仓库到远程服务端,首先要添加它的上游分支:
# 在这里我将自己的一个 Github 仓库放到这里作为一个上游 git remote add origin https://github.com/LiuXianghai-coder/Test-Repo.git
此时通过
git remote --verbose
可以查看存在那些上游分支: 有了上游分支后我们就可以将我们本地的仓库推送到上游服务器上了 :)
使用
git push
命令可以将我们的仓库推送上去,具体命令如下:# 这里需要注意的是,origin 是指定的上游分支名称,origin 是默认的一个上游分支,具体名称需要参考 `git remote --verbose` 命令下输入的上游名 # main 是上游的一个分支,其实 git 在这里做了一些简化,实际运行的命令是 `git push origin master:master` 将本地的 master 分支推送到上游 origin 的 master 分支。 # 具体的运行内容为 `git push 上游服务器名 本地分支:远程仓库分支`,如果远程仓库的分支名不存在,则会创建一个分支。 git push origin master
之后会要求输入用户名和密码,按照要求输入对应信息即可。
此时再查看对应的远程仓库,可以看到我们的文件已经被推送上去了。
如果你使提交的仓库不是一个空的仓库,那么可能会出现 类似以下的错误:
这是由于两个存储库之间没有公共的祖先,因此导致合并失败。
可选的解决方法是将两个存储库先在本地合并,然后再提交回去:
# origin 和 master 需要换成自己对应的上游和分支 # 使用 --allow-unrelated-histories 允许两个没有关联祖先节点的分支进行合并 git merge origin/master --allow-unrelated-histories # 之后,再次提交即可 git push origin master
- Git 提交日志
上文提到,Git 地每次提交都会创建一个新的快照,同时将校验和作为一个历史提交目录。因此,在每次提交之后,Git 都会带有对应的快照目录,之后就很难在丢失已经提交的文件了。
Git 提供了一个命令用于查看该仓库的提交日志:
git log
。该命令会默认按照提交历史从后往前输出对应的提交日志:
可以查看到最近的几次提交日志。跟在 commit 后面的便是那次提交的校验和。
可能这么看起来不是特别舒服,试试
git log --pretty=oneline --graph
--pretty
可以设置输出格式,online
代表只输出一行,即带 SHA 校验和的那一部分。--graph
选项则是更加直观地查看提交信息,注意分支合并的那部分。
--pretty
可以自自定义输出格式,以下面的命令为例:# format 表示自定义格式化输出,%h 表示输出简写的校验和(一般情况下,Git 通过前8为校验和就能得到对应的提交文件目录),%an 表示作者名字,%ar 表示修订日期,%s 表示提交说明。 git log --pretty=format:"%h - %an, %ar : %s"
具体的格式如下图所示:
使用自定义格式与
--graph
选项配合使用往往时一个很不错的选择。 Git 的每次提交都会附带每次提交发生的一些变化,可以通过
-p
选项(即--patch
)来得到这些信息。由于这样的话输出可能会很长,因此可以使用-<n>
选项(n 在这指的是输出的记录条数)限制输出日志的数量。# 打印出最近的两次提交的补丁信息 git log -p -2
除了使用
-<n>
选项限制输出条目数外,使用--since
和--until
的组合方式来获取指定时间区段也是一个很不错的选择。--grep
过滤那些提交说明与指定正则表达式匹配的提交。--author
选项则只显示作者名与输入的作者名相匹配的提交。 合理利用这些选项,这会给你带来很大的帮助。
-
为提交打上标签
有时可能再提交某个软件版本时希望加上相对应的标签,以表示对应的版本信息等,使用
git tag
命令可以有效地解决这个问题。 Git 有两种标签:轻量标签(lightweight) 和 附注标签(annotated)
-
轻量标签
-
轻量标签本质上只是将提交校验和存储到一个文件中,没有保存任何其它的信息。创建轻量标签很简单,只需要在
git tag
命令后加上对应的标签名即可。# 为当前分支打上 v1.0-lw 的轻量标签 git tag v1.0-lw
-
使用
git tag --list
可以查看对应的标签 -
如果希望通过标签名来检出相对应的提交,可以使用一般的正则表达式匹配符来匹配。如
git tag --list "v1.0*"
即可检出所有标签以 v1.0 开头的所有提交。
-
-
附注标签
- 附注标签会包含许多的额外信息,包括打标签的人的名字、邮件地址、日期时间,以及该标签对应的信息。
- 创建一个附注标签需要加上
-a
选项,即 “annotate”。同时也可以通过-m
选项添加一条标签对应的信息。与提交一样,如果没有指定-m
选项,Git 会启动指定的编辑器提示输入标签信息。
-
可以通过
git show <tag>
来查看对应的标签和对应的提交信息# 这里 v1.9.1 是我自己给一个提交添加了一个附注标签 git show v1.9.1
可能得到的结果如下所示:
如果将该命令运行在轻量标签上,则只会看到对应的提交信息而看不到标签的相关信息。
-
如果想要对某个特定的提交添加标签信息,那么只需要在添加标签的命令后加上对应的提交校验和即可。这是因为校验和是一个提交的引用,因此对应的提交校验和就相当于是一个提交的引用。可以通过上文的
git log
命令查看相关的提交历史记录从而得到对应的校验和。# 这里给 720e86018f33410ef12f0bfbf7911e37f07dfccf 提交添加 v1.9.2 的附注标签。轻量标签只需去掉 -a 选项即可。 git tag -a v1.9.2 720e86018f33410ef12f0bfbf7911e37f07dfccf
-
共享标签
默认情况下,Git 在推送时并不会将打好的标签一起推送过去,因此在创建完标签之后必须手动将标签推送到远程仓库上。这个与上传分支有点类似:
git push origin v1.9.0
这里的origin
是上文提到的上游,v1.9.0 是要提交的标签。 如果要一次性提交多个标签,可以使用
git push origin --tags
这会把所有不在远程仓库的标签都提交到远程仓库。这里提交的标签是所有的标签,包括轻量标签和附注标签。在一次性批量提交的情况下,Git 没有办法区分轻量标签和附注标签。 -
删除标签
如果要删除本地标签,可以使用
git tag -d <tagName>
来删除指定的标签。 如果要删除远程仓库上的标签,一般有两种方式:
-
一是推送一个空值到远程仓库的标签,从而替换掉它
# 推送一个空值到远程分支的对应标签,也就相当于删除了之前对应的标签 git push <remote> :refs/tags/<tagName>
-
二是直接发送一个删除标签的命令,显示地删除它。
# 推送到上游一个删除标签的命令,显示地删除它 git push <remote> --delete <tagName>
-
-
文件版本的管理
上文提到,Git 对每个提交都保存着一个对应的文件版本,通过生成的校验和来作为该次提交的文件版本的引用。一般来讲,有时可能需要回滚到之前的版本,有时可能需要在一个现有的文件版本上进行开发,或者有时需要将多个文件版本进行合并。这些功能 Git 都能帮助我们很好地完成。
-
文件版本的回滚
根据前文的介绍,使用
git log
命令可以查看最近的历史提交以及相关的检索,同时也介绍到得到的校验和是一个文件版本的引用。因此,通过git log
命令可以很轻松地实现文件版本地回滚操作。 使用
git reset
可以更新相关的三个区,还记得上文提到到三个核心区域吗?git reset
有三个常用的选项,分别对应三个不同的区:-
git reset --soft <tree-ish>
这里的tree-ish
表示的是之前提到过的文件校验和 使用这个命令将将
HEAD
指针(在存储库中)将其移动到tree-ish
对应的版本。注意,此时的暂存区和工作区依旧保存着之前内容。因此此时运行git status
你将会看到是出于一个未提交的状态。 使用以下的图片来描述可能会更加直观一些:
完成提交之后的三个区域的内容:
此时执行
git reset --soft a7d5c77
(之前讲过,一般文件管理八位的校验和 Git 就可以找到对应的引用文件版本),将会使得 HEAD 指针移动到a7d5c77
的文件版本。此时看起来像这样:
这样的话,修改暂存区的内容再次提交就相当于取消了上次的提交,然后更换为了新的提交。(事实上,将这一操作理解为在仓库中根据上一版本新建了一个分支可能更为合理一些)。这也是
git commit --amend
(修改最后一次提交)的工作原理。 -
git reset --mixed <tree-ish>
使用
--mixed
选项会将暂存区的内容修改为tree-ish
版本的内容,同时也会更新 HEAD 指针到tree-ish
。如果此时运行git status
查看状态的话,就会发现当前的状态处于 未添加 状态。这是由于工作区的内容依旧未发生改变,因此 Git 检测这两个区域的内容不一致所导致的。 依旧以上文的版本为例,执行
git reset --mixed a7d5c77
后三个区域的内容如下所示: -
git reset --hard <tree-ish>
前文提到过,尽量使得你的 Git 状态时干净的。这是因为只有在提交创建对应的快照之后才能保证文件版本的稳定性。但是相反,在未提交之前,所有的文件的改动都是有可能丢失的。比如说在使用
--hard
的选项的条件下,如果你的工作区内含有未提交的文件,那么这么做之后将会丢失所有你未提交的改动文件,因此,在执行这个命令之前,务必确保你的 Git 状态时干净的。 使用
--hard
选项会将三个区的所有内容都更新到指定的tree-ish
版本。依旧以上文为例,此时的三个区的状态看起来如下图所示:
-
使用
git reset
会导致相关的日志的变化,如果此时回到的是最初的提交,那么就无法看到之后的提交校验和。此时需要使用git reflog
来查找对应的校验和,然后再通过git reset
更新回去。这更加说明时刻保持工作区干净的重要性,只要将工作区的内容提交,那么数据就不会丢失。
-
-
分支的管理
前面简要介绍了一下有关 Git 的分支信息。在一个软件的开发过程中,由于需求的不确定性,有时往往需要推出好几个版本。Git 的分支就很适合完成这件事。
-
分支的创建
-
创建分支的命令
# 创建 main 分支 git branch main # 将当前的分支切换到 main git checkout main # 或者,你可以一次性的执行完这两个命令。通过 git checkout 添加 -b 选项即可在切换指定的分支不存在时创建分支 git checkout -b main
此时执行
git branch
查看当前的分支信息,你可以看到当前的分支是位于main
分支。 -
创建远程分支
前面提到过推送本地仓库时将其推送到对应对应的远程服务器分支,如果当前的分支不存在则会创建对应的分支,使用这个方法可以很容易实现远程仓库的分支创建。这相当与一个简化的推送命令。
# 将当前的分支切换到 test git checkout -b test # 将当前的分支推送到 origin 上游 test 分支,由于上游现在不存在 test 分支,因此这会自动在 origin 上创建一个 test 分支 git push origin test # 这相当于执行了以下命令 git push <remote> source:target。即将本地的 source 分支推送到 remote 的 target 分支。同样的,当 remote 的 target 分支不存在时,会自动创建 target 分支。 # 因此上面的推送命令与下面的推送命令等效 git push origin test:test # 如果想将本地的分支推送到远程服务器的不同分支,只需改一下 target 分支即可。 # 这里将本地的 test 分支推送到远程服务器的 master 分支 git push origin test:master
-
跟踪分支
从远程仓库克隆的存储库可能包含很多的分支,但是克隆的时候只会克隆默认的分支(一般是 master)。如果想要直接跟踪对应的远程分支,这也是很简单的。
首先,查看对应的远程仓库信息,运行
git remote show <remote>
显示对应的远程仓库的信息。这里以center
仓库为例。# 查看远程仓库 center 的详细信息 git remote show center
可能会看到如下的输出:
现在,跟踪
center
仓库的main
分支# 这会在本地仓库中自动创建一个 main 分支用于跟踪 center 远程仓库 main 分支 git checkout --track center/main # 上面的命令还可以简化一下 git checkout main # 由于本地并没有 main 分支,因此 Git 首先会检测远程仓库是否有该分支,如果有该分支,那么 Git 将会自动创建该远程仓库对应的分支的跟踪。 # 如果想要使用一个不同的分支名字来跟踪 center 仓库的 main 分支,可以这样做 git checkout -b main2 center/main # 在本地创建一个 main2 分支用于跟踪远程仓库的 main 分支 # 如果想要切换本地的 master 分支去跟踪其它的上游分支,可以通过以下命令来实现 # git branch -u <remote>/<remoteBranch> <localBranch> # 注意 git 中的 -u 选项一般指 --set-upstream 即设置上游 # 这里将本地的 master 分支用于跟踪 orign 上游下的 master 分支 git branch -u origin/master master
-
-
分支的合并
-
简单的合并
以上文为例,现在将
main
分支同test
分支合并。# 由于这两个分支包含的所有文件的文件名不一样,因此在这里不会出现合并冲突的问题 # 此外,如果这两个分支没有公共的祖先节点,如果你很明白你在做什么,请记得加上 --allow-unrelated-histories 选项 git merge main test
-
合并冲突的解决
现在使用
git reset
使main
分支 和test
分支回到合并之前的节点。然后在这两个分支里都创建一个名为combine.txt
的文件,然后在两个分支分别写入不同的内容,然后提交。 在执行之前的合并命令,你会看到类似以下的输出:
由于两个分支当前版本都含有
combine.txt
文件,并且文件的内容有冲突,因此 Git 的自动合并失败了。此时查看combine.txt
文件,会看到类似下面的输出: (合并时冲突的文件)
此时使用
git status
可以查看所有合并冲突的文件。 解决冲突文件的一般做法是在两个分支文件的内容中保留其中一个分支的内容,当然你可以自己手动修改它们。
如果希望使用合并工具来解决冲突的话,
git mergetool
是一个很好的选择。在 Linux 下一般默认将vimdiff
作为合并解决工具,可以通过添加-t <tool>
来指定合并解决工具,可以通过git mergetool --tool-help
来查看那些合并解决工具是可用的,但是前提是你必须现在你的计算机上安装好它。 处理好之后,再执行提交即可。
值得注意的是,使用合并工具解决冲突后会保留之前冲突的文件的副本,你可以保留它并将它提交放入本地仓库,或者也可以直接删除它。
-
变基的使用与建议
使用变基可以使得你的提交看起来是线性的,在单人开发的情况下不会出现什么问题,但是对于一个软件开发来讲,这是不可能的。由于多人一起协同开发时使用变基带来的一些令人头痛的问题,我在此强烈建议开发情况下不要使用变基合并分支,所以对于变基的使用将只是简要介绍它的工作原理。
与
merge
不同,merge
是将两个分支的内容直接进行合并,遇到冲突时提示对应的信息。而如果使用变基来合并分支时,首先它会找到要合并的目标分支的共同公共祖先节点,然后将当前在公共祖先节点添加的补丁在添回到目标分支,然后将当前的分支的父提交指向目标分支。因此这么一顿操作后,当前的分支就直接添加到目标分支的后面,使其看起来像一条直线。# 将当前的分支变基到 master 分支,再次建议,不要在多人开发的情况下使用变基 git rebase master
-
-
以上就是有关 Git 的一些基本使用,希望它对你有帮助。