Git学习笔记

一、版本控制

Git 是版本控制器,版本控制是一种记录一个或若干文件内容变化以便将来查阅特定版本修订情况的系统

版本控制系统分为集中式和分布式

为什么要使用版本控制

(1)软件开发中采用版本控制系统可将某个文件回溯到之前的状态,甚至将整个项目都回退到过去某个时间的的状态,且额外增加的工作量很少

(2)还可比较文件的变化细节,查出最后是谁修改了哪个地方,从而找出导致问题出现的原因,又是谁在何时报告了某个功能缺陷等

集中式版本控制系统

集中化的版本控制系统,如 CVS,Subversion(svn) 以及 Perforce 等,都有一个单一的集中管理的服务器,保存所有文件的修订版本,而协同工作的人们都通过客户端连到这台服务器,取出最新的文件或提交更新。

svn 因为每次存的都是差异(A1、A2-A1…An-A(n-1)),需要的硬盘空间会相对小一点,可是回退的速度会很慢

优点:代码存放在单一服务器上便于项目管理

每个人都可以在一定程度上看到项目中的其他人正在做些什么,而管理员也可以轻松掌控每个开发者的权限,并管理一个集中化的版本控制系统,远比在各个客户端上维护本地数据库来的轻松容易

缺点:服务器宕机:员工写的代码得不到保障,不敢轻易修改代码;服务器炸了:整个项目历史记录都会丢失

中央服务器的单点故障,若服务器宕机一小时,这一小时内谁都无法提交更新也就无法协同工作。若中央服务器磁盘发生故障,且没备份或备份不及时,就会有丢失数据的风险,甚至彻底丢失整个项目的所有历史更改记录,而被客户端偶然提取出来保存在本地的某些快照数据不能保证所有数据都有人事先完整提取出来过,只要整个项目的历史记录被保存在单一位置,就有丢失所有历史更新记录的风险

分布式版本控制系统

在这类系统中,如 Git、BitKeeper 等客户端不只提取最新版本的文件快照,而是把代码仓库完整地镜像下来(每台电脑都相当于一台服务器)。这样任何一处协同工作用的服务器发生故障事后都可以用任何一个镜像出来的本地仓库恢复,因为每一次的提取操作,实际上都是一次对代码仓库的完整备份

许多这类系统都可以指定和若干不同的远端代码仓库进行交互,所以就可以在同一个项目中分别和不同工作小组的人相互协作

git 每次存的是项目完整快照(A1、A1A2…A(n-1)An),需要的硬盘空间相对大一点,但 Git 团队对代码做了极致的压缩,最终需要的实际空间比 svn 大不了很多,可是 Git 的回滚速度很快

分布式版本控制系统在管理项目时,存放的不是项目版本与版本之间的差异,它存的是索引,因此所需磁盘空间很少,每个客户端都可以放下整个项目的历史记录

优点:分布式版本控制系统解决了集中式版本控制系统的缺陷

— 断网时也可以进行开发(因为版本控制是在本地进行的)

— 使用 github 进行团队协作,哪怕 github 挂了,每个客户端保存的也都是包含历史记录的整个完整项目

缺点:学习起来比 svn 陡峭

二、Git

Git 是目前世界上最先进的分布式版本控制系统,设计目标是分支切换速度快,容量小(压缩),完全分布式,非线性分支管理(允许上千个并行开发的分支),适合管理大项目(对速度和数据量的高要求,如有能力管理类似 Linux 内核一样的超大规模项目)

1、Git 安装和配置

官网下载并安装

Git 提供了一个叫 git config 的命令来配置或读取相应的工作环境变量,这些变量可以存放在以下三个不同地方

/etc/gitconfig 文件:系统中所有用户都普遍适用的配置,若使用 git config 时用 --system 选项,读写的就是这个文件
~/.gitconfig 文件:用户都目录下的配置文件只适用于该用户,若使用 git config 时用 --global 选项,读写的就是这个文件
.git/config 文件:当前项目的 Git 目录中的配置文件(也就是工作目录中的 .git/config 文件),这里的配置仅针对当前项目有效

注意每个级别的配置都会覆盖上层的相同配置(项目目录下的配置文件优先级最高)

安装后需要配置用户信息(用户名称和电子邮箱),每次 Git 提交时都会引用这两条信息,会随更新内容一起被永久写入历史记录

git config --global user.name "xxx"
git config --global user.email xxx@xxx.com

通过 git config --list 查看已有配置信息

注:也可通过 git config --global --unset user.namegit config --global --unset user.email 删除配置信息

2、区域

区域有三个区域:工作区、暂存区(索引区)、版本库

工作区就是本地代码,是个沙箱环境,在将修改提交到暂存区并记录到历史之前可以随意修改

暂存区可通过 git ls-files -s 查看暂存区当前的样子

版本库可通过 find .git/objects -type f 查看版本库中所有文件

3、对象

对象有 Git 对象、树对象、提交对象

git 对象相当于文件的一个个版本

树对象相当于项目的一个个版本的快照

提交对象只是树对象的封装,提供作者等信息,提交对象是链式的

一个提交对象对应一个树对象,一个树对象可以对应很多个 git 对象,一个 git 对象对应一个文件,则一个提交对象可以对应很多个文件

4、Git 底层概念(底层命令)

基础 Linux 命令

clear:清除屏幕

echo ‘xxx’:往控制台输出信息

echo ‘xxx’ > xxx.txt 往控制台输出信息并存到文件中

ll:将当前目录下的子文件和子目录平铺在控制台

find 目录名:将对应目录下的子孙文件和子孙目录平铺在控制台

find 目录名 -type f:将对应目录下的文件平铺在控制台

rm 文件名:删除文件

mv 源文件 重命名文件:重命名

cat 文件的url:查看对应文件内容

vim 文件的url:编辑文件,按 esc 后按 :q!(强制推出不保存)或按 :wq(保存退出)或按 :set nu(设置行号)

git 对象

git 对象用于存储数据内容

