简介
Git 作为分布式版本控制系统,基于去中心化的设计思想,在每个分布式节点上都保存有完整的版本,降低了对中心仓库的依赖,增加了版本安全性。
Git 的使用过程中,并不是必须设置中心仓库,各个节点之间完全可以互相推送和拉取更新内容。不过考虑到相互的通信问题和团队协作,所以一般会选择一个 24 小时运行的主机作为中心仓库,以此来获取和交换更新内容,例如 GitHub 则提供了这样的托管服务。
三种状态
Git 对文件的跟踪管理存在三个阶段,工作区、暂存区和分支:
- 工作区就是实际操作的文件目录;
- 暂存区是一个索引文件,记录已跟踪文件的目录树,保存文件的时间戳、大小等易比较的信息;
- 分支与暂存区类似,也保存有文件的目录树,用于记录文件系统的快照。
暂存区和分支都依赖 .git/objects 目录,该目录称为对象库,记录了真实文件系统的快照。暂存区只是一个 index 文件,存放在 .git 目录中。当工作区文件发生修改时,将工作区中文件的时间戳和大小,与 index 中记录的文件时间戳和大小进行比较,可以快速的判断工作区文件是否发生了更改,然后再具体的进行工作区文件内容与 objects 中记录文件内容进行比较。
暂存区的作用更像是工作区和分支之间的一个缓冲区域,或者称之为 “预提交文件改动到分支” 的区域。暂存区的存在,允许我们在工作区和暂存区之间方便的进行文件修改的添加与撤回,以及对修改内容的分部分提交。例如当工作中需要修改两部分内容,目前只完成了一部分的修改时,可以将完成的这部分添加到暂存区,接着在工作区继续修改另一部分,需要提交到分支上时,则提交缓存区的内容,未修改完成的部分可以继续修改。
因为分支更重要的作用是维护文件系统的版本序列,与远程仓库的通信,所以如果没有暂存区的存在,那么我们的文件修改则只能频繁的与分支打交道,届时版本序列将变得复杂且不易于维护,且每个版本记录保存的是文件系统快照,若频繁的进行文件修改和提交,则仓库大小将快速膨胀。
Git 使用
Git 安装
下载安装 Git:| Mac OS X | Windows | Linux/Unix |
官网下载速度较慢,这里提供一个 Windows 版本的下载链接:Git for Windows
Git 配置
Git 安装之后,首先进行用户名和邮箱的配置,配置信息会记录到每次的提交记录中,并且当推送更新到 GitHub 上的项目时,会与 GitHub 账号进行匹配,在历史提交记录中会显示出用户头像,并且点亮提交次数。
通过 git config
命令进行用户名和邮箱配置时存在三种级别:
-
--system
:当前机器上的配置,面向所有用户; -
--global
:当前用户的配置,面向当前用户的所有仓库; -
--local
:当前仓库的配置,只对当前仓库生效。
配置使用的优先级为:
local > global > system
,即范围小的配置会覆盖范围大的配置。
配置文件所在位置为:system:etc/gitconfig
global:~/.gitconfig
local:.git/config
windows 系统中的位置为:system
:git 工具安装位置的etc/gitconfig
global
:当前用户主目录下的.gitconfig
local:.git/config
配置方式为:
git config --global user.name "abc"
git config --global user.email "abc@xxx.com"
查询配置方式为:
git config --global --list
一般只需要对当前用户进行配置即可,即使用 --global
级别,如果某个仓库有特殊安排,则可以在具体的仓库级别进行配置即可。不明确指定级别的话,默认设置的为 --local
级别。
对于版本号在 2.0.0 以上的 git
,提供了一个简单的查询配置文件目录的命令:
git config --list --show-origin
创建仓库
创建仓库有两种方式,选择本地工作目录初始化为仓库,或者从远程仓库克隆到本地。克隆仓库的方式在下面的内容中再进行讲述,这里首先使用初始化本地工作目录为仓库的方式。
进入选定目录,执行如下命令即可:
git init
命令执行之后,在当前目录中会生成 .git 文件夹,此时当前目录即为一个“崭新”的仓库。
之所以用“崭新”来描述仓库,是因为在执行仓库初始化命令后,无论当前目录下是否存在文件,.git 目录生成后都不存在 index 文件,objects 目录下的文件夹中也没有具体的文件生成。即此时暂存区和分支都为空,只有向仓库中添加文件后,才会生成暂存区 index 文件,objects 目录下才会生成文件。
记录文件/更新
首先要明确一点,工作目录中的文件只有两种状态,已跟踪和未跟踪,也就是已经纳入版本记录,和未纳入版本记录。使用上面的 git init
命令生成仓库时,工作目录中的所有文件都是未跟踪状态,从远程仓库克隆生成本地仓库时,工作目录中的所有文件都是已跟踪状态。
对于未跟踪文件,则无所谓文件是否发生了修改,因为不会跟踪记录该文件的状态。对于已跟踪文件,则会检测记录该文件是否发生了修改。
git add <file>
git add
命令面向两种对象,一个是将未跟踪文件纳入暂存区,进行跟踪记录;另外一个是将已跟踪文件的修改,添加到暂存区,记录文件的更新。当命令后跟着一个目录时,则递归添加目录及目录下所有文件。
git status
git status
命令用于查看文件的状态,未跟踪文件只有一种状态:文件未跟踪,或者称为未纳入暂存区,状态显示为 Untracked files
。已跟踪文件有两种状态:一是纳入暂存区,等待提交到版本库,状态显示为 Changes to be committed
;二是文件发生了修改,且修改部分尚未添加到暂存区,状态显示为 Changes not staged for commit
。
删除文件
git add
命令用于向暂存区添加文件,或记录文件的更新内容。若执行此命令后,发现该文件并不需要跟踪记录,或者已经添加到暂存区的文件更新内容需要取消,git
提供了相应的撤回操作命令。
git rm --cache <file>
git rm --cache <file>
命令用于从暂存区移除对文件的跟踪。
git rm <file>
git rm <file>
命令不仅从暂存区移除对文件的跟踪,并且从工作目录中也删除了该文件。
git rm --cache <file>
命令和git rm <file>
命令都存在一个-f
选项,用于强制删除。当已跟踪的文件发生了修改,并且修改未添加到暂存区时,则需要git rm --cache -f <file>
命令才能从暂存区移除对文件的跟踪;当已跟踪的文件发生了修改,并且修改已经添加到暂存区时,则需要git rm -f <file>
命令才能同时从暂存区和工作目录中删除文件。
删除更新
这里的更新有两种情况:
- 工作目录下已跟踪文件进行了更新,且更新内容尚未提交到暂存区;
- 工作目录下已跟踪文件进行了更新,且更新内容已经提交到暂存区。
git checkout -- <file>
git checkout -- <file>
命令用于撤销第一种情况下的更新内容,可以理解为拿暂存区的文件内容替换掉工作区的文件内容。
git reset HEAD <file>
git reset HEAD <file>
命令用于撤销第二种情况下的更新内容,可以理解为拿上个版本的系统快照替换掉暂存区的文件内容。
git checkout HEAD <file>
git checkout HEAD <file>
命令能够同时撤销工作区和暂存区的更新内容,可以理解为拿上个版本的系统快照替换掉工作区和暂存区的文件内容。
因为容易引起工作内容丢失,所以使用
git checkout HEAD <file>
命令时需要注意。
提交文件
工作中对每一个文件修改完成后,将修改内容依次添加到暂存区,当完成所有修改后,则提交暂存区文件到当前分支上。
git commit
git commit
命令用于提交暂存区文件到当前分支上,执行该命令后会打开文本编辑器提示输入提交信息。
可以直接执行
git commit -m 'commit message'
命令,将提交信息写入命令中。
git log
git log
命令用于查看提交历史,每个提交都会记录时间、用户信息、输入的 commit
信息及 commit
值 ,这里的 commit
值是一个 SHA1
校验和,在后续的版本回退中会使用到 。
git reflog
git reflog
命令用于查看 HEAD
变动历史,当执行提交、分支切换以及版本回退这类改变 HEAD
指向的操作时,都可以通过该命令查询出 HEAD
指向的提交值,即 SHA1
校验和。该命令更多时候用于版本回退时,若想撤销回退操作,恢复到回退之前的记录时,通过该命令可以查询到回退之前的校验和。
分支切换
分支的使用很广泛,修改 bug
,或者开发新功能,都可以拉出一个新分支,等功能开发完成并测试通过后,再合并分支内容到主干分支上。
在 git
的分支使用中,不同的分支实际就是指向各个文件系统快照的指针,所以在诸多 VCS
中 git
提供了轻量级且高效的分支创建、切换操作。
HEAD
可以理解为一个指针,指向当前访问的分支。
git branch
git branch
命令用于查看当前的分支情况。
git branch <name>
git branch <name>
命令用于创建新分支。
git branch -d <name>
git branch -d <name>
命令用于删除指定分支。
git checkout <name>
git checkout <name>
命令用于切换到指定分支。
git checkout -b <name>
git checkout -b <name>
命令用于创建分支,并切换到新分支。相当于 git branch <name>
和 git checkout <name>
两条命令合并到一起。
分支合并与冲突解决
当在功能分支上完成新需求的开发任务后,需要切换回主分支,并将修改内容回合到主分支上,删除该功能分支。
git merge <name>
git merge <name>
命令用于合并指定分支的修改内容到当前分支上。
以合并 dev
分支修改内容到 master
分支为例,若 master
分支的指向处于 dev
分支的直接上游时,如图 merge-1
所示,此时合并分支速度较快,因为只需要更改 master
分支的指向即可。
此时的合并方式为
Fast-forward
方式,因为只需要更改分支的指向,所以速度较快,且不会产生新的提交记录。
若 master
分支的指向不处于 dev
的直接上游,如图 merge-2
所示,则合并过程需要比较 C3
提交的修改内容与 C4
提交的修改内容。如果两个提交中不存在对 同一处文件内容 的修改,则此时可以顺利合并修改内容,并产生一次新的合并提交,如下图中的 C5
;如果两个提交中存在对 同一处文件内容 的修改,则此时合并存在冲突,需要手动解决冲突并完成合并提交。
图
merge-2
所示的合并方式为recursive
方式,或者称之为三路合并方式。因为合并C3
与C4
的提交,需要与公共上游C2
相比较,以达到与 同一处文件内容 相比较的目的,所以该合并方式主要观察C2
、C3
与C4
三个文件系统快照,所以称为三路合并方式。因为复杂情况下公共上游并不想图中所示这么明显,可能需要进行多次迭代合并处理方可产生虚拟的公共上游,所以也称此方式为recursive
方式。
git cherry-pick <commitId>
git cherry-pick <commitId>
命令用于合并其他分支上的某次提交到当前分支上。
git cherry-pick <start_commitId>^..<end_commitId>
git cherry-pick <start_commitId>^..<end_commitId>
命令用于将其他分支上从 <start_commitId>
起始到 <end_commitId>
结束的提交合并到当前分支上,包括起始和结束提交。
理想的
VCS
使用方式是,克隆、修改、提交代码,不存在任何的bug
修改或者冲突解决问题,但这是不可能的,所以实际工作中总会遇到各样的情形。例如暂时不准备将某个特性分支的开发修改合入主干,但是又要引入特性分支的某次提交(bug
修改、配置管理或者安全处理),此时可以使用cherry-pick
来将指定的提交合入主干。
关联远程仓库
在团队协作过程中,经常的场景就是团队的每位成员都 fork
一份项目代码到自己的个人库中,然后在自己的库里面做修改,修改完成再合入到团队的项目代码库中。所以我们的本地仓库一般关联两个远程仓库,一个是团队空间的项目代码,用于拉取最新更新内容;一个是个人库中的项目代码,用于推送个人修改内容。
git remote
git remote
命令用于展示当前仓库关联的远程仓库。
git remote add <name> <address>
git remote add <name> <address>
命令用于为当前仓库添加关联的远程仓库。
git remote remove <name>
git remote remove <name>
命令用于删除当前仓库关联的远程仓库。
git fetch <name>
git fetch <name>
命令用于从远程仓库拉取最新分支信息。
git fetch <name>
命令只会拉取分支信息,生成<remote_name>/<branch_name>
远程分支,并不会为本地仓库生成分支。
git branch -u <remote_name>/<branch_name>
git branch -u <remote_name>/<branch_name>
命令用于将当前分支与远程分支进行关联,即建立关联关系。
git checkout --track <remote_name>/<branch_name>
git checkout --track <remote_name>/<branch_name>
命令用于在本地仓库上新建分支,并与远程分支进行关联。命令 git checkout -b <branch_name> <remote_name>/<branch_name>
提供同样建立关联分支的作用。
git push <remote_name> <branch_name>
git push <remote_name> <branch_name>
命令用于推送本地仓库的分支到远程仓库上,相当于在远程仓库上建立新分支。命令 git push <remote_name> <branch_name>:<branch_name>
提供同样推送分支的作用。
使用该命令只会在远程仓库上建立新分支,并不会自动与当前仓库上的分支进行关联,
git push -u <remote_name> <branch_name>
命令可以在推送分支的同时进行关联,所以一般在github
网站上新建仓库时都会有类似的提示,表示用于从本地初始化仓库,然后推送到远程仓库上。
git push <remote_name> --delete <branch_name>
git push <remote_name> --delete <branch_name>
命令用于删除远程仓库上的指定分支。命令 git push <remote_name> :<branch_name>
提供同样删除远程仓库分支的作用。
当使用
git clone <remote_address>
命令来构造本地仓库时,会自动建立本地分支master
,并与远程仓库分支origin/master
进行关联。
当本地分支已经关联到远程分支之后,拉取更新和推送更新都变得较为简单。在分支上直接执行
git push
即可推送更新到关联的远程分支上,执行git fetch
即可拉取关联分支更新,然后执行git merge
即可合入更新到当前分支上。此外,git
还提供有命令可以直接拉取更新并合入到当前分支上,git pull
命令相当于合并了git fetch
和git merge
两个命令的功能。
版本回退
虽然有了暂存区可以检查待提交内容的正确性,但是仍不免有错误或不恰当的内容被提交,git
提供了在分支上回退版本记录的命令。
git reset <level> <commitId>
git reset <level> <commitId>
命令用于回退版本到指定提交记录点。这里 commitId
是 SHA1
校验和,用于标识待回退到的提交记录点。回退的 level
有三种:
--soft
:修改HEAD
指向指定的提交记录点,并将指定记录到最新提交记录之间的修改回退至暂存区,工作区不受影响。--mixed
:修改HEAD
指向指定的提交记录点,并将指定记录与最新提交记录之间的修改回退至工作区,暂存区会被清除。--hard
:修改HEAD
指向指定的提交记录点,并将指定记录到最新提交记录之间的修改清除。
该命令不填写具体
level
时,默认级别为--mixed
。这里注意一下--hard
的使用,该级别会清除工作区和暂存区的修改,即便撤销回退操作回到最新提交,工作区和暂存区的修改也不会恢复,所以谨慎使用。同理,--mixed
级别也会清除暂存区的修改,所以版本回退过程中,需要注意选择恰当的回退方式。
执行版本回退命令时,并不一定每次都要提供指定版本记录的校验和,也可以通过
HEAD
来指定回退到相邻的哪一个版本记录。HEAD^
表示回退到上一个版本记录,HEAD^^
表示回退到上两个版本记录,HEAD~n
表示回退到上n
个版本记录。
git revert <commitId>
git revert <commitId>
命令用于回退指定提交记录。
git revert <commitId>
命令和git reset <level> <commitId>
命令都可以用于回退版本,不同之处在于reset
用于回退到指定提交记录,revert
用于撤销指定提交记录,并且产生一个新的提交记录。
除了对提交历史的变动不同之外,
git revert <commitId>
和git reset <level> <commitId>
命令使用的侧重场景也不同。reset
命令更多用于在本地分支进行回退,避免对团队其他人的提交产生影响;revert
命令则可以使用在公共分支上,当进行代码检视时,发现提交历史中的某一次提交存在bug
,则可以使用revert
命令撤销那一次提交。
在本地仓库的分支上执行回退操作后,有些情况下可能要同步回退远程仓库。
git push -f
git push -f
命令用于同步回退当前分支关联的远程分支,因为当前分支的版本落后于远程分支,所以需要加 -f
选项,执行强制推送。
文件异同
通过 git status
只能查看出文件的状态以及是否发生了修改,并不能具体的展示出差异内容。
git diff <file>
git diff <file>
命令为查看工作目录的文件与暂存区文件的差异,也就是查看从上次提交文件修改到暂存区后,到目前为止,工作目录的文件又做了什么修改。
git diff --cached <file>
git diff --cached <file>
命令为查看暂存区的文件与当前分支的文件差异,也就是此次准备提交到分支上的有哪些修改内容。
git diff
命令还有其他形式:
git diff <branch> <file>
git diff <branch> <file>
命令为查看当前工作目录文件与其他分支文件差异。
git diff --cached <branch> <file>
git diff --cached <branch> <file>
命令为查看当前暂存区文件与其他分支文件差异。
其实工作中这种命令的使用场景不多,这里只是举个例子,说明
git diff
的使用形式是灵活多样的,例如也可以用于比较两次commit
的差异等。
储藏修改
当工作过程中需要临时解决某个问题,即需要在主分支上拉取 bugfix
分支修复 bug
时,若当前特性开发分支 dev
上的工作还没有完成,无法立即提交。此时切换并拉取新分支会对工作目录的修改内容造成干扰,则此时需要把当前修改存储起来,保持工作目录的整洁,然后再建立bugfix
分支修复 bug
。
git stash
git stash
命令用于储藏当前工作目录的修改和暂存区内容到一个栈空间上,使得当前的工作目录和暂存区不存在任何修改,即保持干净状态。
git stash pop --index
git stash pop --index
命令用于恢复栈空间上的储藏到工作目录和暂存区,即恢复原状。命令的使用中若不加 --index
参数,则储藏会恢复到工作目录,暂存区会清空。
该命令不仅适用于分支切换时,对于之前提到的版本回退命令,若可能造成工作目录和暂存区内容丢失,则可以使用该命令来储藏信息。