[TOC]
背景
以前协同修改文件的方法:
通过复制文件来备份不同版本,按照日期等命名规则来区分。
文件共享,大家都能编辑,容易被别人的修改覆盖,所以需要文件名加上编辑者的名字。
Git解决的问题
- 以前方式比较麻烦、容易出错。
Git
是一个分布式版本管理系统,是为了更好地管理Linux内核
开发而创立的。
Git
可以在任何时间点,把文档的状态作为更新记录保存起来。因此可以把编辑过的文档复原到以前的状态,也可以显示编辑前后的内容差异。
而且,编辑旧文件后,试图覆盖较新的文件的时候(即上传文件到服务器时),系统会发出警告,因此可以避免在无意中覆盖了他人的编辑内容。
管理历史记录的数据库(Repository)
数据库是记录文件或目录状态
的地方,存储着内容修改的历史记录
。在数据库的管理下,把文件和目录修改的历史记录放在对应的目录下。
本地数据库
为了方便用户个人使用,在自己的机器上配置的数据库。
远程数据库(共享数据库)
配有专用的服务器,为了多人共享而建立的数据库。
如果想要公开在本地数据库中修改的内容,把内容上传到远程数据库就可以了。另外,通过远程数据库还可以取得其他人修改的内容。
创建数据库的方法
有两种方法:
- 创建全新的数据库。
- 复制远程数据库。
修改记录的提交
若要把文件或目录的添加和变更
保存到数据库,就需要进行提交。
执行提交后,数据库中会生成上次提交的状态与当前状态的差异记录
(也被称为revision
)。
系统会根据修改的内容和目录结构
使用SHA1
哈希函数计算出没有重复的40位16进制的英文及数字
来给提交命名。指定这个命名,就可以在数据库中找到对应的提交。
工作树和索引
在Git管理下,我们实际操作的目录就是工作树
。
在数据库和工作树之间有索引
,索引是为了向数据库提交作准备的区域。
https://ws1.sinaimg.cn/large/006tKfTcgy1ftn4p78kw4j30fv07a0tg.jpg
基础配置
三个配置文件
Git
工作环境变量三个存放位置:
-
/etc/gitconfig
:对系统所有用户都普遍适用的配置。 -
~/.gitconfig
:只适用当前用户的配置。 -
your-project/.git/config
:只适用当前项目的配置。
重要配置1:用户信息
$ git config --global user.name "dszkng"
$ git config --global user.email dszkng@outlook.com
这两项代表着是哪个用户提交的。
使用--global
选项时说明配置的是当前用户主目录下这个配置文件。
重要配置2:文本编辑器
Git
需要你输入一些额外消息的时候,会自动调用一个外部文本编辑器给你用。默认会使用操作系统指定的默认编辑器,一般可能会是Vi
或者Vim
。如果你有其他偏好,可以重新设置:
$ git config --global core.editor emacs
重要配置3:差异分析工具
在解决合并冲突时使用哪种差异分析工具?
比如要改用vimdiff
的话:
$ git config --global merge.tool vimdiff
Git
可以理解kdiff3
,tkdiff
,meld
,xxdiff
,emerge
,vimdiff
,gvimdiff
,ecmerge
,和 opendiff
等合并工具的输出信息。当然,你也可以指定使用自己开发的工具。
查看配置信息
$ git config --list
credential.helper=osxkeychain
user.name=dszkng
user.email=dszkng@outlook.com
core.repositoryformatversion=0
core.filemode=true
core.bare=false
core.logallrefupdates=true
core.ignorecase=true
core.precomposeunicode=true
remote.origin.url=ssh://gogs@git.genecard.cn:2222/zhichun/zc-cms.git
remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*
branch.master.remote=origin
branch.master.merge=refs/heads/master
$ git config user.email
dszkng@outlook.com
有时候会看到重复的变量名,那就说明它们来自不同的配置文件,不过最终Git
实际采用的是最后一个。
基本使用
Git思想及基本工作原理
只有掌握了基本的原理,用起来才会知其所以然,游刃有余。
分支(branch)操作
使用场景
在开发软件时,可能有多人同时为同一个软件(并行)开发功能
或修复Bug
,可能存在多个Release
版本,并且需要对各个版本进行维护。
所幸,Git的分支功能可以支持同时进行多个功能的开发和版本管理。
什么是分支?
分支是为了将修改记录的整体流程分叉保存
。分叉后的分支不受其他分支的影响,所以在同一个数据库里可以同时进行多个修改。
为了不受其他开发人员的影响,一般是在主分支上建立自己专用的分支。完成工作后,将自己分支上的修改合并到主分支。因为每一次提交的历史记录都会被保存,所以当发生问题时,定位和修改造成问题的提交就容易多了。
创建分支
$ git branch branch_name
- 切换并创建分支
$ git checkout -b branch_name
- 查看创建过的分支
$ git branch
切换分支
$ git checkout branch_name
在切换工作分支时需要进行checkout
操作。Git
会从工作树
还原向目标分支提交的修改内容。checkout
之后的提交记录将被追加
到目标分支。
- HEAD
HEAD
指向的是现在使用中的分支的最后一次更新
。通常默认指向master
分支的最后一次更新。通过移动HEAD
,就可以变更使用的分支。
ps
:提交时使用~(tilde)
和^(caret)
就可以指定某个提交的相对位置
。
比如对于:HEAD3 -> HEAD2 -> HEAD1 -> HEAD(master)。
HEAD1
用HEAD~1
或HEAD~^
表示。
HEAD2
用HEAD~2
或HEAD~1^1
表示。
HEAD3
用HEAD~3
或HEAD~1^2
或HEAD~2^1
表示。
- stash(暂存区)
stash
是临时保存文件修改内容的区域。stash
可以暂时保存工作树
和索引里还没提交的修改内容
,等事后再取出暂存的修改,应用到原先的分支或其他的分支上。
ps
:如果在还未提交的修改内容
以及新添加的文件
,留在索引区域
或工作树
的情况下切换到其他的分支时,修改内容会从原来的分支移动
到目标分支。
但是如果在checkout
的目标分支中相同的文件
也有修改,checkout
会失败的。这时要么先提交修改内容
,要么用stash暂时保存修改内容
后再checkout
。
合并分支
$ git merge branch_name
将完成作业的Topic
分支合并到Merge
分支有两种方法,合并后分支的历史记录
会有很大的差别。
比如bugfix
分支是从master
分支分叉出来的,bugfix
合并到master
时:
- merge
$ git merge branch_name
- 如果
master
分支状态没有被更改过,bugfix
分支的历史记录包含master
分支所有的历史记录,这时候合并是最简单的,这就是快进(fast-forward)合并
。
- 如果
master
分支在分叉之后又有了更新,这种情况下,要把master
分支的修改内容和bugfix
分支的修改内容汇合
起来。因此,合并
两个修改会生成一个提交。这时,master
分支的HEAD
会移动到该提交上。
ps
:执行合并时,如果设定了non fast-forward
选项,即使在能够fast-forward
合并的情况下也会生成新的提交并合并
。
- rebase
$ git init
$ vim a.txt # 新增a.txt,内容:create file
$ git commit -a -m 'first commit'
$ git branch issue # 新建issue分支
$ vim a.txt # 修改文件制造冲突条件,增加内容:first edit
$ git commit -a -m 'update a.txt in master branch'
$ git checkout issue # 切换分支
$ vim a.txt # 修改文件,增加内容:second edit
$ git commit -a -m 'update a.txt in issue branch'
$ git rebase master # 合并master和issue,但是会冲突
$ vim a.txt # 修改冲突内容
$ git add . # 不需要commit
$ git rebase --continue # 继续修改冲突后的提交
$ git rebase --abort # 如果要取消rebase的话
$ git checkout master
$ git merge issue # 这时候执行合并操作,实际进行的是fast-forward合并
bugfix
分支的历史记录
会添加在master
分支的后面。历史记录
成一条线,相当整洁。
rebase
操作可能会有冲突,需要修改各自产生冲突的部分。rebase
之后,master
的HEAD
位置不变。因此,要合并master
分支和bugfix
分支,即是将master
的HEAD
移动到bugfix
的HEAD
这里。
ps
:merge
和rebase
都是合并历史记录
,但是各自的特征不同。
- merge
保持修改内容的历史记录,但是历史记录
会很复杂。 - rebase
历史记录简单,是在原有提交的基础上将差异内容反映进去
。
因此,可能导致原本的提交内容无法正常运行。
删除分支
$ git branch -d branch_name
分支实践
两种分支
分支可以被任意创建,但是,要先确定运用规则
才可以有效地利用分支。
-
Merge
分支
Merge
分支是为了可以随时发布release
而创建的分支,它还能作为Topic
分支的源分支
使用。保持分支稳定
的状态是很重要的。如果要进行更改,通常先创建Topic
分支,而针对该分支,可以使用Jenkins之类的CI工具进行自动化编译以及测试。
通常,大家会将master
分支当作Merge
分支使用。
ps
:在数据库初次提交后,Git
会默认创建一个master分支,不修改分支情况下,每次修改提交都是在master
分支上。
-
Topic
分支
Topic
分支是为了开发新功能
或修复Bug
等任务而建立的分支。若要同时进行多个的任务,请创建多个的Topic
分支。
Topic
分支是从稳定的Merge
分支创建的。完成作业后,要把Topic
分支合并回Merge
分支。
常用分支
- 主分支
- master:只负责
管理发布的状态
,在提交时使用标签
记录发布版本号
。
- master:只负责
- develop:日常开发分支。
- release分支
- release-2018.9:从
develop
分叉出,为release
作准备的分支,做最后的调整,再合并到develop
分支。
- release-2018.9:从
- release-2017.8
- 特性分支(topic分支)
- feature:日常新功能开发。
- bugfix:从
release
分支分叉出,解决完bug
问题,最后合并到release
分支。
- bugfix:从
- hotfix分支
- hotfix-20180805:从
master
分支分叉出,解决完紧急bug
问题,最后合并到develop
分支。因为直接从develop
分支创建可以发布的版本要花许多的时间,所以最好选择从master
分支创建。
- hotfix-20180805:从
- hotfix-20180802
远程分支操作
fetch
- 四种用法
$ git fetch # 更新git remote中所有的远程repo所有branch的最新commit_id,将其记录到.git/FETCH_HEAD文件中。
$ git fetch remote_repo # 只更新名称为remote_repo的远程repo上的所有branch的最新commit_id,将其记录。
$ git fetch remote_repo remote_branch_name # 只更新名称为remote_repo的远程repo上的remote_branch_name分支。
$ git fetch remote_repo remote_branch_name:local_branch_name # 同上,并在本地创建local_branch_name分支来保存远端分支的所有数据。
ps
:FETCH_HEAD
是一个版本链接,记录在本地的.git/FETCH_HEAD
文件中,指向目前已经从远程仓库取下来的分支的末端版本。
pull
- 运行过程
首先,基于本地的FETCH_HEAD
记录,比对本地的FETCH_HEAD
记录与远程仓库
的版本号,然后git fetch
获得当前指向的远程分支的后续版本的数据,然后再利用git merge
将其与本地的当前分支
合并。
push
标签(tag)操作
使用场景
通常在发版本时会给版本库打个tag
,这个tag
就是这次版本的快照,指向某个commit
的指针,类似于branch
,但是tag
不能像branch
一样可以移动。
好处是查找以前某个版本时不用记0348411ee60d4951aa53afb50bc7d6b1c6d5cdfc
这种长串的commit_id
,通过tag
来查找更清晰明了。
两种标签
-
轻标签
:只添加名称。 -
注解标签
:添加名称、注解和签名。
打标签
$ git tag tag_name # 不带注解
$ git tag -a tag_name # 调用编辑器来写注解
$ git tag -am "注解" tag_name # 简单字符串注解
- 为
commit
的历史记录打上标签:
$ git tag v0.1 0348411
查看标签
$ git tag
$ git tag -n # 查看标签和注解信息
- 列出的
tag
是按名称排序的,也可以查看某个tag
的详细信息:
$ git show tag_name
删除标签
- 本地删除:
$ git tag -d tag_name
- 删除远程标签:
$ git tag -d v1.0 # 先本地删除
$ git push origin :refs/tags/v1.0 # 远程删除
推送标签
创建的标签都是只存储在本地的,不会自动推送到远程。所以打错的标签可以在本地安全删除。
- 推送某个标签到远程:
$ git push origin tag_name
- 一次性推送全部尚未推送到远程的本地标签
$ git push origin --tags
已提交(committed)操作
修改最后一次提交
使用选项:git commit --amend
。
$ git log
$ vim xxx # wq
$ git add xxx
$ git commit --amend # 本次修改并入最后一次提交,进入编辑,修改提交说明
撤销/反悔(revert)某次提交
$ git revert HEAD # 撤销前一次commit
$ git revert HEAD^ # 撤销前前一次commit
$ git revert commit_id
ps
:第三种方式撤销,在没有和后面提交相冲突的情况下是直接成功的。
比如说:那次提交是新增了一个文件,后面的每次提交都没有修改这个文件,这时候撤销那次提交,直接做相反的操作把新增的文件删了,比较顺利。
回退/重置(reset)到某次提交
$ git reset --hard # 最后一次commit
$ git reset --hard HEAD~~ # 回退到前前一次commit
$ git reset --hard ORIG_HEAD # 回退到某次commit
ps
:git revert
和git reset
的区别:
git revert
是用一次新的commit
来回滚之前的commit
,而git reset
是直接删除指定commit
之后的所有commit
。在回滚这一操作上看,效果差不多。但是在日后继续
merge
以前的老版本时有区别。因为git revert
是用一次逆向的commit
来中和
之前的提交,因此日后合并老的branch
时,导致这部分改变不会再次出现,但是git reset
是之间把某些commit
在某个branch
上删除,因而和老的branch
再次merge
时,这些被回滚的commit
应该还会被引入。git reset
是把HEAD
向后移动了一下,而git revert
是HEAD
继续前进,只是新的commit
的内容和要revert
的内容正好相反,能够抵消
要被revert
的内容。
导入提交
使用场景:
在branch1
开发时进行了多次提交,这时切换到branch2
,想把之前branch1
分支提交的commit
都copy
过来,怎么办?
做法
:首先切换到branch1
分支,然后查看提交历史记录,也可以用sourceTree
查看,也可以用命令git log
。用法:
$ git checkout branch1
$ git log # 找到想要操作的commit
$ git checkout branch2 # 将commit导入branch2
$ git cherry-pick commit_id # 单个commit
$ git cherry-pick commit_id1..commit_id2 # commit_id1和commit_id2之间的commit
汇合(合并)提交
- 用法:
$ git rebase -i HEAD~~
自动打开编辑器,HEAD
- HEAD~~
的commit
的都被列出来了,将最后一个pick
改成squash
,保存,进入注释说明编辑,再保存退出,完成合并(两个提交合并成了一个)。
修改提交
参考资料
Git Community Book 中文版
廖雪峰的官方网站 - Git教程
猴子都能懂的GIT入门
Git Book