Git 对象核心部分是个简单的键值对数据库,可向数据库插入任意类型内容,返回一个键值,通过键值可在任意时刻再次检索该内容

Git 存储的键值对(即 Git 对象)是 blob 类型

向数据库写入内容并返回对应键值

将控制台内容存入文件

echo 'test content' | git hash-object -w --stdin

-w 选项指示 hash-object 命令存储数据对象,若不指定该选项,则该命令仅返回对应键值,不存储数据

–stdin (standard input)选项指示该命令从标准输入读取内容,若不指定该选项则须在命令尾部给出待存储文件的路径 git hash-object -w 文件路径

该命令输出一个长度为 40 个字符的校验和(SHA-1 哈希值)

存文件(往 git 数据库中存对象)

git hash-object -w 文件路径

返回对应文件的键值

git hash-object 文件路径

Git 如何存储数据

开始时 Git 存储内容的方式是一个文件对应一条内容,校验和的前两个字符用于命名 objects 下子目录,余下的 38 个字符用作文件名

如存储数据后通过命令 find .git/objects -type f 查看保存的文件(假设该文件哈希值为 d670460aaaaaaaaaaaaabbbbbbbbbbcccccccccc),返回 .git/objects/d6/70460aaaaaaaaaaaaabbbbbbbbbbcccccccccc,该命令用于查看版本库内容

根据键值拉取数据

下列命令返回文件内容

git cat-file -p 完整哈希值

-p 选项指示该命令自动判断内容的类型,并显示格式友好的内容(即不是压缩后的乱码)

下列命令可显示存储的任何对象的类型

git cat-file -t 完整哈希值

对一个文件进行简单版本控制

创建一个新文件并将其内容存入数据库

echo 'version 1' > test.txt
git hash-object -w test.txt

返回文件哈希值

向文件写入新内容,并再次将其存入数据库

echo 'version 2' > test.txt
git hash-object -w test.txt

此时会生成新哈希值就会在 objects 中生成新目录新文件

注意上述所有操作当前都是对本地数据库进行操作,直接存至版本库,不涉及暂存区

问题

1、记住文件的每个版本所对应的 SHA-1 值并不现实

2、在 Git 中,文件名并没有被保存,仅保存了文件内容

解决:树对象

树对象

树对象(tree object)能解决文件名保存的问题,也允许将多个文件组织到一起。

Git 以一种类似于 UNIX 文件系统的方式存储内容,所有内容均以树对象和数据对象(git 对象)的形式存储,其中树对象对应了 UNIX 中的目录项,数据对象(git 对象)大致上对应文件内容

一个树对象包含一条或多条记录(一条记录即一个 git 对象),也可以包含另一个树对象,每条记录含有一个指向 git 对象或子树对象的 SHA-1 指针,及对应的模式、类型、文件名信息

构建树对象

可通过 update-index、write-tree、read-tree 等命令构建树对象并塞入暂存区

(1)利用 update-index 命令为 git 数据库中已存在的 test.txt 文件的首个版本创建一个暂存区,让 git 对象对应上文件名,并通过 write-tree 命令生成树对象并存入版本库

git update-index --add --cacheinfo 100644 d670460aaaaaaaaaaaaabbbbbbbbbbcccccccccc test.txt
git write-tree

其中文件模式
100644 表示这是个普通文件
100755 表示一个可执行文件
120000 表示一个符号链接

--add 选项:因为此前该文件并不在暂存区中,首次添加需要 --add

--cacheinfo 选项:因为将要添加的文件位于 Git 数据库中,而不是位于当前目录下,所以需要 --cacheinfo

(2)新增 new.txt 将 new.txt 和 test.txt 文件的第二个版本塞入暂存区,并通过 write-tree 命令生成树对象并存入版本库

echo 'new file' > new.txt
git update-index --cacheinfo 10064 d670460aaaaaaaaaaaaabbbbbbbbbbcccccccccc test.txt 此时会覆盖原先暂存区中的test.txt
git update-index --add new.txt 该命令完成两步(1)生成 git 对象存入版本库(2)将 new.txt 文件放入暂存区(执行类似上条命令,但需要加 --add,因为该文件首次加入暂存区)

(3)将第一个树对象加入第二个树对象,使其称为新的树对象

git read-tree --prefix=bak 第一个树对象的哈希值
git write-tree

read-tree 命令可以把树对象读入暂存区

最终树对象结构如下

树对象

查看树对象

git cat-file -p master^{tree}或是树对象的hash

master^{tree} 语法表示 master 分支上最新提交所指向的树对象

解析树对象

Git 给根据某一时刻暂存区(即 index 区域)所表示的状态创建并记录一个对应的树对象,如此重复便可依次记录某个时间段内一系列的树对象

其实树对象是对暂存区内操作的抽象,树对象相当于快照,当工作区有任何更改同步到暂存区时,便会调用 write-tree 命令

通过 write-tree 命令向暂存区内容写入一个树对象,它会根据当前暂存区状态自动创建一个新的树对象,即每一次同步都产生一个树对象,且该命令返回一个 hash 指向树对象

在 Git 中每个文件(数据)都对应一个 hash(blob 类型),每个树对象都对应一个 hash(tree 类型)

问题

现有两个树对象分别代表想要跟踪的不同项目快照,若想宠用这些快照必须记住所有 SHA-1 哈希值,可能完全不指定谁在什么时候保存了这些快照

提交对象

可通过调用 commit-tree 创建一个提交对象,为此需要指定一个树对象的 SHA-1 值,以及该提交的父提交对象(若有的话)(第一次将暂存区做快照就没有父对象)

git commit-tree 不但生成提交对象,而且会将对应的快照(树对象)提交到版本库中

创建提交对象

echo 'first commit' | git commit-tree 树对象哈希值

返回提交对象哈希值

查看提交对象

git cat-file -p 提交对象哈希值

提交对象的格式

它先指定一个顶层树对象,代表当前项目快照,然后是作者/提交信息(依据 user.name 和 user.email 配置,外加一个时间戳),空一行,最后是提交注释

