Git的学习
Git简介
Git由Linux操作系统内核的创造者Linus Torvalds在2005年创造,是目前世界上被最广泛使用的现代软件版本管理系统。Git是一个成熟并处于活跃开发状态的高质量开源项目,目前Git支持绝大多数的操作系统,绝大多数的IDE也对Git有支持。
版本控制
版本控制是一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统。版本控制的种类大致有三种,分为‘本地版本控制’、‘集中化版本控制’以及‘分布式版本控制’。
- 本地版本控制:
- 本地版本控制的工作原理是在本地硬盘上保存补丁集(补丁是指文件修订前后的变化)通过应用所有的补丁,可以重新计算出各个版本的文件内容。
- 集中化版本控制:
- 本地化版本有个很大的缺陷就是很难做到协同工作,集中化版本控制解决了这个问题。
- 集中化的版本控制系统能够让在不同系统上的开发者协同工作。这类版本控制系统都有一个单一集中管理的服务器,保存所有文件的修订版本,而协同工作的人都通过客户端连到这台服务器,取出最新的文件或者提交更新。这类版本控制系统的代表作便是Subversion版本控制系统(SVN)。
- 每个人都可以在一定程度上看到项目中的其他人正在做些什么,管理员可以轻松掌控每个开发者的权限。
- 分布式版本控制:
- 不再有一个统一的集中管理的服务器,客户端并不只提取最新版本的文件快照,而是把代码仓库完整地克隆下来。
- 任何一处协同工作用的服务器发生故障,事后都可以用任何一个克隆镜像出来的本地仓库恢复。
- 每一次的克隆操作,实际上都是一次对代码仓库的完整备份。
- 比较典型的分布式版本控制系统有Git,也是我们接下来要讨论的重点。
分布式版本控制系统Git
Git底层对象
- 实体对象
- 原文(blob)
- 树(tree)
- 链表(linked list)
- 补丁(patch)
- 虚拟对象
- 指针(pointer)
- 引用(ref)
Git底层对象的关联
Blob → Tree → Commit
Commit → Branch
说明
tree:每次commit生成的文件快照,以快照树结构存在
commit:每个文件树快照的容器,包含一个指向对应文件树的指针,以及作者、提交者姓名、时间以及最重要的属性:parent(父节点)
linked list:关联commit,构成branch(分支)
ref:branch、tag、stash等名字都是引用,它们都可以指向任意一个commit
-
pointer:常见的指针有HEAD以及FETCH_HEAD
- HEAD:指向某一个branch(指针指向引用),方便我们做分支操作,正常状态下是attached到某个branch,如果特殊情况,也可以将HEAD指向某个commit,此时指针为deattached状态,比较危险
- FETCH_HEAD:指向目前已经从远程仓库(Remote Repo)取下来的分支的末端版本,表示最后一次到本地仓库的更新。
HEAD和FETCH_HEAD平时都是用来操作本地仓库的,即Local Repo
-
- 将文件中的内容通过其hash算法生成一个160比特的报文摘要,即40个十六进制数字(每个十六进制数字占4位)。
- 通过SHA-1计算出来每个Blob和Tree的hash值,可以用git hash-object 进行测试。
Git工作的原理
- 本质:Git的底层是一个微型内容寻址系统
- 基于文件树Hash快照。Git只关心文件数据的整体是否发生变化,而大多数其他系统则只关心文件内容的具体差异。Git并不保存这些前后变化的差异数据,实际上,Git更像是把变化的文件作快照后,记录在一个微型的文件系统中。每次提交更新时,它会纵览一遍所有文件的指纹信息并对文件作快照,然后保存一个指向这次快照的索引。为提高性能,若文件没有变化,Git不会再次保存,而只对上次保存的快照作一链接。Git更像是个小型的文件系统,但它同时还提供了许多以此为基础的超强工具。
Git文件状态
先来看一张图
Git文件状态有以下几种
- Untracked:没有加入到Git版本控制中的文件
- Unmodified:已经提交文件(已经存在于Git仓库中的文件)
- Modified:已经存在于Git仓库的文件,后面做了修改但是没有提交
- Staged:已经暂存(Git暂存区)等待提交的文件
Git工作区域组成
- 工作区(WorkSpace):这是电脑上的一个目录,存放从Git仓库的压缩数据库中提取出来的文件,存放的文件状态Untracked、Unmodified、Modified
- 暂存区(Stage):保存了需要下次提交的文件列表信息,文件状态:Staged
- 本地仓库区(LocalRepo):用来保存项目的元数据和对象数据库的地方(存在于工作目录下的隐藏文件夹
.git
下,备份这个文件夹即可备份整个仓库),文件状态:Unmodified
Git在本地工作去中有三颗文件快照树:
- HEAD:上一次提交的文件快照,下一次提交的父结点
- WorkRepo:记录整个工作目录的文件树结构
- Stage\Index:预期下一次要提交的文件快照
.git目录
在Mac OS X操作系统中这是一个隐藏的目录,我们可以通过快捷键Command
+ shift
+ .
将其显示出来
当我们用Git命令git init
对一个目录进行初始化后便可以看到.git
这个文件目录(如图)
.git
这个目录中包含了几乎所有的Git存储和操作的对象,如果想要复制一个版本库只要将这个目录复制一下即可。下面说明下该目录
- description:仅供GitWeb程序使用
- config:包含项目特有的配置
- info:包含一个全局性排除(global exclude)文件,用以放置那些记录在
.gitignore
文件中不希望添加到版本控制中的文件 - hooks:包含客户端或服务端的钩子脚本(hook scripts)
- HEAD:指示目前被检出的分支
- index:保存暂存区的相关信息
- objects:存储所有数据内容
- refs:存储指向数据(Branch)的提交对象的指针
Git操作基本流程
- 在工作目录中修改某些文件
- 对修改后的文件进行快照,保存至暂存区域
- 提交更新,将保存在暂存区域的文件快照永久转储到Git目录中
Git命令
初次运行Git的配置命令
在计算机上安装好Git后可以定制你的Git环境。每台计算机上只需要配置一次,你也可以在任何时候再次通过运行命令来修改这些配置。
- 全局配置用户信息
git config --global user.name "xxx"
git config --global user.email "xxx@meituan.com"
- 检查配置信息
git config --list
- 检查某单一配置信息
git config <#key#>
// eg:检查用户名字
git config user.name
一些较为常用的Git命令
- 初始化一个Git仓库
// 需要进入你想初始化为Git仓库的目录执行以下命令
git init
该命令将创建一个名为.git
的子目录,这个子目录含有你初始化的Git仓库中所有的必须文件,这些文件是Git仓库的骨干。 但是,到此为止我们仅仅是做了一个初始化的操作,项目里的文件还没有被跟踪。
- 跟踪项目里的文件
// 一般格式
git add [<options>] [--] <pathspec>...
// 常用的git add 命令
git add -u // 添加仅索引修改或删除的文件,而不是那些创建的文件
git add -u [<path>] // 把path路径中所有被跟踪文件中被修改或者删除的文件的信息添加到索引库当中,省略path即表示在当前目录中
git add -A(--all) // 把当前目录中所有被跟踪的文件中被修改过或已删除的文件和所有未指定的文件信息添加到索引库。
git add . // 把工作时的所有变化提交到暂存区,包括文件内容修改(modified)以及新文件(new),但不包括被删除的文件。
- 查看当前仓库状态
git status // 此条命令会较为详细得展示当前仓库的状态
git status -s // 查看当前仓库状态(简略)
-
切换分支或者恢复文件
- 切换分支
git checkout <branch name>
- 修复工作目录下的文件,使得跟仓库文件同步
git checkout <filename>
- 如果1中<branch name>和2中<filename>相同,调用上面命令会出问题,所以对于文件同步用下面命令(撤销文件修改):
git checkout -- <filename>
- 新建并切换分支
git checkout -b <branchname>
- 切换到旧的提交版本,commit是表示提交的id
$ git checkout <commit>
- 删除分支:
// 需要先切换到其他分支,无法删除正在使用的分支(切回master分支) git checkout master // 删除本地分支xxx git branch -d xxx // 开始推送删除共享git服务器上分支 git push origin :xxx // 删除远程服务器上的分支xxx git push origin --delete xxx // 强行删除某个分支xxx(当分支新增功能而且已经commit但是没有合并,然后现在不需要这个分支) git branch -D xxx
-
提交文件到本地仓库
- 提交xxC语言文件到本地仓库中
git add xx.c git commit -m "commit xx.c"
- 所有已经跟踪过的文件暂存起来一并提交,无需
git add
命令
$ git commit -a -m 'added new benchmarks'
- 修改最后一次提交
// 修改最后一次提交信息,输入下面命令后会跳出编辑器,修改相关内容关闭即可 git commit --amend // eg:当有些文件忘了提交、有些文件忘了删除,需要修改最后一次提交时可以这样做 git add xxx // 提交忘记提交的文件 git rm xxxx // 删除忘记删除的文件 git commit --amend // 修改最后一次提交的信息
-
查看任意两棵文件树之间的差异
- 查看工作环境与暂存区之间的差异(尚未提交的文件)
git diff
- 查看暂存区域与你最后提交之间的差异
git diff --staged
- 比较两个提交记录的差异
git diff branchA branchB
- 检查空白错误(空格、tab等)
git diff --check
- 查看合并后本分支变动了的内容
git diff --ours
- 查看被合并分支与合并后有哪些不同(-b去除空白)
git diff --theirs -b
- 查看合并后,合并两个版本各自变动内容(-b去除空白)
git diff --base -b
扩展
合并分支
// eg 合并某分支(branchA)到当前分支(master)
// 使用Fast-forward模式(Git默认模式)
git merge branchA
git branch -d branchA
// 禁用Fast-forward模式
git merge --no-ff -m "merge with no-ff" branchA
// 两者的区别:使用Fast-forward,无法知道branchA存在过
-
与合并相关的命令
- 合并时,有冲突,用自己分支代码解决冲突
git merge -Xours branchname
- 合并时,有冲突,用别人分支代码解决冲突
git merge -Xtheirs branchname
- 使用此方式合并,留下合并记录
git merge --no-ff -m "merge with no-ff" <branchname> git merge <branchname>
- 列出本地仓库,分支前面有* 表示是当前使用的分支
git branch
- 列出分支,并且显示每个分支最后一次提交信息
git branch -v
- 查看哪些分支已经合并到当前分支
git branch --merged
- 查看所有包含未合并工作的分支
git branch --no-merged
- 合并出现冲突,不想合并了,可以用下面命令撤销
git merge --abort // 或者 git reset --hard HEAD
-
拉取代码和推送代码
git fetch // 远程仓库拉取到本地仓库(仅仅更新到本地仓库并没有更新到工作区) git push // 从本地仓库推送到远程仓库 git pull // 从远程仓库拉取到本地仓库并与本地进行合并 // git pull <=> git fetch 后 git merge git pull --rebase // push本地仓库的commit之前,先把其它人的改动更新到本地,这个比git pull有更严格的本地检查 // git pull --rebase <=> git fetch 后 git rebase
-
回滚版本、回滚暂存区文件
-
撤销暂存区文件
- 撤销暂存区文件xxx(其实就是把HEAD版本的xxx文件恢复到暂存区)
git reset HEAD xxx
- 撤销暂存区文件xxx(xxx可能会跟分支名相同,所以用--来表示后面是路径或者文件)
git reset HEAD -- xxx // xxx是文件名字
- 把指定版本(xxx)的文件xxx.c恢复到暂存区
git reset xxx xxx.c // 第一个xxx是版本ID
根据上个命令显示的commit对应的值回滚
git reset --hard <commit> // --hard表示删除工作区的所有在commit版本后的改动
-
撤销版本提交
- 恢复工作区(同步缺少的文件,对于已经修改的文件只尝试把仓库中版本合并到本地,保留本地修改)
git reset HEAD
- 恢复工作区(把本地修改的文件全部从硬盘删除,同步成仓库中版本)
it reset --hard HEAD // git reset --hard HEAD 命令包括以下三个步骤 // 1.移动HEAD分支指向上一个版本 // 2.使索引看起来像现在的HEAD // 3.使工作目录看起来像索引
-
-
变基
git rebase
在Git中整合来自不同分支的修改主要有两种方法:merge 和 rebase
变基:提取分支版本中引入的补丁和修改,然后合并到其他分支中。变基操作的实质是丢弃一些现有的提交,然后相应地新建一些内容一样但实际上不同的提交。
变基的准则:不要对在仓库外有副本的分支执行变基。
-
变基原理:
- 首先找到两个分支(即当前分支、变基操作的目标基底分支)的最近共同祖先
- 然后对比当前分支相对于该祖先的历次提交,提取相应的修改并存为临时文件
- 然后将当前分支指向目标基底
- 最后以此将之前另存为临时文件的修改依序应用到目标基底分支
说明
- 变基有风险,只在本地变基,不要变基服务器上的分支
- 变基操作的实质是丢弃一些现有的提交,然后相应地新建一些内容一样但实际上不同的提交。如果有人依赖那些丢弃的提交,会产生问题
- 如果有人变基服务器上的分支,其它人更新数据时要执行
git pull --rebase
命令 - 只要你把变基命令当作是在推送前清理提交使之整洁的工具,并且只在从未推送至共用仓库的提交上执行变基命令,就不会有多大问题。
git rebase
与 git merge
对比
- merge 是一个合并操作,会将两个分支的修改合并在一起,默认操作的情况下会提交合并中修改的内容
- merge 的提交历史忠实地记录了实际发生过什么,关注点在真实的提交历史上面
- rebase 并没有进行合并操作,只是提取了当前分支的修改,将其复制在了目标分支的最新提交后面
- rebase 的提交历史反映了项目过程中发生了什么,关注点在开发过程上面
- merge 与 rebase 都是非常强大的分支整合命令,没有优劣之分,使用哪一个应由项目和团队的开发需求决定
- 使用 merge 时应考虑是采用 --no-ff 默认操作,生成一个对回顾提交历史并不友好的合并记录,还是采用 --ff-only 方式
- rebase 操作会丢弃当前分支已提交的 commit,故不要在已经 push 到远程,和其他人正在协作开发的分支上执行 rebase 操作
- 当有修改未 commit 时,不能进行 rebase 操作,此时可以考虑先用 git stash 命令暂存
参考资料