Git介绍
Git是目前世界上最先进的分布式版本控制系统(没有之一)。
用来进行程序猿代码、文件的提交和对应版本的管理,目前主要用在了GitHub和GitLab中。
配置
Git 自带一个 git config 的工具来帮助设置控制 Git 外观和行为的配置变量。 这些变量存储在三个不同的位置:
- /etc/gitconfig 文件: 包含系统上每一个用户及他们仓库的通用配置。 如果在执行 git config 时带上 --system 选项,那么它就会读写该文件中的配置变量。 (由于它是系统配置文件,因此你需要管理员或超级用户权限来修改它。)
- ~/.gitconfig 或 ~/.config/git/config 文件:只针对当前用户。 你可以传递 --global 选项让 Git 读写此文件,这会对你系统上所有的仓库生效。
- 当前使用仓库的 Git 目录中的 config 文件(即 .git/config):针对该仓库。 你可以传递 --local 选项让 Git 强制读写此文件,虽然默认情况下用的就是它。 (当然,你需要进入某个 Git 仓库中才能让该选项生效。)
每一个级别会覆盖上一级别的配置,所以 .git/config 的配置变量会覆盖 /etc/gitconfig 中的配置变量。
一般情况下,只需要进行git user.name和user.email的配置
# 查看配置信息
$ git config --list
# 全局配置,对所有代码库生效 # name填公司邮箱前缀,email填公司邮箱
$git config --global user.name "你的名字"
$git config --global user.email "你的邮箱"
# 局部配置,只对当前的代码库有效
$git config --local user.name "你的名字"
$git config --local user.email "你的邮箱"
配置后,远程仓库提交的commit里对应的用户即为 user.name
内部规范
代码库结构
- 代码库根目录结构需要保持整洁、清晰。仅保留合适的入口文件(如main.py、client_train.py等),其余单独文件应归入对应功能子文件夹中管理
- 每个代码库需更新对应的 README.md,表述清楚:代码库功能、代码结构及其他的重要内容。
- 代码库下需要有对应的docs(存放日常文档,包括功能文档、接口文档、安装文档、使用文档等)、examples(代码示例)、tests(单元测试,如有)。文件地址应在README.md中索引体现)。
- 代码库命名包含大小写字母,-,_。避免使用特殊字符或者数字。尽量使用小写字母。命名需要清晰,能阐述所做的事情。比如bms-backend,说明是项目bms的后端代码。
- 包含对应的CHANGELOG.md
Git
- 必须添加 .gitignore 文件,里面可添加文件或者文件夹,支持常规正则表达方式。目的是用于在提交代码时,过滤掉大文件或者无用文件(如.idea、env等等) 。,将文件重命名为:.gitignore,不带前缀。
- 当多人开发时,自己从主干拉一个分支(分支名称需要有区分性,可以为名字或者主要功能点)进行开发,当自测通过后,再提起合并申请,代码审核通过后予以合入master。
- 对于时间较久的各自分支,合入后,需要定时进行分支清理,避免冗余产生歧义。
常见命令
先介绍下git的仓库:本地仓库(工作区 + 版本区)+ 远程仓库。一个最简单的版本管理图例如下图所示:
本地仓库是对于远程仓库而言的。本地仓库 = 工作区 + 版本区。
工作区即本地磁盘上的文件集合,可增删改。
版本区(版本库)即.git文件,用来保存在工作区已操作过的记录。
版本库 = 暂存区(stage) + 分支(master) + 指针Head。
常见命令:
git init 初始化你的仓库
git remote add origin https://github.com/name/name_cangku.git 把本地已初始化的仓库与远程仓库连接起来
git clone https://github.com/name/name_cangku.git 初始化项目
git clone --recursive https://github.com/name/name_cangku.git 初始化带有子项目的项目
git status查看当前仓库的状态
git diff 查看文件修改的具体内容
git log 显示从最近到最远的提交历史
git rm 删除版本库的文件
git pull 把最新的提交从远程仓库中抓取下来,在本地合并,和git push相反
git add . 把工作区的文件全部提交到暂存区
git add your_file 把工作区的特定文件提交到暂存区
git checkout -- _your_file 撤销提交命令,用版本库里的文件替换掉工作区的文件。我觉得就像是Git世界的ctrl + z
git commit -m "xxx" 把暂存区的所有文件提交到仓库区,暂存区空空荡荡
git push origin master 把仓库区的主分支master提交到远程仓库主干里
git push origin test 把test分支提交到远程仓库origin/test分支里
git branch test 创建本地分支test
git checkout -b test origin/test 拉取远程test分支到本地test分支
git checkout test 切换到test分支
git branch -d test 删除test分支,有可能会删除失败,因为Git会保护没有被合并的分支
git push origin --delete test 删除远程test分支,不用带前缀origin
git branch -d -r origin/test 删除本地存在的已删除的远程test分支
git merge test 合并本地test分支内容到本地当前分支
git merge --no-ff test 合并本地test分支的时候禁用Fast forward模式,因为这个模式会丢失分支历史信息
git reset --soft commit-id 某个commit之前的commit-id,撤销commit,会保留工作区的修改内容 git reset --hard commit-id 撤销commit,不保留工作区的修改内容,慎用!!!
代码合并
git rebase和git merge都被设计来将一个分支的更改并入另一个分支,只不过方式有些不同。
举例:当前的分支结构如下:
merge
merge相对来说是一个比较安全的操作,不会重写项目历史。
将 master 分支合并到 feature 分支最简单的办法就是用下面这些命令:
git checkout feature git merge master 或者,也可以一行完成: git merge master feature
feature 分支中新的合并提交(merge commit)将两个分支的历史连在了一起。你会得到下面这样的分支结构:
rebase
使用rebase时,要实现相同的操作,命令如下:
git checkout feature git rebase master
它会把整个 feature 分支移动到 master 分支的前面,有效地把所有 master 分支上新的提交并入过来。但是,rebase 为原分支上每一个提交创建一个新的提交,重写了项目历史,并且不会带来合并提交。你会得到下面这样的分支结构:
rebase最大的好处是你的项目历史会非常整洁。首先,它不像 git merge 那样引入不必要的合并提交。其次,如上图所示,rebase 导致最后的项目历史呈现出完美的线性——你可以从项目终点到起点浏览而不需要任何的 fork。这让你更容易使用 git log、git bisect 和 gitk 来查看项目历史。
不过,这种简单的提交历史会带来两个后果:安全性和可跟踪性。如果你违反了 rebase 黄金法则,重写项目历史可能会给你的协作工作流带来灾难性的影响。此外,rebase 不会有合并提交中附带的信息——你看不到 feature 分支中并入了上游的哪些更改。
rebase黄金法则
最重要的就是什么时候 不能用 rebase。git rebase 的黄金法则便是,绝不要在公共的分支上使用它。由于rebase会更新项目历史(创建新的提交),当公共分支有其他协作者时,会带来误导。
代码回滚
写代码时,偶尔会出现提交失误的情况,那么如何在 add、commit、push后进行撤销操作呢?一般可以从reset、checkout、revert入手来实现。一个操作汇总如下:
命令 | 作用域 | 说明 | |
---|---|---|---|
git reset | 提交层面 | 在私有分支上舍弃一些没有提交的更改 | |
git reset | 文件层面 | 将文件从缓存区中移除 | |
git checkout | 提交层面 | 切换分支或查看旧版本 | |
git checkout | 文件层面 | 舍弃工作目录中的更改 | |
git revert | 提交层面 | 在公共分支上回滚更改 | |
git revert | 文件层面 | (然而并没有) |
提交层面
reset
提交层面上,reset 将一个分支的末端指向另一个提交。这可以用来移除当前分支的一些提交。一般用于commit后的的撤销操作。
比如,下面这两条命令让 hotfix 分支向后回退了两个提交。
git checkout hotfix git reset HEAD~2
除了在当前分支上操作,你还可以通过传入这些标记来修改你的缓存区或工作目录:
- --soft – 缓存区和工作目录都不会被改变
- --mixed – 默认选项。缓存区和你指定的提交同步,但工作目录不受影响
- --hard – 缓存区和工作目录都同步到你指定的提交
checkout
当传入分支名时,可以切换到那个分支。
git checkout hotfix
上面这个命令做的不过是将HEAD移到一个新的分支,然后更新工作目录。因为这可能会覆盖本地的修改,Git 强制你提交或者缓存工作目录中的所有更改,不然在 checkout 的时候这些更改都会丢失。
除了分支之外,你还可以传入提交的引用来 checkout 到任意的提交。这和 checkout 到另一个分支是完全一样的:把 HEAD 移动到特定的提交。一般用于add后的撤销操作。
比如,下面这个命令会 checkout 到当前提交的祖父提交。
git checkout HEAD~2
revert
Revert 撤销一个提交的同时会创建一个新的提交。这是一个安全的方法,因为它不会重写提交历史。比如,下面的命令会找出倒数第二个提交,然后创建一个新的提交来撤销这些更改,然后把这个提交加入项目中。
git checkout hotfix git revert HEAD~2
相比 git reset
,它不会改变现在的提交历史。因此,git revert
可以用在公共分支上,git reset
应该用在私有分支上。
你也可以把 git revert
当作撤销已经提交的更改,而 git reset HEAD
用来撤销没有提交的更改。
文件层面
reset
当检测到文件路径时,git reset
将缓存区同步到你指定的那个提交。比如,下面这个命令会将倒数第二个提交中的 foo.py
加入到缓存区中,供下一个提交使用。
git reset HEAD~2 foo.py
--soft
、--mixed
和 --hard
对文件层面的 git reset
毫无作用,因为缓存区中的文件一定会变化,而工作目录中的文件一定不变。
checkout
Checkout 一个文件和带文件路径 git reset
非常像,除了它更改的是工作目录而不是缓存区。不像提交层面的 checkout 命令,它不会移动 HEAD引用,也就是你不会切换到别的分支上去。
如果你缓存并且提交了 checkout 的文件,它具备将某个文件回撤到之前版本的效果。注意它撤销了这个文件后面所有的更改,而 git revert
命令只撤销某个特定提交的更改。
和 git reset
一样,这个命令通常和 HEAD 一起使用。比如 git checkout HEAD foo.py
等同于舍弃 foo.py
没有缓存的更改。这个行为和 git reset HEAD --hard
很像,但只影响特定文件。
revert
无
实用技巧
BUG分支
bug就像家常便饭一样。有了bug就需要修复,在Git中,由于分支是如此的强大,所以,每个bug都可以通过一个新的临时分支来修复,修复后,合并分支,然后将临时分支删除。
当你接到一个修复一个代号101的bug的任务时,很自然地,你想创建一个分支issue-101来修复它,但是,等等,当前正在dev上进行的工作还没有提交,Git提供了一个stash功能,可以把当前工作现场“储藏”起来,等以后恢复现场后继续工作:
$ git stash Saved working directory and index state WIP on dev: f52c633 add merge
首先确定要在哪个分支上修复bug,假定需要在master分支上修复,就从master创建临时分支:
# 创建Bug分支
$ git checkout master
Switched to branch 'master' Your branch is ahead of 'origin/master' by 6 commits. (use "git push" to publish your local commits)
$ git checkout -b issue-101
Switched to a new branch 'issue-101'
# 修复bug
$ git add readme.txt
$ git commit -m "fix bug 101"
[issue-101 4c805e2] fix bug 101
1 file changed, 1 insertion(+), 1 deletion(-)
# 合并修复内容,删除bug
$ git switch master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 6 commits.
(use "git push" to publish your local commits)
$ git merge --no-ff -m "merged bug fix 101"
issue-101 Merge made by the 'recursive' strategy.
readme.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
bug修复后,即可回到原有开发分支,恢复现场:
# 查看现场
$ git stash list
stash@{0}: WIP on dev: f52c633 add merge
# 两种恢复现场方法
$ git stash apply stash@{0} 恢复,但是恢复后,stash内容并不删除,你需要用git stash drop来删除;
$ git stash pop 恢复的同时把stash内容也删了:
之前是在master分支进行bug修复,需要同步到当前的dev分支,避免代码冲突。
可以使用git的 cherry-pick,只需要把4c805e2 fix bug 101这个提交所做的修改“复制”到dev分支。注意:我们只想复制4c805e2 fix bug 101这个提交所做的修改,并不是把整个master分支merge过来:
$ git branch
* dev master
$ git cherry-pick 4c805e2
[master 1d4b803] fix bug 101
1 file changed, 1 insertion(+), 1 deletion(-)
Git自动给dev分支做了一次提交,注意这次提交的commit是1d4b803,它并不同于master的4c805e2,因为这两个commit只是改动相同,但确实是两个不同的commit。用git cherry-pick,我们就不需要在dev分支上手动再把修bug的过程重复一遍。
Feature分支
添加一个新功能时,你肯定不希望因为一些实验性质的代码,把主分支搞乱了,所以,每添加一个新功能,最好新建一个feature分支,在上面开发,完成后,合并,最后,删除该feature分支。
假如要开发一个新功能:HappyNewYear。
# 新建分支,添加功能
$ git branch happy_new_year
$ git add happy_new_year.py
$ git commit -m"happy new year"
# 切回主干,进行合并
$ git checkout master # 新功能不再继续落地,强制删除
$ git branch -D happy_new_year
标签管理
标签是版本库的一个快照。Git的标签虽然是版本库的快照,但其实它就是指向某个commit的指针(跟分支很像对不对?但是分支可以移动,标签不能移动),所以,创建和删除标签都是瞬间完成的。
注意:标签总是和某个commit挂钩。如果这个commit既出现在master分支,又出现在dev分支,那么在这两个分支上都可以看到这个标签。
创建标签
创建标签的过程如下
git branch #查看当前分支,
git checkout master切换到master分支。
git tag <name> #打标签,默认为HEAD。比如git tag v1.0。默认标签是打在最新提交的commit上的。如果想要打标签在以前的commit上,要git log找到历史提交的commit id。如果一个commt id是du2n2d9,执行git tag v1.0 du2n2d9就把这个版本打上了v1.0的标签了。
git tag #查看所有标签,可以知道历史版本的tag。标签不是按时间顺序列出,而是按字母排序的。
git show <tagname> #查看标签信息。
git tag -a <标签名> -m "<说明>", #创建带说明的标签。-a指定标签名,-m指定说明文字。用show可以查看说明。
编辑标签
git tag -d v1.0 # 删除标签。因为创建的标签都只存储在本地,不会自动推送到远程。所以,打错的标签可以在本地安全删除。
git push origin <tagname> # 推送某个标签到远程
git push origin --tags #一次性推送全部尚未推送到远程的本地标签。如果标签推送到远程。
git tag -d v1.0 #先删除本地标签v1.0。
git push origin :refs/tags/v1.0 #删除远程标签v1.0