创建另外两个提交对象,它们分别引用各自的上一个提交作为其父对象

echo 'second commit' | git commit-tree 树对象哈希值 -p 第一个提交对象哈希值
echo 'third commit' | git commit-tree 树对象哈希值 -p 上一个提交对象哈希值

5、Git 本地操作(高层命令)

git 操作基本流程

(1)创建工作目录,对工作目录进行修改

(2)git add 路径

(3)git commit -m “注释内容”

初始化新仓库

要对现有的某个项目开始用 Git 管理,只需在此项目所在目录下执行 git init 初始化新仓库

作用:初始化后在当前目录下会出现一个名为 .git 的目录,所有 Git 需要的数据和资源都存放在这个目录中

hooks:目录包含客户端或服务端的钩子脚本
info:包含一个全局性排除的文件
logs:保存日志信息
objects:目录存储所有数据内容
refs:目录存储指向数据的提交对象的指针(即分支)
config:文件包含项目特有的配置选项
description:用来显示对仓库的描述信息
HEAD:文件指示目前所检出的分支
index:文件保存暂存区信息

至此仅是按照既有的结构框架初始化里边所有文件和目录,还没开始跟踪管理项目中的任何一个文件

记录每次更新到仓库

工作目录下所有文件都不外乎两种状态:已跟踪 或 未跟踪

已跟踪的文件指本来就被纳入版本控制管理的文件,在上次快照中有它们的记录,工作一段时间后,它们的状态可能是已提交、已修改或已暂存

所有其他文件都属于未跟踪文件,它们既没有上次更新时的快照也不在当前的暂存区域

初次克隆某个仓库时,工作目录中的所有文件都属于已跟踪文件,且状态为已提交,在编辑过某些文件后,Git 将这些文件标记为已修改,我们逐步把这些修改过的文件放到暂存区,最后一次性提交所有暂存文件

跟踪新文件(将修改添加到暂存区)

git add ./

git add 命令是先将工作区的文件以 git 对象(几个文件就对应几个 git 对象)存储到版本库中,再存放到暂存区

此时这些文件受到 git 管理

该命令相当于底层命令中的 git hash-object -w 文件名(修改了多少个工作目录中的文件此命令就要被执行多少次)和 git update-index ...

提交更新(将暂存区提交到版本库)

方式一:

git commit

这种方式会启动文本编辑器以便输入提交注释,默认的注释是最后一次 git status 的输出

方式二:

git commit -m "注释"

执行上述两种命令进行提交,不会清空暂存区

该命令相当于底层命令中的 git write-treegit commit-tree

检查当前文件状态

git status

查看文件当前处于什么状态

文件状态生命周期如下图所示

文件状态生命周期

注意若对已经 add 但未 commit 的文件进行修改后使用 git status 查看会显示该文件有两种状态,一种是已暂存(这种状态对应最新修改前的版本),一种是已修改未暂存(这种状态对应最新修改后的版本),此时需要重新 add 后 commit 提交的才是最新版本

查看已暂存和未暂存的更新

git status 只是列出修改过的文件,若要查看具体修改的地方,用 git diff 命令

查看当前做的哪些更新还没有暂存

git diff

有哪些更新已经暂存还没提交

git diff --cached
或
git diff --staged(1.6.1以上版本)

跳过使用暂存区

给 git commit 加上 -a 选项,Git 会自动把所有已跟踪的文件暂存起来一并提交,从而跳过 git add 步骤(注意只有已跟踪的文件才能用这条命令,没有跟踪过的文件还是需要先 add)

git commit -a
或
git commit -a -m "注释"

移除文件

方式一:

若在工作目录中删除某文件相当于一次修改操作,依然需要 add 和 commit

方式二:

要从 Git 中移除某文件,必须从已跟踪文件清单中年注册删除(也就是在暂存区注册删除),然后提交

git rm 文件路径和名
git status
git commit -m "注释"

git rm 删除工作目录中指定文件,再将修改添加到暂存区,这样以后就不会出现在未跟踪文件清单中

文件重命名

git mv 原文件 新文件路径和名
git status

git mv 将工作目录中的文件进行重命名,再将修改添加到暂存区

运行 git mv 相当于运行了三条命令:(1)git mv 原文件 新文件(2)git rm 原文件(3)git add 新文件

查看历史记录

git log

默认不加任何参数时 git log 会按提交时间列出所有更新,最近的排在最上面,会列出哈希值、作者名、邮箱、提交时间、提交说明

git log --pretty=oneline

每条记录的信息排列在一行

git log --oneline

每条记录的信息排列在一行,且提交对象哈希值只显示前 7 位

git reflog

只要 HEAD 有变化,git reflog 就会记录下来

6、Git 分支操作(杀手功能)

几乎所有版本控制系统都以某种形式支持分支。使用分支可以把工作从开发主线上分离开来,以免影响开发主线,相当于创建一个源码目录副本,十分耗时低效,但 Git 的分支模型及其高效轻量,也是这一特性让 Git 脱颖而出

分支的本质是指向提交对象的可变指针,可理解为其实就是一个提交对象(相当于给提交对象取名),那个可变指针是 HEAD

HEAD 是个指针,默认指向 master 分支,切换分支时就是让 HEAD 指向不同分支

每次有新的提交时,HEAD 都会带着当前指向的分支一起往前移动

创建分支

git branch 分支名

作用:在当前所在提交对象上创建一个可移动的新指针

注意:创建新分支后不会自动切换到新分支上

git checkout -b 分支名

上述命令新建一个分支并切换到该分支上

git branch 分支名 提交对象哈希值

上述命令新建一个分支并使分支指向对应提交对象,通过该命令可实现版本回退,想回哪里回哪里,只需在之前某版本处创建一个分支指向它即可查看该版本内容

查看分支

git branch

得到当前所有分支的列表

删除分支

git branch -d 分支名

查看每个分支的最后一次提交

git branch -v

查看已合并到当前分支的分支

git branch -merged

在这个列表中分支名且前没有 * 号的分支通常可以使用

查看所有包含未合并工作的分支

