Git

133 阅读3分钟

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

内部规范

代码库结构

  1. 代码库根目录结构需要保持整洁、清晰。仅保留合适的入口文件(如main.py、client_train.py等),其余单独文件应归入对应功能子文件夹中管理
  2. 每个代码库需更新对应的 README.md,表述清楚:代码库功能、代码结构及其他的重要内容。
  3. 代码库下需要有对应的docs(存放日常文档,包括功能文档、接口文档、安装文档、使用文档等)、examples(代码示例)、tests(单元测试,如有)。文件地址应在README.md中索引体现)。
  4. 代码库命名包含大小写字母,-,_。避免使用特殊字符或者数字。尽量使用小写字母。命名需要清晰,能阐述所做的事情。比如bms-backend,说明是项目bms的后端代码。
  5. 包含对应的CHANGELOG.md

Git

  1. 必须添加 .gitignore 文件,里面可添加文件或者文件夹,支持常规正则表达方式。目的是用于在提交代码时,过滤掉大文件或者无用文件(如.idea、env等等) 。,将文件重命名为:.gitignore,不带前缀。
  2. 当多人开发时,自己从主干拉一个分支(分支名称需要有区分性,可以为名字或者主要功能点)进行开发,当自测通过后,再提起合并申请,代码审核通过后予以合入master。
  3. 对于时间较久的各自分支,合入后,需要定时进行分支清理,避免冗余产生歧义。

常见命令

先介绍下git的仓库:本地仓库(工作区 + 版本区)+ 远程仓库。一个最简单的版本管理图例如下图所示:

git本地_仓库_远程.png

本地仓库是对于远程仓库而言的。本地仓库 = 工作区 + 版本区。

工作区即本地磁盘上的文件集合,可增删改。

版本区(版本库)即.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都被设计来将一个分支的更改并入另一个分支,只不过方式有些不同。

举例:当前的分支结构如下:

A forked commit history

merge

merge相对来说是一个比较安全的操作,不会重写项目历史。

将 master 分支合并到 feature 分支最简单的办法就是用下面这些命令:

git checkout feature git merge master  或者,也可以一行完成: git merge master feature

feature 分支中新的合并提交(merge commit)将两个分支的历史连在了一起。你会得到下面这样的分支结构:

Merging master into the feature branch

rebase

使用rebase时,要实现相同的操作,命令如下:

git checkout feature git rebase master

它会把整个 feature 分支移动到 master 分支的前面,有效地把所有 master 分支上新的提交并入过来。但是,rebase 为原分支上每一个提交创建一个新的提交,重写了项目历史,并且不会带来合并提交。你会得到下面这样的分支结构:

Rebasing the feature branch onto master

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

参考文档

Git教程-廖雪峰

Git 中文教程

附件

git command.png

git命令思维导图.jpeg