Git介绍及基本概念
- 分布式版本控制系统
Git是一个分布式版本控制系统,每个本地保留远程仓库的完整副本,在本地可以进行任何版本控制操作,比如修改,提交,创建分支,合并分支,回退等。需要时才推送到远程仓库。
- Git文件保存
Git对每次提交,有变化的文件都会整个文件存储起来,而不是像其它版本控制系统,比如cvs,svn,perforce等存储的是文件的差异(#直接记录快照,而非差异比较)部分。但是git有package机制,适当的时候会自动运行git gc命令(也可以手动运行)对文件进行差异存储。当然为了节约空间,也会进行压缩。这种方式在分支方面会带来很大的好处,Git的分支,其实本质上仅仅是指向提交对象的可变指针。
- Git对数据的引用
Git中所有数据,包括提交,目录,文件等,在存储前都计算校验和,然后以校验和来引用,这个校验和相当于指针。Git用以计算校验和的机制叫做 SHA-1 散列(hash,哈希),这是一个由 40 个十六进制字符(0-9 和 a-f)组成字符串。 SHA-1 哈希看起来是这样:24b9da6552252987aa493b52f8696cd6d3b00373。
- Git对象
Git有4种对象,blob对象(通常是文件),树(tree)对象(通常是目录),提交(commit)对象和标签(tag)对象。当使用 git commit 进行提交操作时,Git会先计算每一个文件的校验和,在Git仓库中保存为blob对象,然后计算每一个子目录的校验和,保存为树对象,随后,Git便会创建一个提交对象,它包含提交信息(比如提交者名字,邮件,提交时间等),指向这个树对象(项目根目录)的指针和父提交对象(第一次提交无父提交对象)。如此一来,Git 就可以追踪任何对象。标签对象实际上是一个加了标签信息的提交对象,它像是一个永不移动的分支引用——永远指向同一个提交对象,只不过给这个提交对象加上一个更友好的名字罢了。
Blob对象
一个blob通常用来存储文件的内容.
你可以使用git show命令来查看一个blob对象里的内容。
Tree对象
一个tree对象有一串(bunch)指向blob对象或是其它tree对象的指针,它一般用来表示内容之间的目录层次关系。
git show命令还可以用来查看tree对象,但是git ls-tree能让你看到更多的细节。
Commit对象
"commit对象"指向一个"tree对象", 并且带有相关的描述信息.
你可以用 --pretty=raw 参数来配合git show或 git log 去查看某个提交(commit):
Tag对象
你可以用 git cat-file命令来查看。
如果我们一个小项目, 有如下的目录结构:
$ tree
.
|-- README
|-- lib
|-- inc
|-- tricks.rb
|-- mylib.rb
2 directories, 3 files
如果我们把它提交(commit)到一个Git仓库中, 在Git中它们也许看起来就如下图:
你可以看到: 每个目录都创建了 tree对象 (包括根目录), 每个文件都创建了一个对应的 blob对象 . 最后有一个 commit对象来指向根tree对象(root of trees), 这样我们就可以追踪项目每一项提交内容.
5.Git目录
这个叫'.git'的目录在项目的根目录下,存储所有历史和元信息,包括所有的对象(commits,trees,blobs,tags)。如下:
|-- HEAD # 这个git项目当前处在哪个分支里
|-- config # 项目的配置信息,git config命令会改动它
|-- description # 项目的描述信息
|-- hooks/ # 系统默认钩子脚本目录
|-- index # 索引文件
|-- logs/ # 各个refs的历史信息
|-- objects/ # Git本地仓库的所有对象 (commits, trees, blobs, tags)
|-- refs/ # 标识你项目里的每个分支指向了哪个提交(commit)。
6.工作目录
Git的 '工作目录' 存储着你现在签出(checkout)来用来编辑的文件。当你在项目的不同分支间切换时, 工作目录里的文件经常会被替换和删除。所有历史信息都保存在 'Git目录'中,工作目录只用来临时保存签出(checkout) 文件的地方, 你可以编辑工作目录的文件直到下次提交(commit)为止。
7.Git索引
Git索引(index)是一个在你的工作目录和项目仓库间的暂存区(staging area). 有了它, 你可以把许多内容的修改一起提交(commit). 如果你创建了一个提交(commit), 那么提交的是当前索引(index)里的内容, 而不是工作目录中的内容.
Git基本应用
- Git 配置
使用Git的第一件事就是设置你的名字和email,这些就是你在提交commit时的签名。
$ git config --global user.name "Administator"
$git config --global user.email "[email protected]"
执行了上面的命令后,会在你的主目录(home directory)建立一个叫 ~/.gitconfig 的文件. 内容一般像下面这样:
[user]
name = Administator
email = [email protected]
如果你想使项目里的某个值与前面的全局设置有区别(例如把私人邮箱地址改为工作邮箱);你可以在项目中使用git config 命令不带 --global 选项来设置. 这会在你项目目录下的 .git/config 文件增加一节[user]内容(如上所示).
$ git config --list ---查看当前config。
- 建立仓库
$ git init
$ git clone [email protected]:root/training.git
- 提交到Git仓库
先修改或者创建文件file1,file2,然后添加file1,file2到暂存区(index):
$ git add file1 file2
也可以添加所有文件到暂存区:
$ git add .
如果不想提交file2,可以从暂存区移除它:
$ git rm --cached file2
(有了第一次提交之后,可以git reset file2 替代git rm --cached file2)
最后提交到git仓库:
$ git commit -m 'my first commit'
commit前可以用git status查看状态:
$ git status
- 回退提交
有2种方式:
$ git reset [--soft | --mixed | --hard]
--mixed
默认方式,会保留源码,只是将git commit和index信息回退到了某个提交(版本)(修改.git/HEAD的内容),如果还需要提交,先add再直接commit,或者commit -a
--soft
保留源码,只回退到commit 信息到某个提交(版本),不涉及index的回退,如果还需要提交,直接commit即可.
--hard
不保留源码, 源码和commit,index 都会回退到某个提交(版本)。这种方式比较危险,reset commit_id相当于把commit_id之后的提交删除,git log里都不会出现(当然git reflog里会有,所以也能undo reset)。
举例:
撤销到上一个版本:
$ git reset HEAD~1
撤销到任意一个版本:
$ git log ---查看提交,然后
$ git reset xxx ---恢复到版本xxx或者git reset HEAD~n ---恢复到倒数第n个提交。
当你push到远程仓库之后再reset,修改文件再commit,没问题,可是push的时候问题就来了,这个时候相当于有人在远程仓库里已经push了你上次的提交,必然会产生冲突,你就需要解决冲突才能push。
- 清除/废弃提交
$ git revert
git revert用一个新提交来清除/废弃某些或者某个历史提交(比如a123)所做的任何修改,历史提交a123未修改的文件不会受影响,但是如果a123之后修改了a123修改的文件,那么就需要合并了。执行revert命令时要求工作树必须是干净的。
git reset 跟git revert有时候容易搞混淆,其实这两个命令区别还是挺大的,前者回退/回滚到某个提交(版本),某个提交之后的提交都不要了;而后者是清除/废弃某些/某个提交,就是说某些/某个提交不要了,但是之后的提交还要的。
- [忽略文件]
在工作区根目录创建一个叫.gitignore的文件,把不需要git来管理的文件名放在里面,可以使用通配符格式。
格式规范如下:
所有空行或者以 # 开头的行都会被 Git 忽略。
可以使用标准的 glob 模式匹配。
匹配模式可以以(/)开头防止递归。
匹配模式可以以(/)结尾指定目录。
要忽略指定模式以外的文件或目录,可以在模式前加上惊叹号(!)取反。
所谓的 glob 模式是指 shell 所使用的简化了的正则表达式。 星号()匹配零个或多个任意字符;[abc] 匹配任何一个列在方括号中的字符(这个例子要么匹配一个 a,要么匹配一个 b,要么匹配一个 c);问号(?)只匹配一个任意字符;如果在方括号中使用短划线分隔两个字符,表示所有在这两个字符范围内的都可以匹配(比如 [0-9] 表示匹配所有 0 到 9 的数字)。 使用两个星号() 表示匹配任意中间目录,比如a/**/z 可以匹配 a/z, a/b/z 或 a/b/c/z等。
- 分支
7.1分支介绍
假设我们有三个文件:README test.rb LICENSE第一次提交到了仓库,现在,Git 仓库中有五个对象:三个 blob 对象(保存着文件快照)、一个树对象(记录着目录结构和 blob 对象索引)以及一个提交对象(包含着指向前述树对象的指针和所有提交信息):
我们做了2次修改后又提交了2次,这时候提交对象看起来就是下面这个样子:
分支是这样:
用命令
git checkout testing
切换分支到testing之后:
git commit -a -m 'made a change'
切换回master分支:
$ git checkout master
git commit -a -m 'made other changes'
7.2创建分支
从当前分支创建dev分支:
git branch
显示所有分支,包括远程分支:
git checkout dev
7.3合并分支
假设远程分支"origin"已经有了2个提交,如图
现在我们在这个分支做一些修改,然后生成两个提交(commit),但是与此同时,有些人也在"origin"分支上做了一些修改并且做了提交了. 这就意味着"origin"和"mywork"这两个分支各自"前进"了,它们之间"分叉"了。
正常合并:
git merge origin
$ git commit -a
变基(rebase)合并:
git rebase origin
$ git merge origin
解决合并中的冲突
如果你修改了某个文件,提交之前有人修改该个文件,就会有冲突,执行自动合就不会成功,git会在索引和工作树里设置一个特殊的状态, 提示你如何解决合并中出现的冲突。
有冲突(conflicts)的文件会保存在索引中,除非你解决了问题了并且更新了索引,否则执行 git commit都会失败:
$ git commit
file.txt: needs merge
如果执行 git status会显示这些文件没有合并(unmerged),这些有冲突的文件里面会添加像下面的冲突标识符:
<<<<<<< HEAD:file.txt
Hello world
=======
Goodbye
>>>>>>> 77976da35a11db4580b80ae27e8d65caf5208086:file.txt
你所需要的做是就是编辑解决冲突,(接着把冲突标识符删掉),再执行下面的命令:
$ git add file.txt
$ git commit 或者
$ git rebase --continue
如果有了冲突,想取消合并
$ git merge --abort 或者
$ git rebase --abort
如果dev分支不要了,可以删除:
$ git branch -d dev
删除远端分支:
$ git push origin --delete dev
查看已合并分支:
$ git branch --merged
查看未合并分支:
$ git branch --no-merged
- 打标签
打一个轻量标签:
$ git tag v1.0 [commit_id]
打一个附注标签(annotated):
$ git tag -a v2.0 -m "version 2.0" [commit_id]
显示标签:
$ git tag
推送标签到远程仓库:
$ git push origin v2.0
- 比较
比较工作目录(Working tree)和暂存区域快照(index)之间的差异,也就是修改之后还没有暂存起来的变化内容:
$ git diff
比较已经暂存起来但是还没提交的差异,也就是下一次commit时会提交到HEAD的内容:
$ git diff --cached
显示工作版本(Working tree)和HEAD(上次提交)的差异:
$ git diff HEAD
比较dev最新提交跟master最新提交之间的差异:
$ git diff master dev or git diff master..dev
比较dev从master分支分开以来的差异,就是dev与分开点的差异:
$ git diff master...dev
比较任意两个提交之间的差异
$ git diff fabca703f9682cbc6 2dabcf99b863c46b4f
比较dev最新提交跟master最新提交之间的关于文件test.txt的差异:
$ git diff master..dev test.txt
- 储藏
在工作区修改了文件,暂时不想提交,现在又想要切换分支,这时候就需要把修改储藏起来:
$ git stash
切换回分支过后,把储藏起来的文件取回工作区:
$ git stash pop
- 彻底删除文件
比如我现在的目录是这样:
做几个提交如下:
$ git add file1
$ git commit -m 'add file1'
$ git add test.tar
$ git commit -m 'add the big tar file test.tar'
$ git add file2
$ git commit -m 'add file2'
$ git rm --cached test.tar
$ git commit -m 'remove the big tar file test.tar'
$ git branch dev
$ git checkout dev
$ git add m.sh
$ git commit -m 'add m.sh'
我们看一下完整的git log以便和彻底删除文件之后的比较:
$ git log
commit 67d712fcd4c3e9b02bda773151def4421657a160
Author: Administrator [email protected]
Date: Wed Aug 21 15:24:20 2019 +0800
add m.sh
commit 52ec9d47f010d7132fe3cbcc5c53dda5aed129e0
Author: Administrator [email protected]
Date: Wed Aug 21 15:24:20 2019 +0800
remove the big tar file test.tar
commit e007b16860cd55470e5c57ebccf0e2e721d81530
Author: Administrator [email protected]
Date: Wed Aug 21 15:24:20 2019 +0800
add file2
commit a6f1b49c37c4c724e579088d28fce711f229c028
Author: Administrator [email protected]
Date: Wed Aug 21 15:24:20 2019 +0800
add the big tar file test.tar
commit 62c6d0887e87da7d5c17aa705d933adc0a9c0507
Author: Administrator [email protected]
Date: Wed Aug 21 15:24:09 2019 +0800
add file1
$ git checkout master
$ git log
commit 52ec9d47f010d7132fe3cbcc5c53dda5aed129e0
Author: Administrator [email protected]
Date: Wed Aug 21 15:24:20 2019 +0800
remove the big tar file test.tar
commit e007b16860cd55470e5c57ebccf0e2e721d81530
Author: Administrator [email protected]
Date: Wed Aug 21 15:24:20 2019 +0800
add file2
commit a6f1b49c37c4c724e579088d28fce711f229c028
Author: Administrator [email protected]
Date: Wed Aug 21 15:24:20 2019 +0800
add the big tar file test.tar
commit 62c6d0887e87da7d5c17aa705d933adc0a9c0507
Author: Administrator [email protected]
Date: Wed Aug 21 15:24:09 2019 +0800
add file1
我们看看仓库大小:
$ git count-objects -v
count: 11
size: 221428
in-pack: 0
packs: 0
size-pack: 0
prune-packable: 0
garbage: 0
size-garbage: 0
看到没,虽然我们删除了它,其实它还是在里面的,每次提交都要对它进行SHA-1计算,每次clone都会下载它,虽然我们根本不需要它。
接下来我们要彻底删除它:
先要找到添加这个大文件的提交:
$ git log --oneline --branches -- test.tar
52ec9d4 remove the big tar file test.tar
a6f1b49 add the big tar file test.tar
这里是a6f1b49,
然后,你必须重写 a6f1b49 提交及其之后的所有提交才能从Git历史中完全移除这个文件。为什么要重写a6f1b49 之后的所有提交呢?回顾一下前面讲的git对象,每次提交都要计算各个对象的SHA-1值,既然一个文件彻底移除了,那么包含这个文件的树对象及其父树对象,提交对象及其子提交对象都要重新计算。
$ git filter-branch --index-filter 'git rm --ignore-unmatch --cached test.tar' --prune-empty -- a6f1b49 HEAD --all
Rewrite a6f1b49c37c4c724e579088d28fce711f229c028 (2/5)rm 'test.tar'
Rewrite e007b16860cd55470e5c57ebccf0e2e721d81530 (3/5)rm 'test.tar'
Rewrite 67d712fcd4c3e9b02bda773151def4421657a160 (5/5)
Ref 'refs/heads/master' was rewritten
Ref 'refs/heads/dev' was rewritten
WARNING: Ref 'refs/heads/master' is unchanged
解释下,这条命令的意思是在index(cached)里删除test.tar,然后在branch里删除test.tar。这里要注意的是符号" -- "起分隔filter-branch选项和rev-list选项的作用,所以这里的--all是指rev-list的选项--all,就是refs/ 下面的所有refs,可以理解为所有分支。
看一下log:
$ git log
commit e92badb3ba49c072fdcd234d65f49d8dd9dc04da
Author: Administrator [email protected]
Date: Wed Aug 21 15:24:20 2019 +0800
add file2
commit 62c6d0887e87da7d5c17aa705d933adc0a9c0507
Author: Administrator [email protected]
Date: Wed Aug 21 15:24:09 2019 +0800
add file1
看到没,从添加这个大文件开始之后的提交全部被改写了。
看一下dev分支:
$ git checkout dev
$ git log
commit 41316c577c29473f73aea323bbc8e539cd500aa3
Author: Administrator [email protected]
Date: Wed Aug 21 15:24:20 2019 +0800
add m.sh
commit e92badb3ba49c072fdcd234d65f49d8dd9dc04da
Author: Administrator [email protected]
Date: Wed Aug 21 15:24:20 2019 +0800
add file2
commit 62c6d0887e87da7d5c17aa705d933adc0a9c0507
Author: Administrator [email protected]
Date: Wed Aug 21 15:24:09 2019 +0800
add file1
让我们看看节省了多少空间:
$ git count-objects -v
count: 16
size: 221448
in-pack: 0
packs: 0
size-pack: 0
prune-packable: 0
garbage: 0
size-garbage: 0
啊?!没有节省空间嘛……
因为还有引用指向这个大文件,你需要手动删除以前的引用:
$ rm -Rf .git/refs/original
更新本地log:
$ git reflog expire --expire=now --all
如果有远程引用,删除它们,比如:
$ git remote rm origin
再做一次垃圾回收:
$git gc --prune=now
这下大文件就彻底永久的删除了:
$ git count-objects -v
count: 0
size: 0
in-pack: 9
packs: 1
size-pack: 5
prune-packable: 0
garbage: 0
size-garbage: 0
如果你不知道某个大文件在哪里,可以通过下面的命令查找大文件的SHA-1值(第一列)和文件大小(第三列):
$ git gc
$ git verify-pack -v .git/objects/pack/pack-0b512708d645a6b5f15891de74b68fac1f6abd1b.idx | sort -k 3 -n
chain length = 1: 2 objects
.git/objects/pack/pack-0b512708d645a6b5f15891de74b68fac1f6abd1b.pack: ok
b18797efe0949a25dff3fec9b58760f3cf86a793 tree 4 14 719 1 71cdad7926263a6b82c8dcc189ab086f6e50dac1
26a46ea3ec23a452abd49d6461a0d97f26c3f3e7 blob 7 18 4364 1 f3362d9be6f76acff3a8de6fdf04b7b6a6cd8b6b
non delta: 9 objects
33ba55b0966d37bd4b396f640acdf28c7631b1c0 tree 33 44 226571881
57c286b455dc3cf998c36b18c7175b6734b19d5a tree 69 77 4382
71cdad7926263a6b82c8dcc189ab086f6e50dac1 tree 102 103 616
fa6d0e110b78152883a3d6dd28eb4f94ca32389c commit 176 122 494
7a333477d059486728ac43c41f9b5a899c6a3f26 commit 224 151 343
7e8910d778a62f8a315476840e289ef5e90b7dc8 commit 244 164 179
acbe66fd2cf05b050fe39e4a999e38fffd078dec commit 247 167 12
f3362d9be6f76acff3a8de6fdf04b7b6a6cd8b6b blob 7606 3631 733
f2595ad951ee37ffc07c6df054d93fe5b09a3c28 blob 230195200 226567422 4459
然后:
$ git rev-list --objects --all | grep f2595ad951e
f2595ad951ee37ffc07c6df054d93fe5b09a3c28 test.tar
文件这个操作对提交历史的修改是破坏性的,一般由管理员完成。管理员先通知所有开发人员push他们的提交,之后让他们不要再做任何操作。自己做好备份,然后才执行这个操作,最后$ git push --all origin
- 远程仓库
查看远程仓库配置:
$ git remote -v
添加远程仓库链接tr:
$ git remote add tr [email protected]:root/training.git
删除远程仓库链接tr:
$ git remote rm tr
查看远程仓库链接tr:
$ git remote show tr
重命名tr为training:
$ git remote rename tr training
从远程仓库tr拉取分支dev并合并到当前分支
$ git pull tr dev
git fetch 跟git pull区别:
git pull用remote仓库更新本地仓库(merge),并更新本地分支指针比如.git/refs/heads/master和本地保存的remote分支指针比如.git/refs/remotes/origin/master;
git fetch只更新.git/FETCH_HEAD和本地保存的remote分支指针比如.git/refs/remotes/origin/master,并不跟新本地仓库。
所以,git pull=git fetch + git merge FETCH_HEAD
Git 权限控制原理
Git虽说很强大,但是Git也有一个很“严重”的问题,那就是没有权限控制功能。Git的创建是作为开源社区的代码版本管理工具而存在的,但当我们把Git引入到团队内部的开发流程中后就会发现没有权限控制的Git无法保护代码的安全。幸好还有SSH。由于Git的主流连接方式是SSH连接,因此当我们试图控制一个Git中心库权限的时候,可以通过控制SSH登录用户的权限来间接的达到目的。那如何控制SSH的用户权限呢?既然我们SSH到中心库的时候用的可能都是git用户(Linux的用户),又如何区分到底真是用户是谁呢?这里就不得不说SSH的公私钥模式,当我们将某一个用户生成的公钥添加到git用户的 ~/.ssh/authorized_keys 文件后,以git用户身份SSH连接到Git 中心库时可以免去输入密码的步骤。而Git权限控制的关键环节就在这个authorized_keys文件中。
一般典型的authorized_keys文件每一行都是一个公钥串,简单直接,但其实这个文件可以支持更丰富的SSH连接模式,请看下方的截图:
很显然,这里面除了公钥之外还包括几个其他的配置:
command:以该公钥对应的私钥登陆后执行的命令
no-xxxxx:表示不支持该模式的连接
最后才是对应的公钥。
这样配置的话,如果用户试图使用SSH默认连接方式会得到如下的返回结果:
这样可以限制用户不可以毫无顾忌的连接到Git中心库进而绕过SSH的权限控制系统。而每次Git的push/pull/clone等操作其实都是一次SSH连接从而激活command命令而执行脚本。那么剩下的就好解释了,脚本接受参数,然后去做用户权限的判断,如果用户具有权限那么直接执行SSH连接附带的命令请求,如果没有权限直接exit。