git branch --no-merged

一旦出现在这个列表中就应该观察一些是否需要合并

使用 git branch -d 删除在这个列表中的分支时会失败,若真要删除分支并丢掉那些工作可使用 -D 选项强制删除它

查看当前分支所指对象

git log --oneline --decorate

切换分支

git checkout 分支名

即将 HEAD 指针指向切换的分支,HEAD 默认指向 master(有新版本 HEAD 带动着 master 向前移动)

切换分支会动三个地方:HEAD、暂存区、工作目录

最佳方法:每次切换分之前,当前分支一定得是干净的(已提交状态)(可通过 git status 查看是否 clean)

坑:在切换分支时,若当前分支上有未暂存修改或未提交的暂存,此时分支可以切换成功,但可能会污染其他分支(这种情况针对于增加某文件时,若文件已在某分支提交过只是修改,则不会有该坑,git 自己会禁止切换分支操作)

如在分支test中新增文件但未跟踪(没有 add)或 add 了但没有 commit,此时切回 master,该文件依然存在

若在分支test中修改文件但没有 add 或 add 了但没有 commit,此时 Git 就不让切换分支

注意:分支切换回改变工作目录中的文件,在切换分支时,一定要注意工作目录里的文件会被改变,若是切换到一个较旧的分支,工作目录会恢复到该分支最后一次提交时的样子,若 Git 不能干净利落完成这个任务将禁止切换分支

每次在切换分支前,提交一下当前分支

允许切换分支的情况:

(1)分支上所有内容处于已提交状态
(2)分支上内容是初始化创建的,处于未跟踪状态,但这种情况要避免切换分支
(3)分支上内容是初始化创建的且该文件是第一次处于已暂存状态(即第一次 add 该文件,且 add 后没有修改过该文件),但这种情况要避免切换分支

不允许切换分支的情况:

(1)分支上所有内容处于已修改状态
(2)第二次以后的已暂存状态

查看项目分支历史

git log --oneline --decorate --graph --all

分支合并

先切回 master 分支,再合并分支

git merge 分支名

快进合并(fast-forward):若合并两个分支时,顺着一个分支走下去能到达另一个分支,那么 Git 在合并时只会简单地将指针向前推进。快进合并没有冲突问题

典型合并:此时 master 不是待合并分支的直接祖先(不在一条线路上),可能两个分支上都对某文件内容进行了修改,要解决冲突就是打开有冲突的文件保留下有用代码删除无用代码后 add(该命令即可标记冲突已解决) 和 commit 提交即可

给命令配别名

Git 不会在输入部分命令时自动推断你想要的命令,若不想每次输入完整 Git 命令,可通过 git config 文件位每个命令设置一个别名,如

git config --global alias.简称 原命令(不包含'git')
或
git config --global alias.简称 "原命令(不包含'git')"
git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.ci commit
git config --global alias.st status

当要输入 git commit 时只需输入 git ci

分支实战

如在开发某网闸时为实现某新需求创建一个分支

原版本中出现一个 bug 需要修复时,要先将当前分支内容 add 和 commit 提交后切换到 master 分支,然后新建一个分支完成 bug 修复

测试通过后切回 master 分支,合并这个修补分支,最后将改动推送到 master 上,此时可以删除修补分支

最后切换回最初工作的分支上继续工作

分支模式

长期分支(即 master)

特性分支(如 dev、topic功能分支)

分支原理

.git/refs 目录中保存了分支及其对应的提交对象

HEAD 引用

当运行类似于 git branch 分支名 这样的命令时,Git 会取得当前所在分支最新提交对应的哈希值,并将其加入你想要创建的任何新分支中

当执行 git branch 分支名 时 Git 如何直到最新提交的哈希值呢?

答:通过 HEAD 文件,HEAD 文件是一个符号引用(symbolic reference),指向目前所在的分支,所谓符号引用意味着它不像普通引用那样包含一个 SHA-1 值,它是一个指向其他引用的指针

7、Git 存储

若工作一半需要切换分支,但原工作分支上不想创建一次提交,此时可以使用 git stash 命令

git stash 会将未完成的修改保存到一个栈上,可在任何时候通过 git stash apply 重新应用这些改动

git stash 相关命令

git stash list: 查看存储
git stash apply stash@{编号}:若不指定储藏,默认是最近的储藏,注意该命令只会应用栈顶元素不会从栈里删除,应用后可搭配 git ,编号可通过 git stash list 查看
stash drop 删除该元素
git stash drop stash@{编号}:移除某储藏
git stash pop: 应用储藏然后立即从栈上扔掉它(即应用并从栈中删除)

8、数据恢复

可能因为强制删除正在工作的分支或硬重置一个分支而丢失一次提交

撤销

撤回工作目录中的修改

git checkout -- 文件

撤回暂存

git reset HEAD 文件

撤回提交

git commit --amend

作用:该命令会将暂存区中的文件提交,若自上次提交以来还未做任何修改,那么快照会保持不变,修改的只是提交信息

若提交时注释写错了,可通过 git commit --amend 重新给用户一次机会改注释

若提交后还有需暂存的文件,或者 add 后又对文件进行修改,可使用以下命令提交新文件

git commit -m "第一次提交"   对于第二种情况这里提交的是之前 add 的文件即修改前的文件
git add 新的暂存文件
git commit --amend

第二次提交将代替第一次提交结果,最终只会有一个提交

重置

reset 三部曲

假设文件最初有三个版本,且 HEAD 和 master 都指向最后一次提交,如下图

reset0

(1)移动 HEAD

git reset --soft HEAD~
或
git reset --soft 上一次提交对象哈希值

上述指令与 checkout 所作的改变 HEAD 自身不同(HEAD 移动,分支不动),reset 是移动 HEAD 指向的分支

该命令只动 HEAD(带着分支一起移动),工作区和暂存区不变

