大家好!
欢迎大家来一起交流Git使用心得,相信很多同事对Git都很熟悉了,如果下面说的有错误的“知识点”,欢迎批评指正。
初识Git
我认识Git已经很多年了(我在有道云笔记里面“Git”文件夹的创建时间是: 2014-03-24 11:04)。
我接触的第一个软件版本控制系统是SVN(在Windows7系统下面)。
简单的安装,配置账号把代码clone下来就可以使用了(写完一个功能提交、拉取就好了);
一切都很简单,我一个人用SVN非常愉快(期间遇到过SVN账号权限不足问题,老大就直接给了我超级权限,问题解决了)。
我当时负责的是一个社交网络系统,一个人愉快的做了一个多月(期间发布版本、修复bug自然不会有不愉快),之后来了一个哥们与我一同开发,在两周时间内,发生了三四次代码相互覆盖的问题。
当时是“提交的代码被覆盖了就找不回来了(也可能是对SVN不熟悉,不知道如何找回)”,这让大家都非常的不愉快。
之后我就在网上找了下,看有没有类似问题的解决方法,就这样,我知道了Git;
自己一个人用了一个星期之后,推荐给“老大”,之后在技术部推广Git。
再之后,我只要感觉别人有需要就给他推销Git。
Git诞生
资料来源
Linus在1991年创建了开源的Linux,从此,Linux系统不断发展,已经成为最大的服务器系统软件了。
在2002年以前,世界各地的志愿者把源代码文件通过diff的方式发给Linus,然后由Linus本人通过手工方式合并代码!
2002年,Linus选择了一个商业的版本控制系统BitKeeper,BitKeeper的东家BitMover公司出于人道主义精神,授权Linux社区免费使用这个版本控制系统。
开发Samba的Andrew试图破解BitKeeper的协议(这么干的其实也不只他一个),被BitMover公司发现了(监控工作做得不错!),于是BitMover公司怒了,要收回Linux社区的免费使用权。
Linus花了两周时间自己用C写了一个分布式版本控制系统,这就是Git!一个月之内,Linux系统的源码已经由Git管理了!牛是怎么定义的呢?大家可以体会一下。
Git缺点
实在找不到Git的什么缺点了,就把“命令太多、学习曲线陡峭”算一个吧;
Git优点
资料来源(自己有添加),我这里列举了10条
1 快速
支持快速切换分支方便合并,比较合并性能好。在同一目录下即可切换不同的分支,方便合并,且合并文件速度比SVN快。
如果你每移动一下鼠标都要等待五秒,是不是很受不了?版本控制也是一样的,每一个命令多那么几秒钟,一天下来也会浪费你不少时间。Git的操作非常快速,你可以把时间用在别的更有意义的地方。
2 离线工作
版本库本地化,支持离线提交,相对独立不影响协同开发。
在没有网络的情况下如何工作?如果你用SVN或者CVS的话就很麻烦。而Git可以让你在本地做所有操作,提交代码,查看历史,合并,创建分支等等。
3 回退
人难免犯错。我很喜欢Git的一点就是你可以“undo”几乎所有的命令。你可以用这个功能来修正你刚刚提交的代码中的一个问题或者回滚整个代码提交操作。你甚至可以恢复一个被删除的提交,因为在后端,Git几乎不做任何删除操作。
4 省心
你们有没有丢失过版本库?
我有,而那种头疼的感觉现在还记忆犹新。而用Git的话,我就不必担心这个问题,因为任何一个人机器上的版本都是一个完整的备份。
(我有过一次“不小心把.git 目录给覆盖掉了,有几个commit再也找不回来了”的经历)一次换电脑,我直接覆盖项目目录,把.git目录给覆盖了
5 更少的“仓库污染”
git对于每个工程只会产生一个.git目录,这个工程所有的版本控制信息都在这个目录中,不会像SVN那样在每个目录下都产生.svn目录。
6 完整克隆的版本库
把内容按元数据方式存储,完整克隆版本库。所有版本信息位于.git目录中,它是处于你的机器上的一个克隆版的版本库,它拥有中心版本库上所有的东西,例如标签、分支、版本记录等。
7 分布式版本库,无单点故障,内容完整性好
内容存储使用的是SHA-1哈希算法。这能确保代码内容的完整性,确保在遇到磁盘故障和网络问题时降低对版本库的破坏。
8 保持工作独立
把不同的问题分开处理将有助于跟踪问题的进度。当你在为功能A工作的时候,其他人不应该被你还没有完成的代码所影响。分支是解决这个问题的办法。虽然其他的版本控制软件业有分支系统,但是Git是第一个把这个系统变得简单而快速的系统。
9 随时暂存当前工作
在同一项目中,可以随时暂存当前工作结果,去处理其他任务;我经常有该功能,强烈的感受到git带来的便利。
10 自由选择工作方式
使用Git,你可以同时和多个远程代码库连接,“rebase”而不是"merge"甚至只连接某个模块。但是你也可以选择一个中央版本库,就像SVN那样。你依然可以利用Git的其他优点。
简单的下载安装配置
- Windows安装
msysgit是Windows版的Git,从https://git-for-windows.github.io下载,然后按默认选项安装即可.
安装完成后,在开始菜单里找到“Git”->“Git Bash”,蹦出一个类似命令行窗口的东西,就说明Git安装成功!
- MacOS 安装
brew install git
- Linux Debian或Ubuntu Linux
sudo apt-get install git
# or
sudo apt-get install git-core
因为以前有个软件也叫GIT(GNU Interactive Tools),结果Git就只能叫git-core了。由于Git名气实在太大,后来就把GNU Interactive Tools改成gnuit,git-core正式改为git。
- CentOS安装
yum install git
- 源码安装
参考
安装完成后,还需要最后一步设置,在命令行输入:
$ git config --global user.name "Your Name"
$ git config --global user.email "email@example.com"
注意git config命令的--global参数,用了这个参数,表示你这台机器上所有的Git仓库都会使用这个配置,当然也可以对某个仓库指定不同的用户名和Email地址。
因为Git是分布式版本控制系统,所以,每个机器都必须自报家门:你的名字和Email地址。你也许会担心,如果有人故意冒充别人怎么办?这个不必担心,首先我们相信大家都是善良的群众,其次,真的有冒充的也是有办法可查的。(查登陆日志,根据pub key唯一定位一个用户;如果是https,需要用户名和口令登陆,用户名可以唯一定位一个用户)
Git几个基本的概念
上图中左侧为工作区,右侧为版本库。在版本库中标记为 "index" 的区域是暂存区(stage, index),标记为 "master" 的是 master 分支所代表的目录树。
一般来说,日常使用只要记住下图6个命令,就可以了。但是熟练使用,恐怕要记住60~100个命令。
远程仓储
简单的理解,就是把本地的版本库的数据在远端服务器做一个备份,第一是便于和其他伙伴交换代码;第二是可以给数据做一份安全备份。
当然,还可以在远端服务器添加一些脚本(钩子),实现更多的功能。比如自动代码质量检测,自动发布等
版本库
Git 版本库(repository)只是一个简单的数据库,其中包含所有用来维护与管理项目的修订版本和历史的信息。在Git中,跟大多数版本控制系统一样,一个版本库维护项目整个生命周期的完整副本。Git版本库不仅仅提供版本库中所有文件的完整副本,还提供版本库本身的副本。
暂存区(stage、index)
该区域是用来保存要提交到本地版本库中的所有文件, 称为stage或index。当执行git commit指令时候,会一次性将该区域类的文件提交到本地版本库。
可以理解为相当于一个缓存区,用来缓存要交给本地版本库管理的文件。而要将文件加入暂存区域,需要使用git add指令进行添加操作。也就是说要提交到本地版本库需要两步操作: git add + git commit。执行完这两条只是提交到了本地版本库,只能自己来使用,要是在团队开发中,要需要执行git push提交到远程仓储.
工作区
所谓的工作区, 就是你项目所在的文件夹里,都可以统称为工作区。
HEAD指针
说简单一点,HEAD 就是当前活跃分支的游标。也就是我们是通过HEAD所指向的版本来确定当前所在的版本,所以当我们进行版本切换的时候,进行的就是改变HEAD指针的指向。
HEAD严格来说不是指向提交,而是指向master,master才是指向提交的,所以,HEAD指向的就是当前分支。
Git对象
什么是HEAD?什么是master?为什么它们二者(在上一章)可以相互替换使用?为什么Git中的很多对象像提交、树、文件内容等都用40位的SHA1哈希值来表示?这一章的内容将会揭开这些奥秘,并且还会画出一个更为精确的版本库结构图。
Git对象库探秘 内容来源
通过查看日志的详尽输出,会惊讶的看到非常多的“魔幻数字”,这些“魔幻数字”实际上是SHA1哈希值。
$ git log -1 --pretty=raw
commit e695606fc5e31b2ff9038a48a3d363f4c21a3d86
tree f58da9a820e3fd9d84ab2ca2f1b467ac265038f9
parent a0c641e92b10d8bcca1ed1bf84ca80340fdefee6
author Jiang Xin <jiangxin@ossxp.com> 1291022581 +0800
committer Jiang Xin <jiangxin@ossxp.com> 1291022581 +0800
which version checked in?
一个提交中居然包含了三个SHA1哈希值表示的对象ID。
-
commit e695606fc5e31b2ff9038a48a3d363f4c21a3d86
这是本次提交的唯一标识。
-
tree f58da9a820e3fd9d84ab2ca2f1b467ac265038f9
这是本次提交所对应的目录树。
-
parent a0c641e92b10d8bcca1ed1bf84ca80340fdefee6
这是本地提交的父提交(上一次提交)。
研究Git对象ID的一个重量级武器就是git cat-file命令。用下面的命令可以查看一下这三个ID的类型。
$ git cat-file -t e695606
commit
$ git cat-file -t f58d
tree
$ git cat-file -t a0c6
commit
在引用对象ID的时候,没有必要把整个40位ID写全,只需要从头开始的几位不冲突即可。
下面再用git cat-file命令查看一下这几个对象的内容。
- commit对象e695606fc5e31b2ff9038a48a3d363f4c21a3d86
$ git cat-file -p e695606
tree f58da9a820e3fd9d84ab2ca2f1b467ac265038f9
parent a0c641e92b10d8bcca1ed1bf84ca80340fdefee6
author Jiang Xin <jiangxin@ossxp.com> 1291022581 +0800
committer Jiang Xin <jiangxin@ossxp.com> 1291022581 +0800
which version checked in?
这些对象保存在哪里?当然是Git库中的objects目录下了(ID的前两位作为目录名,后38位作为文件名)。用下面的命令可以看到这些对象在对象库中的实际位置。
$ for id in e695606 f58da9a a0c641e fd3c069; do \
ls .git/objects/${id:0:2}/${id:2}*; done
.git/objects/e6/95606fc5e31b2ff9038a48a3d363f4c21a3d86
.git/objects/f5/8da9a820e3fd9d84ab2ca2f1b467ac265038f9
.git/objects/a0/c641e92b10d8bcca1ed1bf84ca80340fdefee6
.git/objects/fd/3c069c1de4f4bc9b15940f490aeb48852f3c42
下面的图示更加清楚的显示了Git对象库中各个对象之间的关系。
从上面的图示中很明显的看出提交(Commit)对象之间相互关联,通过相互之间的关联则很容易的识别出一条跟踪链。这条跟踪链可以在运行git log命令时,通过使用--graph参数看到。下面的命令还使用了--pretty=raw参数以便显示每个提交对象的parent属性。
$ git log --pretty=raw --graph e695606
* commit e695606fc5e31b2ff9038a48a3d363f4c21a3d86
| tree f58da9a820e3fd9d84ab2ca2f1b467ac265038f9
| parent a0c641e92b10d8bcca1ed1bf84ca80340fdefee6
| author Jiang Xin <jiangxin@ossxp.com> 1291022581 +0800
| committer Jiang Xin <jiangxin@ossxp.com> 1291022581 +0800
|
| which version checked in?
|
* commit a0c641e92b10d8bcca1ed1bf84ca80340fdefee6
| tree 190d840dd3d8fa319bdec6b8112b0957be7ee769
| parent 9e8a761ff9dd343a1380032884f488a2422c495a
| author Jiang Xin <jiangxin@ossxp.com> 1290999606 +0800
| committer Jiang Xin <jiangxin@ossxp.com> 1290999606 +0800
|
| who does commit?
|
* commit 9e8a761ff9dd343a1380032884f488a2422c495a
tree 190d840dd3d8fa319bdec6b8112b0957be7ee769
author Jiang Xin <jiangxin@ossxp.com> 1290919706 +0800
committer Jiang Xin <jiangxin@ossxp.com> 1290919706 +0800
initialized.
最后一个提交没有parent属性,所以跟踪链到此终结,这实际上就是提交的起点。
commit,blob,tree,tag
每个对象(object) 包括三个部分:类型,大小和内容。大小就是指内容的大小,内容取决于对象的类型,有四种类型的对象:"blob"、"tree"、 "commit" 和"tag"。
- “tree”有点像一个目录,它管理一些“tree”或是“blob”(就像文件和子目录)
- “blob”用来存储文件数据,通常是一个文件。
- 一个“commit”只指向一个"tree",它用来标记项目某一个特定时间点的状态。它包括一些关于时间点的元数据,如时间戳、最近一次提交的作者、指向上次提交(commits)的指针等等。
- 一个“tag”是来标记某一个提交(commit) 的方法。
ps:git的文件和文件名没关系。只要是内容相同,文件名不同,git默认是同一个文件。
可以根据上面的查看文件类型、大小、内容的命令,自行查看自己git项目中的这三个对象。
现在来看看HEAD和master的奥秘吧
因为在上一章的最后执行了git stash将工作区和暂存区的改动全部封存起来,所以执行下面的命令会看到工作区和暂存区中没有改动。
$ git status -s -b
## master
说明:上面在显示工作区状态时,除了使用了-s参数以显示精简输出外,还使用了-b参数以便能够同时显示出当前工作分支的名称。这个-b参数是在Git 1.7.2以后加入的新的参数。
git log -1 HEAD
...
git log -1 master
...
git log -1 refs/heads/master
...
也就是说在当前版本库中,HEAD、master和refs/heads/master具有相同的指向。现在到版本库(.git目录)中一探它们的究竟。
$ find .git -name HEAD -o -name master
.git/HEAD
.git/logs/HEAD
.git/logs/refs/heads/master
.git/refs/heads/master
找到了四个文件,其中在.git/logs目录下的文件稍后再予以关注,现在把目光锁定在.git/HEAD和.git/refs/heads/master上。
显示一下.git/HEAD的内容:
$ cat .git/HEAD
ref: refs/heads/master
把HEAD的内容翻译过来就是:“指向一个引用:refs/heads/master”。这个引用在哪里?当然是文件.git/refs/heads/master了。
看看文件.git/refs/heads/master的内容。
$ cat .git/refs/heads/master
e695606fc5e31b2ff9038a48a3d363f4c21a3d86
原来分支master指向的是一个提交ID(最新提交)。这样的分支实现是多么的巧妙啊:既然可以从任何提交开始建立一条历史跟踪链,那么用一个文件指向这个链条的最新提交,那么这个文件就可以用于追踪整个提交历史了。这个文件就是.git/refs/heads/master文件。
下面看一个更接近于真实的版本库结构图:
目录.git/refs是保存引用的命名空间,其中.git/refs/heads目录下的引用又称为分支。对于分支既可以使用正规的长格式的表示法,如refs/heads/master,也可以去掉前面的两级目录用master来表示。Git 有一个底层命令git rev-parse可以用于显示引用对应的提交ID。
$ git rev-parse master
e695606fc5e31b2ff9038a48a3d363f4c21a3d86
$ git rev-parse refs/heads/master
e695606fc5e31b2ff9038a48a3d363f4c21a3d86
$ git rev-parse HEAD
e695606fc5e31b2ff9038a48a3d363f4c21a3d86
可以看出它们都指向同一个对象。为什么这个对象是40位,而不是更少或者更多?这些ID是如何生成的呢?
SHA1哈希值到底是什么,如何生成的?
SHA1摘要算法可以处理从零到一千多万个TB的输入数据,输出为固定的160比特的数字摘要。两个不同内容的输入即使数据量非常大、差异非常小,两者的哈希值也会显著不同。比较著名的摘要算法有:MD5和SHA1。Linux下sha1sum命令可以用于生成摘要。
$ echo -n Git |sha1sum
5819778898df55e3a762f0c5728b457970d72cae -
即使有一天像发现了类似MD5摘要算法漏洞那样,发现了SHA1算法存在人为制造冲突的可能,那么Git可以使用更为安全的SHA-256或者SHA-512的摘要算法。
Git中的各种对象:提交(commit)、文件内容(blob)、目录树(tree)等(还有Tag)对象对应的SHA1哈希值是:
对象类型 + " " + 对象内容字符长度 + "\000" + 节点信息,然后执行SHA1哈希算法。
我执行
echo 'abc.txt' > abc.txt
之后的commit id为: 09a58d0eb74d3ef5fd7a6c3e434a9d27e6556166
- 看看HEAD对应的提交的内容。使用git cat-file命令
git cat-file commit HEAD
tree 075438b8ab5922bd576028a6320efe3236fc7710
parent 6b7b62590f636a546f3df6650ce84806a75267cf
author leeyi <liyuanbing@zsxq.com> 1569167757 +0800
committer leeyi <liyuanbing@zsxq.com> 1569167757 +0800
test
- 提交信息中总共包含207个字符。
git cat-file commit HEAD | wc -c
207
- 在提交信息的前面加上内容commit 207<null>(<null>为空字符),然后执行SHA1哈希算法。
( printf "commit 207\000"; git cat-file commit HEAD ) | sha1sum
09a58d0eb74d3ef5fd7a6c3e434a9d27e6556166 -
- 上面命令得到的哈希值和用git rev-parse看到的是一样的。
git rev-parse HEAD
09a58d0eb74d3ef5fd7a6c3e434a9d27e6556166
其他几种对象,包括在后面学习里程碑(Tag)的时候,会看到Tag对象(轻量级Tag除外)也是采用类似方法在对象库中存储的。
为什么不用顺序的数字来表示提交?
集中式版本控制系统因为只有一个集中式的版本库,可以很容易的实现依次递增的全局唯一的提交号,像Subversion就是如此。Git作为分布式版本控制系统,开发可以是非线性的,每个人可以通过克隆版本库的方式工作在不同的本地版本库当中,在本地做的提交可以通过版本库之间的交互(推送/push和拉回/pull操作)而互相分发,如果提交采用本地唯一的数字编号,在提交分发的时候不可避免的造成冲突。这就要求提交的编号不能仅仅是本地局部有效,而是要“全球唯一”。Git的提交通过SHA1哈希值作为提交ID,的确做到了“全球唯一”。
Mercurial(Hg)是另外一个著名的分布式版本控制系统,它的提交ID非常有趣:同时使用了顺序的数字编号和“全球唯一”的SHA1哈希值。但实际上顺序的数字编号只是本地有效,对于克隆版本库来说没有意义,只有SHA1哈希值才是通用的编号。
Git常用命令
一般来说,日常使用只要记住上图6个命令,就可以了。但是熟练使用,恐怕要记住60~100个命令。
配置相关命令
# 显示当前的Git配置 .git/config 文件的内容
git config --list
# 编辑Git配置文件
git config -e
git config -e --global
# 设置提交代码时的用户信息
git config [--global] user.name "[name]"
git config [--global] user.email "[email address]"
# 创建初始化项目
mkdir projectName && cd projectName
git init
增加/删除文件
# 添加指定文件到暂存区
$ git add [file1] [file2] ...
# 添加指定目录到暂存区,包括子目录
$ git add [dir]
# 添加当前目录的所有文件到暂存区
$ git add .
# 添加每个变化前,都会要求确认
# 对于同一个文件的多处变化,可以实现分次提交
$ git add -p
# 删除工作区文件,并且将这次删除放入暂存区
$ git rm [file1] [file2] ...
# 停止追踪指定文件,但该文件会保留在工作区
$ git rm --cached [file]
# 改名文件,并且将这个改名放入暂存区
$ git mv [file-original] [file-renamed]
代码提交
# 提交暂存区到仓库区
$ git commit -m [message]
# 提交暂存区的指定文件到仓库区
$ git commit [file1] [file2] ... -m [message]
# 提交工作区自上次commit之后的变化,直接到仓库区
$ git commit -a
# 提交时显示所有diff信息
$ git commit -v
# 使用一次新的commit,替代上一次提交
# 如果代码没有任何新变化,则用来改写上一次commit的提交信息
$ git commit --amend -m [message]
# 重做上一次commit,并包括指定文件的新变化
$ git commit --amend [file1] [file2] ...
分支管理
# 列出所有本地分支
$ git branch
# 列出所有远程分支
$ git branch -r
# 列出所有本地分支和远程分支
$ git branch -a
# 新建一个分支,但依然停留在当前分支
$ git branch [branch-name]
# 新建一个分支,并切换到该分支
$ git checkout -b [branch]
# 新建一个分支,指向指定commit
$ git branch [branch] [commit]
# 新建一个分支,与指定的远程分支建立追踪关系
$ git branch --track [branch] [remote-branch]
# 切换到指定分支,并更新工作区
$ git checkout [branch-name]
# 切换到上一个分支
$ git checkout -
# 建立追踪关系,在现有分支与指定的远程分支之间
$ git branch --set-upstream [branch] [remote-branch]
# 合并指定分支到当前分支
$ git merge [branch]
# 选择一个commit,合并进当前分支
$ git cherry-pick [commit]
# 删除分支
$ git branch -d [branch-name]
# 删除远程分支
$ git push origin --delete [branch-name]
$ git branch -dr [remote/branch]
# 批量删除分支
$ git branch |grep 'feature' |xargs git branch -D
还有其他 很多的命令,不一一列举了,如果遇到有用的功能,期望大家和我分享。