目录
1. 概述
本文主要参考了pro git,对git相关知识进行整理,主要包含git基本原理,git分支相关知识以及git常用命令。
2.几种版本控制系统
本地版本控制系统
集中版本控制
分布式版本控制
3. git初识
git诞生
2005 年,开发 BitKeeper 的商业公司同 Linux 内核开源社区的合作关系结束,他们收回了免费使用 BitKeeper 的权力。
速度
简单的设计
对非线性开发模式的强力支持(允许上千个并行开发的分支)
完全分布式
有能力高效管理类似 Linux 内核一样的超大规模项目(速度和数据量)
git的特点
-
直接记录快照,而非差异比较
-
近乎所有操作都是本地执行
-
时刻保持数据完整性
Git 使用 SHA-1 算法计算数据的校验和,通过对文件的内容或目录的结构计算出一个 SHA-1 哈希值,作为指纹字符串。该字串由 40 个十六进制字符(0-9 及 a-f)组成 -
多数操作仅添加数据
git文件的三种状态
Git 对象
Git 从核心上来是简单地存储键值对(key-value)。它允许插入任意类型的内容,并会返回一个键值,通过该键值可以在任何时候再取出该内容。
在 GIT 中最基本的四种对象为 blob,tree,commit,tag
- blob
即文件,注意只包含内容,没有名字,权限等属性(例外的是包含大小) - tree
tree 对象可以存储文件名,同时也允许存储一组文件。一个单独的 tree 对象包含一条或多条 tree 记录,每一条记录含有一个指向 blob 或子 tree 对象的 SHA-1 指针,并附有该对象的权限模式 (mode)、类型和文件名信息 - commit
表示修改历史,每一个commit 对应有一个 tree 的修改结果,都指向了你创建的tree对象快照。commit 包含 作者、提交者、parent commit、tree、log、修改时间、 提交时间, 注意 git 明确区分了 author 和 committer 这两个角色。 - tag
标签,它可以指向 blob, tree, commit 并包含签名,最常见的是指向 commit 的 GPG 签名的标签。
创建Git 对象(实例)
- 创建blob
$ echo 'content: testA version 1' > testA
$ git hash-object -w testA
0e01f0f68a603ef2759334f421bccf3a3db57624
$ git cat-file -t 0e01f0f68a603ef2759334f421bccf3a3db57624
blob
$ echo 'content: testB version 1' > testB
$ git hash-object -w testB
aa8d847c12667ad119731f81fc52b9cc4a1805d7
$ git cat-file -t aa8d847c12667ad119731f81fc52b9cc4a1805d7
blob
$ find .git/objects -type f
.git/objects/aa/8d847c12667ad119731f81fc52b9cc4a1805d7
.git/objects/0e/01f0f68a603ef2759334f421bccf3a3db57624
- 创建tree
/* 为testA version 1创建tree */
//update-index: 为testA文件的version1创建一个index
//–add: 将文件testA的version1添加到暂存区域
//–cacheinfo: 要添加的文件并不在当前目录下而是在数据库中
$ git update-index --add --cacheinfo 100644 \
0e01f0f68a603ef2759334f421bccf3a3db57624 testA
$ git write-tree
4f5947b98973bdc490421e68ac6aab08d411b24f
// -p 参数让该命令输出数据内容的类型
$ git cat-file -p 4f5947b98973bdc490421e68ac6aab08d411b24f
100644 blob 0e01f0f68a603ef2759334f421bccf3a3db57624 testA
$ git cat-file -t 4f5947b98973bdc490421e68ac6aab08d411b24f
tree
/* 为testB version1创建tree*/
$ git update-index --add --cacheinfo 100644 \
aa8d847c12667ad119731f81fc52b9cc4a1805d7 testB
$ git write-tree
47f247ba17e0bcea72521ef034794b462355aa88
$ git cat-file -p 47f247ba17e0bcea72521ef034794b462355aa88
100644 blob 0e01f0f68a603ef2759334f421bccf3a3db57624 testA
100644 blob aa8d847c12667ad119731f81fc52b9cc4a1805d7 testB
$ git cat-file -t 47f247ba17e0bcea72521ef034794b462355aa88
tree
/* 为如上tree创建parent tree*/
$ git read-tree --prefix=parent 47f247ba17e0bcea72521ef034794b462355aa88
$ git write-tree
6c0ad39e4aad564090c1cdb1428caceca8a13cec
$ git cat-file -p 6c0ad39e4aad564090c1cdb1428caceca8a13cec
040000 tree 47f247ba17e0bcea72521ef034794b462355aa88 parent
100644 blob 0e01f0f68a603ef2759334f421bccf3a3db57624 testA
100644 blob aa8d847c12667ad119731f81fc52b9cc4a1805d7 testB
$ git cat-file -t 6c0ad39e4aad564090c1cdb1428caceca8a13cec
tree
- 创建commit对象
/* 第一次commit */
$ echo 'first commit' | git commit-tree 4f5947
838445fcbe65c63ac94b87f8830fb1ac0043a829
$ git cat-file -p 838445f
tree 4f5947b98973bdc490421e68ac6aab08d411b24f
author HZero <jasonactions@foxmail.com> 1637737807 +0800
committer HZero <jasonactions@foxmail.com> 1637737807 +0800
first commit
$ git log --stat 838445f
commit 838445fcbe65c63ac94b87f8830fb1ac0043a829
Author: HZero <jasonactions@foxmail.com>
Date: Wed Nov 24 15:10:07 2021 +0800
first commit
testA | 1 +
1 file changed, 1 insertion(+)
$ echo 'second commit' | git commit-tree 47f247 -p 838445f
c61bca1f5e119306889966670e223dbe8a9c22db
$ git cat-file -p c61bca1f
tree 47f247ba17e0bcea72521ef034794b462355aa88
parent 838445fcbe65c63ac94b87f8830fb1ac0043a829
author HZero <jasonactions@foxmail.com> 1637738012 +0800
committer HZero <jasonactions@foxmail.com> 1637738012 +0800
second commit
$ git log --stat c61bca1f
commit c61bca1f5e119306889966670e223dbe8a9c22db
Author: HZero <jasonactions@foxmail.com>
Date: Wed Nov 24 15:13:32 2021 +0800
second commit
testB | 1 +
1 file changed, 1 insertion(+)
commit 838445fcbe65c63ac94b87f8830fb1ac0043a829
Author: HZero <jasonactions@foxmail.com>
Date: Wed Nov 24 15:10:07 2021 +0800
first commit
testA | 1 +
1 file changed, 1 insertion(+)
$ echo 'content: testA version 2' > testA
$ git hash-object -w testA
b8a3f23f81a14596bdb68f386f5938ae53ce47a8
$ git update-index --add --cacheinfo 100644 b8a3f23f81a14596bdb68f386f5938ae53ce47a8 testA
$ git write-tree
8273ad94db82a7c8919d5ee2827a81bafc9e75fe
$ echo 'third commit' | git commit-tree 8273ad94 -p c61bca1f5
3a2a004c2e08228093f3f28c81506d3677dee768
$ git cat-file -p 3a2a004c2
tree 8273ad94db82a7c8919d5ee2827a81bafc9e75fe
author HZero <jasonactions@foxmail.com> 1637739915 +0800
committer HZero <jasonactions@foxmail.com> 1637739915 +0800
third commit
$ find .git/objects -type f
//top tree(inc parent tree && blob testA && blob testB)
.git/objects/6c/0ad39e4aad564090c1cdb1428caceca8a13cec
//testB blob
.git/objects/aa/8d847c12667ad119731f81fc52b9cc4a1805d7
//testA blob
.git/objects/0e/01f0f68a603ef2759334f421bccf3a3db57624
//tree(include blob testA version1)
.git/objects/4f/5947b98973bdc490421e68ac6aab08d411b24f
//tree(include blob testA version2)
.git/objects/b8/a3f23f81a14596bdb68f386f5938ae53ce47a8
//(include blob testA version 1&& blob testB)
.git/objects/47/f247ba17e0bcea72521ef034794b462355aa88
//(include blob testA version 2&& blob testB)
.git/objects/82/73ad94db82a7c8919d5ee2827a81bafc9e75fe
//first commit
.git/objects/83/8445fcbe65c63ac94b87f8830fb1ac0043a829
//second commit
.git/objects/c6/1bca1f5e119306889966670e223dbe8a9c22db
//third commit
.git/objects/40/8b07a1f4c606a54a41b4ad006c16ddc75f8d3a
- 每一个tree对象会指向一个 SHA-1 指针,一个单独的 tree 对象包含一条或多条 tree 记录,每一条记录含有一个指向 blob 或子 tree 对象的 SHA-1 指针;
- 每一个commit 对应有一个 tree 的修改结果,都指向了你创建的tree对象快照
- 每一个commit对象指向前一个commit对象
4. git配置
配置文件
/etc/gitconfig 文件 git config --system
~/.gitconfig 文件 git config --global
.git/config文件覆盖/etc/gitconfig 中的同名变量
常用配置命令
$ git config --global user.name “John Doe”
$ git config --global user.email johndoe@example.com
$ git config --global core.editor emacs
$ git config --global merge.tool vimdiff
$ git config --list
5. 常用命令
获取仓库
$ git init
$ git clone git://github.com/schacon/grit.git
跟踪文件状态
$ git status
$ git add benchmarks.rb
忽略文件(.gitignore)
# 此为注释 – 将被 Git 忽略
*.a # 忽略所有 .a 结尾的文件
!lib.a # 但 lib.a 除外
/TODO # 仅仅忽略项目根目录下的 TODO 文件,不包括 subdir/TODO
build/ # 忽略 build/ 目录下的所有文件
doc/*.txt # 会忽略 doc/notes.txt 但不包括 doc/server/arch.txt
查看差异
$ git diff
提交更新
$ git commit -m “Story 182: Fix benchmarks for speed”
跳过使用暂存区域
$ git commit -a -m ‘added new benchmarks’
删除文件
$ rm grit.gemspec
$ git rm grit.gemspec
从暂存区域删除文件
$ git rm --cached readme.txt
文件改名
$ git mv file_from file_to
查看提交
$ git log -p -2
$ git log --stat
$ git log --pretty=oneline
$ git log --pretty=format:"%h - %an, %ar : %s"
$ git log --since=2.weeks
$ git last
$ git log --pretty="%h - %s" --author=gitster --since=“2008-10-01”
–before=“2008-11-01” --no-merges – t/
撤消操作
$ git commit --amend
远程仓库
$ git remote -v
6. git分支
什么是git分支?
在Git 中使用“git commit”提交时,会保存一个commit对象,它包含一个指向暂存内容快照的tree指针,以及一定数量(也可能没有)指向该commit对象直接祖先的指针。
作些修改后再次提交,那么这次的提交对象会包含一个指向上次提交对象的指针(即commit指针)。如上是两次提交后,仓库历史的样子
Git 中的分支,其实本质上仅仅是个指向 commit 对象的可变指针,Git 会使用 master 作为分支的默认名字。在若干次提交后,你其实已经有了一个指向最后一次提交对象的 master 分支,它在每次提交的时候都会自动向前移动。分支其实就是从某个commit对象往回看的历史
$ git branch testing
这会在当前 commit 对象上新建一个分支指针
HEAD是一个指向你正在工作中的本地分支的指针(译注:将 HEAD 想象为当前分支的别名。)。运行 git branch 命令,仅仅是建立了一个新的分支,但不会自动切换到这个分支中去,所以在这个例子中,我们依然还在 master 分支里工作
$ git checkout testing
$ vim test.rb
$ git commit -a -m 'made a change'
每次提交后 HEAD 随着分支一起向前移动
$ git checkout master
HEAD 在一次 checkout 之后移动到了另一个分支,这条命令做了两件事。它把 HEAD 指针移回到 master 分支,并把工作目录中的文件换成了 master 分支所指向的快照内容
$ vim test.rb
$ git commit -a -m 'made other changes'
分支的新建与合并
初始状态
$ git checkout -b iss53
Switched to a new branch "iss53"
创建并切换到新的分支
$ vim index.html
$ git commit -a -m 'added a new footer [issue 53]'
$ git checkout master
Switched to branch "master"
$ git checkout -b 'hotfix'
Switched to a new branch "hotfix"
$ vim index.html
$ git commit -a -m 'fixed the broken email address'
[hotfix]: created 3a0874c: "fixed the broken email address"
1 files changed, 0 insertions(+), 1 deletions(-)
hotfix 分支是从 master 分支所在点分化出来的
$ git checkout master
$ git merge hotfix
Updating f42c576..3a0874c
Fast forward
README | 1 -
1 files changed, 0 insertions(+), 1 deletions(-)
$ git branch -d hotfix
Deleted branch hotfix (3a0874c).
$ git checkout iss53
Switched to branch "iss53"
$ vim index.html
$ git commit -a -m 'finished the new footer [issue 53]'
[iss53]: created ad82d7a: "finished the new footer [issue 53]"
1 files changed, 1 insertions(+), 0 deletions(-)
$ git checkout master
$ git merge iss53
Merge made by recursive.
README | 1 +
1 files changed, 1 insertions(+), 0 deletions(-)
Git 自动创建了一个包含了合并结果的提交对象C6
利用分支进行开发的工作流程
master分支用于稳定代码的发布,develop分支用于开发新功能,待测试的非稳定代码
想象成工作流水线
分支的管理
初始的多分支结构
合并了 dumbidea 和 iss91v2 后的分支历史
远程分支
假设地址为 git.ourcompany.com 的 Git 服务器。执行git clone后会有如下的动作:
- Git 会自动为你将此远程仓库命名为 origin,并下载其中所有的数据
- 建立一个指向它的 master 分支的指针,在本地命名为 origin/master
- Git 建立一个属于你自己的本地 master 分支,始于 origin 上 master 分支相同的位置
在本地工作的同时有人向远程仓库推送内容会让提交历史开始分流但是未push到服务器
可以运行 git fetch origin 来同步远程服务器上的数据到本地。该命令首先找到 origin 是哪个服务器(本例为 git.ourcompany.com),从上面获取你尚未拥有的数据,更新你本地的数据库,然后把 origin/master 的指针移到它最新的位置上
把另一个服务器加为远程仓库
git fetch teamone 来获取小组服务器上你还没有的数据。由于当前该服务器上的内容是你 origin 服务器上的子集,Git 不会下载任何数据,而只是简单地创建一个名为 teamone/master 的远程分支,指向 teamone 服务器上 master 分支所在的提交对象 31b8e。你在本地有了一个指向 teamone 服务器上 master 分支的索引
推送本地分支
如果你有个叫 serverfix 的分支需要和他人一起开发,可以运行 git push (远程仓库名) (分支名):
- 方法1
$ git push origin serverfix
Counting objects: 20, done.
Compressing objects: 100% (14/14), done.
Writing objects: 100% (15/15), 1.74 KiB, done.
Total 15 (delta 5), reused 0 (delta 0)
To git@github.com:schacon/simplegit.git
* [new branch] serverfix -> serverfix
Git 自动把 serverfix 分支名扩展为 refs/heads/serverfix:refs/heads/serverfix,意为“取出我在本地的 serverfix 分支,推送到远程仓库的 serverfix 分支中去”。
2. 方法2
git push origin serverfix:serverfix
上传我本地的 serverfix 分支到远程仓库中去,仍旧称它为 serverfix 分支”3.
- 方法3
git push origin serverfix:awesomebranch
想把远程分支叫作 awesomebranch
$ git fetch origin
remote: Counting objects: 20, done.
remote: Compressing objects: 100% (14/14), done.
remote: Total 15 (delta 5), reused 0 (delta 0)
Unpacking objects: 100% (15/15), done.
From git@github.com:schacon/simplegit
* [new branch] serverfix -> origin/serverfix
其他人重新git fetch origin时就可以看到新push的分支
git merge origin/serverfix
该远程分支的内容合并到当前分支
$ git checkout -b serverfix origin/serverfix
Branch serverfix set up to track remote branch refs/remotes/origin/serverfix.
Switched to a new branch "serverfix"
如果想要一份自己的 serverfix 来开发,可以在远程分支的基础上分化出一个新的分支来.这会切换到新建的 serverfix 本地分支,其内容同远程分支 origin/serverfix 一致
跟踪远程分支
$ git checkout --track origin/serverfix
Branch serverfix set up to track remote branch refs/remotes/origin/serverfix.
Switched to a new branch "serverfix"
本地会自动创建serverfix分支跟踪远程origin/serverfix分支
$ git checkout -b sf origin/serverfix
Branch sf set up to track remote branch refs/remotes/origin/serverfix.
Switched to a new branch "sf"
为本地跟踪分支起一个不同的名字
删除远程分支
$ git push origin :serverfix
To git@github.com:schacon/simplegit.git
- [deleted] serverfix
在服务器上删除 serverfix 分支
分支rebase
把一个分支中的修改整合到另一个分支的办法有两种::merge 和 rebase.
最初分叉的提交历史
最容易的整合分支的方法是 merge 命令,它会把两个分支最新的快照(C3 和 C4)以及二者最新的共同祖先(C2)进行三方合并,合并的结果是产生一个新的提交对象(C5)
$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command
它的原理是回到两个分支最近的共同祖先,根据当前分支(也就是要进行衍合的分支 experiment)后续的历次提交对象(这里只有一个 C3),生成一系列文件补丁,然后以基底分支(也就是主干分支 master)最后一个提交对象(C4)为新的出发点,逐个应用之前准备好的补丁文件,最后会生成一个新的合并提交对象(C3’),从而改写 experiment 的提交历史,使它成为 master 分支的直接下游.把 C3 里产生的改变到 C4 上重演一遍
回到 master 分支,进行一次快进合并
合并结果中最后一次提交所指向的快照,无论是通过衍合,还是三方合并,都会得到相同的快照内容,只不过提交历史不同罢了。衍合是按照每行的修改次序重演一遍修改,而合并是把最终结果合在一起。
分支rebase举例
从一个特性分支里再分出一个特性分支的历史。
$ git rebase --onto master server client
取出 client 分支,找出 client 分支和 server 分支的共同祖先之后的变化,然后把它们在 master 上重演一遍”。
$ git checkout master
$ git merge client
$ git rebase master server
现在我们决定把 server 分支的变化也包含进来。我们可以直接把 server 分支衍合到 master,而不用手工切换到 server 分支后再执行衍合操作 — git rebase [主分支] [特性分支] 命令会先取出特性分支 server,然后在主分支 master 上重演:
$ git checkout master
$ git merge server
$ git branch -d client
$ git branch -d server