本质上是撤销上一次 git commit 命令。运行 git commit 命令时 Git 会创建一个新的提交,并移动 HEAD 所指向的分支来使其指向该提交。当 reset 回 HEAD~(HEAD 的父结点)时,其实就是把该分支移动回原来的位置,不会改变索引和工作目录,此后可更新索引并再次运行 git commit 命令来完成 git commit –amend 所要做的事

HEAD~ 表示上一次 HEAD

git reset --soft HEAD~ 相当于 –amend

注意 git reset --soft 提交对象哈希值 可前进到回退之前的版本,这个哈希值可以通过 git reflog 查看

reset1

(2)更新暂存区(索引)

git reset [--mixed] HEAD~

git reset HEAD~ 等同于 git reset --mixed HEAD~

该命令动了 HEAD(带着分支一起移动)和暂存区

它依然会撤销上一次提交,同时还会取消上一次暂存的所有东西,所以回滚到上一次 add 和 commit 执行之前

注意 git reset --mixed 提交对象哈希值 可前进到回退之前的版本,这个哈希值可以通过 git reflog 查看

reset2

(3)更新工作目录

git reset --hard HEAD~

撤销了最后的提交、add、commit 以及工作目录中的所有工作

该命令动了 HEAD(带着分支一起移动)、暂存区和工作区

该命令和 chechout 很像,都动了 HEAD、暂存区和工作区,但唯一区别是 checkout 中分支位置没变(即 master 没有跟着 HEAD 移动)

reset3

注意:–hard 是 reset 命令唯一的危险用法,也是 Git 会真正销毁数据的仅有的几个操作之一,其他任何形式的 reset 都可以轻松撤销,但 –hard 选项不能,因为它强制覆盖了工作目录中的文件

在这种情况下,Git 数据库中的一个提交内还留有该文件的最新版(file.txt v3),可通过 reflog 找回它,但若该文件还未提交,Git 仍会覆盖它从而导致无法恢复

checkout 和 –hard 的区别

git checkout 提交对象哈希值git reset --hard 提交对象哈希值 的区别为

(1)checkout 只动 HEAD,–hard 动 HEAD 且带着分支一起走

(2)checkout 对工作目录是安全的(就算在某分支有没有提交的文件,在切换分支后该文件依然在工作区中),–hard 是强制覆盖工作目录

路径 reset

git reset [--mixed] HEAD 

在 reset 命令中若指定一个路径,reset 将会跳过第 1 步,即该命令只动暂存区,并将它的作用范围限定为指定文件或文件集合。这是因为 HEAD 只是个指针,无法让它同时指向两个提交中各自的一部分,但索引(即暂存区)和工作目录可以部分更新,所以重置会继续进行第 2、3 步

例子

如修改 file.txt 且 add 但没有 commit

add后

然后运行

git reset file.txt
其实是 git reset --mixed HEAD file.txt 的简写

其中 HEAD 指向上一次提交内容,上一次提交内容即为上一次暂存区中的内容,因此相当于用上一次暂存区覆盖当前暂存区,本质上就是一次重置操作

它会移动 HEAD 分支的指向

reset后

checkout

不带路径
git checkout [分支名]

运行该命令与运行 git reset --hard [分支名] 非常相似,它会更新三者使其看起来像要切换的分支,不过有两点重要区别

区别1:不同于 reset --hard,checkout 对工作目录是安全的,它会通过检查来确保不会将已更改的文件弄丢,而 reset --hard 会不做检查就全面替换所有东西
区别2:如何更新 HEAD。reset 会移动 HEAD 分支的指向,而 checkout 只会移动 HEAD 自身来指向另一个分支

如:在 dev 分支运行 git reset master,则 dev 自身会和 master 指向同一个提交,而若运行 git checkout,dev 不会移动,HEAD 自身会移动,现在 HEAD 会指向 master
带路径

方式一:

git checkout 提交对象哈希值 文件

若指定一个文件路径,则会像 reset 一样不会移动 HEAD,就像 git reset --hard [分支名] 文件

这样对工作目录不安全,会跳过第 1 步,更新暂存区和工作目录

方式二:

git checkout -- 文件

相比于 git checkout 提交对象哈希值 文件,上述命令第 1、2 步都没做,即 HEAD 和暂存区都没动,只会动工作目录

数据恢复的方式

方式一:找到对应提交对象的哈希值再使用 reset –hard 硬重置

方式二: 在对应提交对象上创建一个分支

git branch 分支名 提交对象哈希值

9、打 tag

Git 可以给历史中的某个提交打上标签,一般会用这个功能标记发布结点(v1.0 等)

列出标签

git tag
git -l 'v1.2.3*' 会列出 v1.2.3 开头的标签

创建标签

Git 使用两种主要类型的标签:轻量标签和附注标签

轻量标签像是一个不会改变的分支,它只是一个特定提交的引用,轻量标签如下

git tag v1.2
git tag v1.2 提交对象哈希值

附注标签是存储在 Git 数据库中的一个完整对象,它可以有哈希值,其中包含打标签者的名字、邮箱、日期时间、标签信息。附注标签如下

git tag -a v1.2
git tag -a v1.2 提交对象哈希值
git tag -a v1.2 提交对象哈希值 -m "注释"

查看特定标签

git show 标签名

git show 可显示任意类型对象(git 对象、树对象、提交对象、tag 对象)

远程标签

默认情况下,git push 命令不会传送标签到远程仓库服务器上,创建完标签后必须显式推送标签到共享服务器上

git push origin [标签名]

若要一次性推送很多标签也可使用 –tags 选项,会把所有不在远程仓库服务器上的标签都推送到那里

git push origin --tags

删除标签

git tag -d 标签名

上述命令可删除本地仓库上的标签,但不会从任何远程库中移除该标签,必须使用如下命令更新远程仓库

git push origin :refs/tags/v1.2

检出标签

查看某个标签所指向的文件版本并创建新分支指向它:

git checkout 标签名
git checkout -b 分支名

虽说这会使仓库处于 “分离头指针(detached HEAD)” 状态(即 HEAD 并没有指向哪个分支,而是指向了标签对应的提交版本),在该状态下,若做某些更改并提交,标签不会发生变化,但新提交将不属于任何分支,并将无法访问,除非访问确切的提交对象哈希值,因此若需要进行更改,如修复旧版本的错误,通常要创建一个新分支

三、Git 命令整理

1、Git 底层命令

git 对象

git hash-object -w 文件路径
上传一个 key(hash值):val(压缩后的文件内容)存到 .git/objects

tree 对象

git update-index --add --cacheinfo 文件模式 哈希值 文件名
往暂存区添加一条记录(让 git 对象对应上文件名)存到 .git/index
git write-tree
生成树对象

commit 对象

echo 'xxxxxx' | git commit-tree 树对象哈希值
生成一个提交对象

对以上对象的查询

git cat-file -p 哈希值
拿到对应对象的内容
git cat-file -t 哈希值
拿到对应对象的类型

查看暂存区

git ls-files -s

2、Git 高层命令

安装

git --version

初始化配置

git config --global user.name "名字"
git config --gloabal user.email 邮箱
git config --list

初始化仓库

git init

C(新增)

在工作目录新增文件
git status
git add ./
git commit -m "注释"

U(修改)

在工作目录修改文件
git status
git add ./
git commit -m "注释"

D(删除 & 重命名)

git rm 文件路径
git status
git commit -m "注释"

git mv 老文件 新文件
git status
git commit -m "注释"

R(查询)

git status 查看工作目录文件状态
git diff
git diff --cache
git log --oneline
git log --oneline --decorate --graph --all

分支

git log --oneline --decorate --graph --all
查看整个项目的分支图
git branch
查看分支列表
git branch -v
查看分支指向的最新的提交
git branch 分支名
在当前提交对象上创建新分支
git branch 分支名 提交对象哈希值
在指定提交对象上创建新的分支
git checkout 分支名
切换分支
git checkout -b 分支名
创建并切换分支
git branch 分支名 提交对象哈希值
版本穿梭(时光机)
git branch -d 分支名
删除空的分支(即该分支上没有做过任何提交)/删除已经被合并的分支
git branch -D 分支名
强制删除分支
git merge 分支名
合并分支
    快进合并 -- 不会产生冲突
    典型合并 -- 有可能产生冲突
    解决冲突:打开冲突文件进行修改然后 add、commit
git branch --merged
查看合并到当前分支的分支列表,一旦出现在这个列表就应该删除
git branch --no-merged
查看没有合并到当前分支的分支列表,一旦出现在这个列表就应该观察是否需要合并
git stash 将现有做一半的工作存储到栈中
git stash apply 将栈顶工作内容还原,不出栈
git stash drop 删除栈顶
git stash pop 相当于 apply 和 drop 两步操作
git stash list 查看存储

后悔药

git checkout -- 文件名:撤销工作目录的修改
git reset HEAD 文件名:撤销暂存区的修改
git commit --amend: 撤销提交    

reset 三部曲

git reset --soft 提交对象哈希值:用对应提交对象内容重置 HEAD 内容(可对应后悔药中的撤销提交)
git reset [--mixed] 提交对象哈希值:用对应提交对象内容重置 HEAD 内容、重置暂存区(可对应后悔药中的撤销暂存区的修改)
git reset --hard 提交对象哈希值:用对应提交对象内容重置 HEAD 内容、重置暂存区、重置工作目录(可对应后悔药中的撤销工作目录的修改)

三部曲和后悔药其实是一一对应的

路径 reset

所有的路径 reset 都要省略第一步
第一步是重置 HEAD 内容(HEAD 本质指向一个分支,分支的本质是个提交对象,提交对象指向一个树对象,树对象可能指向多个 git 对象,一个 git 对象代表一个文件)
HEAD 可代表一系列文件的状态

git reset [--mixed] 提交对象哈希值 文件
用对应提交对象中的文件的内容重置暂存区

checkout 深入理解

‘git checkout 分支名’ 和 ‘git reset –hard 提交对象哈希值’很像

共同点:

都需要重置 HEAD、暂存区、工作目录

区别:

checkout 对工作目录是安全的,reset --hard 是强制覆盖
checkout 动 HEAD 不会带着分支走,而是切换分支
reset --hard 是带着分支走

checkout + 路径

git checkout 提交对象哈希值 文件
重置暂存区、工作目录
git checkout -- 文件
重置工作目录

四、Git 特点

(1)直接记录快照,而非差异比较

Git 和其他版本控制系统的主要差别在于 Git 只关心数据的整体是否发生变化,而大多数其他系统(如 CVS、Subversion、Perforce、Bazaar 等)则只关心文件内容的具体差异,这类系统每次记录哪些文件做了更新以及更新了哪些行什么内容

(2)近乎所有操作都是本地执行

Git 中绝大多数操作都只需要访问本地文件和资源,不用连网。但若使用 CVCS 的话几乎所有操作都需连网,因为 Git 在本地磁盘上保存着所有当前项目的历史更新,所以处理起来速度飞快

(3)时刻保持数据完整性

在保存到 Git 之前,所有数据都要进行内容的校验和计算,并将此结果作为数据的唯一标识和索引。索引在文件传输时变得不完整或磁盘损坏导致文件数据缺失,Git 都能立即察觉、

Git 使用 SHA-1 算法计算数据的校验,通过对文件内容或目录结构计算出一个 SHA-1 哈希值作为指纹字符串,由 40 个十六进制字符组成。Git 的工作完全依赖于这类指纹字符串

(4)多数操作仅添加数据

因为任何一种不可逆的操作,如删除数据,都会使回退或重现历史版本变得困难重重。

在 VCS 中若还未提交更新就有可能丢失或混淆一些修改内容,但在 Git 里一旦提交快照后就完全不用担心丢失数据

(5)文件的三种状态

对于任何一个文件,在 Git 内部只有三种状态(Git 外的状态就是一个普通文件):

已提交(committed):表示该文件已经被安全保存在本地数据库中

已修改(modified):表示修改了某文件但还没提交保存

已暂存(staged):表示把已修改的文件放在下次提交时要保存的清单中

五、Git 工作流程

每个项目都有个 Git 目录(.git),它是 Git 用来保存元数据和对象数据库的地方。每次克隆镜像仓库时,实际拷贝的就是这个目录里的数据

1、在工作目录中修改某些文件

从项目中去除某个版本的所有文件和目录(工作目录),这些文件实际上都是从 Git 目录中的压缩对象数据库中提取出来的。接下来可在工作目录中编辑文件

2、保存到暂存区,对暂存区做快照

暂存区只不过是个简单的文件,也叫索引文件,一般都放在 Git 目录中

3、提交更新,将保存在暂存区的文件快照永久转储到本地数据库(Git 目录)中

可以从文件所处位置判断状态:

—若是 Git 目录中保存着的特定版本文件,就属于已提交状态

— 若做了修改并已放入暂存区,就属于已暂存状态

— 若自上次取出后,做了修改但还没放到暂存区,就是已修改状态

六、代码风格

Eslint

Eslint 是个开源的 JavaScript 代码检查工具,由 Nicholas C.Zakas 于 2013 年 6 月创建。代码检查是一种静态的分析,常用于寻找有问题的模式或代码,并且不依赖于具体的编码风格,一般编译程序会内置检查工具

JavaScript 是个动态的弱类型语言,在开发中较易出错,因为没有编译程序,为了寻找 JavaScript 代码错误通常需要在执行过程中不断调试。ESLin 可让程序员在编码过程中发现问题而不是在执行过程中

ESLint 初衷是为了让程序员可以创建自己的检测规则。ESLint 的所有规则都被设计成可插入的。ESLint 的默认规则与其他插件没有什么区别,规则本身和测试可依赖于同样的模式。为了便于使用,ESLint 内置了一些规则,也可在使用过程中自定义规则

ESLint 使用 Node.js 编写,这样既有一个快速的运行环境也便于安装

Lint 是检验代码格式工具的统称,具体的工具由 Jslint、Eslint 等

git 时忽略某些文件

一些自动生成的文件如日志文件、编译过程中创建的临时文件等无需纳入 Git 管理

可创建一个 .gitignore 文件列出要忽略的文件模式,如

*.[oa] 忽略以 .o 或 .a 结尾的文件
*~ 忽略以 ~ 结尾的文件

.gitignore 的格式规范

所有空行或以注释符号 # 开头的行都会被 Git 忽略

可使用标准的 glob 模式匹配

* 代表匹配任意个字符
? 代表匹配任意一个字符
** 代表匹配多级目录
匹配模式前跟反斜杠(/)代表项目根目录
匹配模式最后跟反斜杠(/)说明要忽略的是目录
在模式前加感叹号(!)取反表示忽略指定模式以外的文件或目录

.gitignore 文件一般内容如下:

.DS_Store
node_modules/
/dist/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# 编辑器的目录和文件
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln

在 GitHub 有一个针对数十种项目及语言的.gitignore 文件列表,可找到对应的 .gitignore 文件进行使用

glob 模式

glob 模式指 shell 所使用的简化了的正则表达式

使用 Eslint

(1)安装 node 和 npm 环境

(2)创建项目

npm init

(3)本地安装 eslint

npm i eslint --save-dev

(4)设置 package.json 文件

"scripts"{
    "lint": "eslint ./src"
    "lint:create": "eslint --init"
}

(5)生成 .eslintrc.js 文件,提供编码规则

npm run lint:create

(6)校验代码,自动检验 src 目录下所有的 .js 文件

npm run lint

注意(4)(5)(6)也可以换成 npx eslint --initnpx eslint 目录名

eslint 结合 git

husky 帮助自动注册很多 git 的钩子,为 git 仓库设置钩子程序

要先有 git 仓库再安装 husky

git init
npm i husky --save-dev

在 package.json 中添加如下配置

"husky": {
    "hooks": {
        "pre-commit": "npm run lint", //表示在 git commit 之前要通过 npm run lint,不通过不让提交
        "pre-push": "npm test",
        "...": "..."
    }
}

七、团队协作

远程仓库

为了能在任意Git 项目上团队协作,远程仓库是指托管在因特网或其他网络中的你的项目的版本库。

管理远程仓库包括添加远程仓库、移除无效远程仓库、管理不同的远程分支并定义它们是否被跟踪等

远程协作基本流程

GitHub 是最大的 Git 版本库托管商,大部分 Git 版本库都托管在 GitHub,很多开源项目使用 GitHub 实现 Git 托管、问题追踪、代码审查以及其他事情

(1)项目经理创建远程仓库

在 github 中创建一个未初始化的空仓库

(2)项目经理创建本地库

git init

(3)项目经理为远程仓库配置别名与用户信息

git remote add 简称 远程仓库的https路径

添加一个新的远程 Git 仓库,同时指定一个简称

git config user.name "xxx"
git config user.email "xxx@xxx.com"

配置用户信息

git remote -v 

显式远程仓库使用的 Git 别名与其对应的 URL

git remote show 远程仓库名

查看某个远程仓库的更多信息

git remote rename pb 新名字

重命名

git remote rm 远程仓库名

移除某个远程仓库

(4)项目经理推送本地项目到远程仓库

初始化一个本地仓库并清理 Windows 凭据,然后

git push 远程仓库名 分支名

推完后会附带生成远程跟踪分支

(5)成员克隆远程仓库到本地

git clone 远程仓库的https路径

克隆后在本地生成 .git 文件,默认克隆时为远程仓库起的别名为 origin,并且默认主分支有对应的远程跟踪分支

git clone -o 别名

默认的远程仓库别名为自定义

注意:只有在克隆时本地分支 master 和远程跟踪分支 别名/master 是有同步关系的,即后续在本地该分支 push 时即使不指定远程分支也能准确 push 到对应远程分支上

(6)项目经理邀请成员加入团队

在 GitHub 项目仓库的 ‘Settings’ –> ‘Collaborators’ 中设置

(7)成员推送提交到远程仓库

git push 远程仓库名 分支名

此处 push 后也会附带生成远程跟踪分支

(8)项目经理更新成员提交的内容

git fetch 远程仓库名

该命令会访问远程仓库,从中拉取所有你还没有的数据,将修改同步到远程跟踪分支上,执行完后,将拥有那个远程仓库中所有分支的引用,可随时合并或查看

git merge 远程跟踪分支

git fetch 会将数据拉取到本地仓库,它不会自动合并或修改当前工作,需手动将其合并入你的工作

上述 fetch + merge 也可以使用 pull 代替(前提是本地分支需要关联远程跟踪分支才能直接使用 git pull)

正常的数据推送和拉取步骤

(1)确保本地分支已经跟踪了远程跟踪分支(远程跟踪分支和远程分支间的关系在 push 时已自动建立好了)

(2)拉取数据:git pull

(3)上传数据:git push

删除远程分支

删除远程分支

git push origin --delete 分支

列出仍在远程跟踪但远程已被删除的无用分支

git remote prune origin --dry-run

清除上面命令列出来的远程跟踪

git remote prune origin

pull request 流程

派生(Fork)指 GitHub 将在你的空间中创建一个完全属于你的项目副本,且对其有推送权限

当想参与某个项目但没有推送权限时可对该项目进行 “派生”

派生项目后将修改推送到派生处的项目副本中,并通过创建合并请求(pull request)来让改动进入源版本库

基本流程

(1)从 master 分支中创建一个新分支(自己 fork 的项目)

(2)提交一些修改来改进项目(自己 fork 的项目)

(3)将这个分支推送到 GitHub 上(自己 fork 的项目)

(4)创建一个合并请求

(5)讨论,根据实际情况继续修改

(6)项目的拥有者合并或关闭你的合并请求

注意:每次再发起新的 pull request 时,要去拉取最新的源仓库的代码,而不是自己 fork 的那个仓库

git remote add 源仓库简称 源仓库路径
git fetch 远程仓库名字
git merge 对应的远程跟踪分支

深入理解远程仓库

远程跟踪分支

远程跟踪分支是远程分支状态的引用,它们是不能移动的本地分支,当做任何网络通信操作时,它们会自动移动

它们以(remote)/(branch)形式命名,例如若要看最后一次与远程仓库 orgin 通信时 master 分支的状态,可查看 origin/master 分支

在 push 时会产生远程跟踪分支

当克隆一个仓库时,它通常会自动创建一个跟踪 origin/master 的 master 分支(这是个本地分支)

从网络 Git 服务器中克隆一个仓库会自动命名为 origin,拉取它的所有数据,并创建一个指向它的 master 分支的指针,
并在本地将其命名为 origin/master。
Git 也会给你一个与 origin/master 分支指向同一个地方的本地的 master 分支,这样就有工作的基础

跟踪分支(这是个本地分支)

跟踪分支是与远程分支有直接关系的本地分支,若在一个跟踪分支上输入 git pull,Git 能自动识别去哪个服务器上抓取、合并到哪个分支

从一个远程跟踪分支(origin/master)检出一个本地分支会自动创建一个叫作跟踪分支(也称上游分支 master)

只有主分支并且在克隆时才会自动创建跟踪分支

git checkout -b 分支名 远程仓库/远程分支
或
git checkout --track 远程仓库/远程分支

上述命令可设置跟踪分支关联远程跟踪分支,但第二种命令方式创建的本地分支与远程分支同名,若要不同命则使用第一种命令方式

若已有本地分支,想设置已有的本地分支跟踪一个刚刚拉取下来的远程分支,或想修改正在跟踪的跟踪分支,可在任意时间使用 -u 选项运行 git branch 来显式地设置,如下

git branch -u 远程仓库/远程分支
-u 相当于 --set-uptream-to

查看设置的所有跟踪分支:

git branch -vv

推送其他分支

本地分支并不会自动与远程仓库同步,必须显式地推送想要分享的分支

git push origin 本地分支名

这里 Git 自动将本地分支名展开为 refs/heads/本地分支名:refs/heads/远程分支名,也可运行 git push origin 本地分支:远程分支

git fetch origin

抓取数据,注意当抓取到新的远程跟踪分支时,本地不会自动生成一个对应分支,只有一个不可修改的 origin/远程分支 指针

因此需要将抓取到的合并到当前所在本地分支

git merge origin/远程分支

若想要在自己的本地分支上工作,可建立在远程跟踪分支之上

git checkout -b 本地分支 origin/远程分支

注意:上述的 origin 可以换成对应的远程仓库名

本地分支如何跟踪一个远程跟踪分支

1、当克隆时会自动生成一个 master 本地分支(已经跟踪了对应的远程跟踪分支)

2、在新建其他分支的同时可指定想要跟踪的远程跟踪分支

git checkout -b 本地分支名 远程跟踪分支
或
git checkout --track 远程跟踪分支

3、将一个已经存在的本地分支改成一个跟踪分支来对应远程跟踪分支

git branch -u 远程跟踪分支

冲突

当 push 时有冲突,需要先 git pull,修改冲突文件后 add 和 commit,然后再 git push

八、SSH

SSH 协议是 GitHub 自己的协议,https 和 SSH 都是用于验证是哪个用户

使用 https 协议时每次访问远程仓库都要去找到 Windows 凭据,凭据里代表了是哪个用户,而 SSH 协议不用填用户名和密码,和 GitHub 账户是无关联的

在 GitHub 账户中配置公私钥方法:

(1)生成公私钥

ssh-keygen -t rsa -C 邮箱

(2)在 C:\Users\Administrator.ssh 会生成公钥 id_rsa.pub 和公钥 id_rsa

(3)在 GitHub 的 ‘Personal settings’ –> ‘SSH and GPG keys’ 中新增一个 SSH key,把公钥 id_rsa 中的内容复制粘贴进去

测试公私钥是否已经配对

ssh -T git@github.com

之后的克隆需要使用 SSH 协议而不是 https 协议

九、使用频率最高的五个命令

git status
git add
git commit
git push
git pull