工作场景在线模拟
learngitbranching.js.org/?locale=zh_…
图解流程
Git 和其他版本控制系统如SVN 的一个不同之处就是暂存区的概念。
-
Workspace:工作区
-
Repository:版本库/仓库区(或本地仓库),工作区有一个隐藏目录
**.git,**这个不算工作区,是Git 的版本库。Git 的版本库里存了很多东西,其中最重要的就是
stage或叫index的暂存区,还有Git 为开发者自动创建的第一个分支master,以及指向master 的一个指针HEAD。 -
Index / Stage:暂存区
-
Remote:远程仓库
新建代码库
# 在当前目录新建一个git 代码库
git init
# 新建一个目录,将其初始化为git 代码库
git init [project-name]
# 下载一个项目和它的整个代码历史
git clone [url]
配置
Git 的设置文件为.gitconfig,它可以在用户主目录下(全局配置),也可以在项目目录下(项目配置)
# 显示当前的Git 配置
$ git config --list
# 编辑Git 配置文件
$ git config -e [--global]
# 设置提交代码时的用户信息
$ git config [--global] user.name "[name]"
$ git config [--global] user.email "[email address]"
# 颜色设置
git config --global color.ui true # git status等命令自动着色
git config --global color.status auto
git config --global color.diff auto
git config --global color.branch auto
git config --global color.interactive auto
git config --global --unset http.proxy # remove proxy configuration on git
增加/删除文件
# 添加指定文件到暂存区
$ git add [file1] [file2] ...
# 添加指定目录到暂存区,包括子目录
$ git add [dir]
# 添加当前目录的所有文件到暂存区
$ git add .
# 添加每个变化前,都会要求确认
# 对于同一个文件的多处变化,可以实现分次提交
$ git add -p
# 删除工作区文件,并且将这次删除放入暂存区
$ git rm [file1] [file2] ...
# 停止追踪指定文件,但该文件会保留在工作区
$ git rm --cached [file]
# 改名文件,并且将这个改名放入暂存区
$ git mv [file-original] [file-renamed]
代码提交
# 提交暂存区到仓库区
$ git commit -m [message]
# 提交暂存区的指定文件到仓库区
$ git commit [file1] [file2] ... -m [message]
# 提交工作区自上次commit 之后的变化,直接到仓库区
$ git commit -a
# 提交时显示所有diff 信息
$ git commit -v
# 将add 和commit 合为一步
$ git commit -am 'message'
# 使用一次新的commit,替代上一次提交
# 如果代码没有任何新变化,则用来改写上一次commit的提交信息
$ git commit --amend -m [message]
# 重做上一次commit,并包括指定文件的新变化
$ git commit --amend [file1] [file2] ...
分支
# 列出所有本地分支
$ git branch
# 列出所有远程分支
$ git branch -r
# 列出所有本地分支和远程分支
$ git branch -a
# 新建一个分支,但依然停留在当前分支
$ git branch [branch-name]
# 新建一个分支,并切换到该分支
$ git checkout -b [branch]
# 新建一个分支,指向指定commit
$ git branch [branch] [commit]
# 新建一个分支,与指定的远程分支建立追踪关系
$ git branch --track [branch] [remote-branch]
# 切换到指定分支,并更新工作区
$ git checkout [branch-name] // 切换分支:git switch <name>; 创建并切换到新分支:git switch -c feature1
# 切换到上一个分支
$ git checkout -
# 建立追踪关系,在现有分支与指定的远程分支之间
$ git branch --set-upstream [branch] [remote-branch]
# 合并指定分支到当前分支
$ git merge [branch]
# 选择一个commit,合并进当前分支
$ git cherry-pick [commit]
1、紧急 bug 修复 - 当发现一个 bug 时,需要尽快向已发布的产品提供修复补丁,同时也要将补丁整合到开发主分支中
2、在一个错误的分支上创建了一个提交,可以使用 cherry-pick 将其移动到正确的分支上去
- cherry-pick 并不会应用提交所代表的整个文件快照,而是只会影响该在提交中新增、删除或更改的文件。
- cherry-pick 并不是简单的应用目标提交与其父提交的 diff 内容,而是会在内部以该父提交作为基础在当前分支指向提交和目标提交之间进行一次三路合并,因此有可能发生合并冲突。
# 删除分支
$ git branch -d [branch-name]
# 删除远程分支
$ git push origin --delete [branch-name]
$ git branch -dr [remote/branch]
# 检出版本v2.0
$ git checkout v2.0
# 从远程分支develop 创建新本地分支devel 并检出
$ git checkout -b devel origin/develop
# 检出head 版本的README文件(可用于修改错误回退)
git checkout -- README
标签
# 列出所有tag
$ git tag
# 新建一个tag 在当前commit
$ git tag [tag]
# 新建一个tag 在指定commit
$ git tag [tag] [commit]
# 删除本地tag
$ git tag -d [tag]
# 删除远程tag
$ git push origin :refs/tags/[tagName]
# 查看tag 信息
$ git show [tag]
# 提交指定tag
$ git push [remote] [tag]
# 提交所有tag
$ git push [remote] --tags
# 新建一个分支,指向某个tag
$ git checkout -b [branch] [tag]
查看信息
# 显示有变更的文件
$ git status
# 显示当前分支的版本历史
$ git log
# 显示commit 历史,以及每次commit 发生变更的文件
$ git log --stat
# 搜索提交历史,根据关键词
$ git log -S [keyword]
# 显示某个commit 之后的所有变动,每个commit 占据一行
$ git log [tag] HEAD --pretty=format:%s
# 显示某个commit 之后的所有变动,其"提交说明"必须符合搜索条件
$ git log [tag] HEAD --grep feature
# 显示某个文件的版本历史,包括文件改名
$ git log --follow [file]
$ git whatchanged [file]
# 显示指定文件相关的每一次diff
$ git log -p [file]
# 显示过去5次提交
$ git log -5 --pretty --oneline
# 显示所有提交过的用户,按提交次数排序
$ git shortlog -sn
# 显示指定文件是什么人在什么时间修改过
$ git blame [file]
# 显示暂存区和工作区的差异
$ git diff
# 显示暂存区和上一个commit 的差异
$ git diff --cached [file]
# 显示工作区与当前分支最新commit 之间的差异
$ git diff HEAD
# 显示两次提交之间的差异
$ git diff [first-branch]...[second-branch]
# 显示今天你写了多少行代码
$ git diff --shortstat "@{0 day ago}"
# 显示某次提交的元数据和内容变化
$ git show [commit]
# 显示某次提交发生变化的文件
$ git show --name-only [commit]
# 显示某次提交时,某个文件的内容
$ git show [commit]:[filename]
# 显示当前分支的最近几次提交
$ git reflog
远程同步
# 下载远程仓库的所有变动
$ git fetch [remote]
# 显示所有远程仓库
$ git remote -v
# 显示某个远程仓库的信息
$ git remote show [remote]
# 增加一个新的远程仓库,并命名
$ git remote add [shortname] [url]
# 取回远程仓库的变化,并与本地分支合并
$ git pull [remote] [branch]
# 上传本地指定分支到远程仓库
$ git push [remote] [branch]
# 强行推送当前分支到远程仓库,即使有冲突
$ git push [remote] --force
// 适用场景:远程仓库的个人分支 => 本地分支通过git reset --hard <版本号> 回退版本,导致落后于远程分支,必须使用强制推送覆盖远程分支
# 推送所有分支到远程仓库
$ git push [remote] --all
# 删除远程仓库
git remote rm name
# 修改仓库名
git remote rename old_name new_name
git remote add origin <https://gitee.com/lucius_w/xxx.git>
git push -u origin master
# 推送本地分支到远程仓库某个分支
git push --set-upstream origin 分支名
撤回
# 场景一:工作区代码撤回 => 工作区代码在未进行git add之前是标红的,本地修改了test1.py,新增加了test2.py 文件
# 思路:
git status // 查看文件状态,两个文件均标红:**modified: test1.py**、**Untracked files:** **test2.py**
git checkout -- test1.py // 撤回test1.py 文件的修改
git status // 再次查看文件的状态,发现test1.py 文件的修改被还原了,只会看到:**Untracked files:** **test2.py**
# 场景二:add 到暂存区的代码撤回 => 开发修改了test1.py 文件,并且修改记录已经被提交到缓存区
# 思路:
git status // 查看提交记录,modified: test1.py
git reset HEAD
git status // 再次查看文件的状态,发现test1.py 文件已经撤回到工作区,可以再次修改或者继续撤回工作区的代码来还原到上一次提交的状态
# 场景三:提交到本地仓库的说明信息撤回
# 思路:
git log // 查看本地仓库的提交记录,其中“feature: 应用概览” 提交信息就是想要撤回的内容
git commit --amend // 会进入类似vim 的编辑页面,修改提交信息后保存即可
git log // 再次查看本地仓库的提交记录,发现想要修改的信息被撤回且修改了
# 场景四:提交到本地仓库的代码撤回
# 思路:
git log // 查看本地仓库的提交记录,其中“feature: 测试提交2”的记录就是我们想退回的版本,记录对应的commit id
git reset --hard 1d4aa5
git reset --hard <版本号>,每次提交成功后,都会生成一个哈希码作为版本号
// 直接填版本号,表示跳转到某个版本,哈希码很长不用全部写,输入前几个字符即可
// 可以使用HEAD^ 来描述版本,一个^ 表示前一个版本,两个^^表示前两个版本;
// 也可以使用数字来代替^,HEAD~10
git log // 再次查看本地仓库的提交记录,发现回退版本commit id 之后提交的记录已经被撤回
命令git checkout -- xxx.txt意思就是,把xxx.txt文件在工作区的修改全部撤销,这里有两种情况:
一种是xxx.txt自修改后还没有被放到暂存区,现在,撤销修改就回到和版本库一模一样的状态;
一种是xxx.txt已经添加到暂存区后,又作了修改,现在,撤销修改就回到添加到暂存区后的状态。
总之,就是让这个文件回到最近一次git commit或git add时的状态。
git reset – mixed / –soft / –hard
--mixed:默认方式,将撤回的代码,存放到工作区。同时会保留本地未提交的内容。
--soft:回退到某个版本 。将撤回的代码,存放到暂存区。同时会保留本地未提交的内容。
--hard:彻底回退到某个版本,丢弃将撤回的代码,本地没有commit 的修改会被全部擦掉。
注意:
- 使用–hard 需确保本地没有未提交文件
- – hard 回退丢失的文件可以通过git reflog 来恢复(但不包括未提交的文件)
以下内容摘自stackoverflow
If we're on branch master with this series of commits:
- A - B - C (master)
HEADpoints to C and the index matches C.
When we run git reset --soft B, master (and thus HEAD) now points to B, but the index still has the changes from C.
git status will show them as staged. So if we run git commit at this point, we'll get a new commit with the same changes as C.
Now let's do git reset --mixed B. (Note: --mixed is the default option). Once again, master and HEAD point to B, but this time the index is also modified to match B. If we run git commit at this point, nothing will happen since the index matches HEAD. We still have the changes in the working directory, but since they're not in the index, git status shows them as unstaged. To commit them, you would git add and then commit as usual.
And finally, --hard is the same as --mixed (it changes your HEAD and index), except that --hard also modifies your working directory. If we're at C and run git reset --hard B, then the changes added in C, as well as any uncommitted changes you have, will be removed, and the files in your working directory will match commit B. Since you can permanently lose changes this way, you should always run git status before doing a hard reset to make sure your working directory is clean or that you're okay with losing your uncommitted changes.
Note: You can recover any committed changes with the reflog; uncommitted changes that are removed with reset --hard are gone forever.
Git Flow
GitFlow
GitFlow 通常包含五种类型的分支:Master分支、Develop分支、Feature分支、Release分支以及Hotfix分支。
- Master分支:主干分支,通常情况下只允许其他分支将代码合入,不允许向Master 分支直接提交代码(对应生产环境)。
- Develop分支:开发分支,用来集成测试最新合入的开发成果,包含要发布到下一个Release的代码(对应开发环境)。
- Feature分支:特性分支,通常从Develop分支拉出,每个新特性的开发对应一个特性分支,用于开发人员提交代码并进行自测。自测完成后,会将Feature分支的代码合并至Develop分支,进入下一个Release。
- Release分支:发布分支,发布新版本时,基于Develop分支创建,发布完成后,合并到Master和Develop分支(对应集成测试环境)。
- Hot fix分支:热修复分支,生产环境发现新Bug 时创建的临时分支,问题验证通过后,合并到Master 和Develop 分支。
GitHubFlow
它来源于GitHub 团队的工作实践。当代码托管在GitHub上时,则需要使用GitHubFlow。相比GitFlow而言,GitHubFlow没有那么多分支。
GitHubFlow 通常只有一个Master 分支是固定的,而且GitHubFlow 中的Master分支通常是受保护的,只有特定权限的人才可以向Master 分支合入代码。
在GitHubFlow 中,新功能开发或修复Bug 需要从Master 分支拉取一个新分支,在这个新分支上进行代码提交;功能开发完成,开发者创建Pull Request(简称PR),通知源仓库开发者进行代码修改review,确认无误后,将由源仓库开发人员将代码合入Master 分支。
GitLabFlow
GitLabFlow 出现的最晚,GitLabFlow 是开源工具GitLab 推荐的做法。
GitLabFlow 支持GitFlow 的分支策略,也支持GitHubFlow 的“Pull Request”(在GitLabFlow 中被称为“Merge Request”)。
相比于GitHubFlow,GitLabFlow 增加了对预生产环境和生产环境的管理,即Master 分支对应为开发环境的分支,预生产和生产环境由其他分支(如Pre-Production、Production)进行管理。在这种情况下,Master 分支是Pre-Production分支的上游,Pre-Production 是Production 分支的上游;GitLabFlow 规定代码必须从上游向下游发展,即新功能或修复Bug时,特性分支的代码测试无误后,必须先合入Master 分支,然后才能由Master 分支向Pre-Production 环境合入,最后由Pre-Production 合入到Production。
GitLabFlow 中的Merge Request 是将一个分支合入到另一个分支的请求,通过Merge Request 可以对比合入分支和被合入分支的差异,也可以做代码的Review。
用GitLab 的MR 代码评审
MR 的assignee 指的是该请求的负责人,他将负责审阅代码并决定是否将其合并到目标分支。Reviewer 则是指代码审阅人,负责检查代码是否符合规范并提供建议。两者可能是同一个人,也可能是不同的人。
Git 核心优点和实现思路
Git 内部文件处理:
- 代码库里所有文件都是数据,以数据为中心,所有操作、存储和处理逻辑围绕数据展开。
- 跟踪整个项目整体状态,每次建立全局快照,而不是跟踪每个文件的变更。
- 针对git 目录下的每个文件计算一个hash 值,文件内容作为value,hash 值为文件key。
- 如果单个文件内容发生变化, 下次重新计算hash 值。如果文件没有发生变更,当前快照指向历史hash。
- 整个代码库的变化历史和文件组成用树形结构表示。
- Git 每次commit 记录整个代码库的一次快照,当前快照包含发生变化的文件和历史快照(子树)。
Git 执行效率与时间复杂度
对于Git 内部结构可以简单的理解为:Git 内部是一棵树,每个节点都是一个指针(key),这个指针(key)可能代表一个文件,或一次commit 或一个分支起点或一次merge 或一个tag,key 对应的value 就是内容,如果key 是代表一个文件,value就是文件内容;如果key 是代表一次commit,value是一颗子树,包含此次commit 对整个项目的snapshot。
平时很多git 操作都可以近似理解为:在树上执行遍历查找O(lg(n)),切换指针O(1),然后根据指针取文件内容O(1))。
这些操作速度都是很快的,只有在网络交互,文件压缩与解压和计算diff 时,人肉可以感知到有时间等待。
Git 空间压缩与访问效率平衡
对于Git,近期发生变化的数据属于热数据,Git假设这些数据会被频繁访问或使用到。其他数据为历史数据。对于热数据, 即使发生微小变化,Git也会全量冗余存储,提高访问效率。当热数据文件数量达到一定值时,会触发打包压缩逻辑, delta 差异存储,节省空间。
Git 安全性与容错性
Git 对文件内容和项目整体snapshot 都使用hash 值表示,hash 值与内容一一对应, 如果文件内容被篡改或硬盘损坏导致数据丢失,hash 值校验都会失败。
此时Git 设计时已经假设:硬盘是随时崩溃的,即存储是不可靠的;有人恶意引入Bug 或偷偷修改代码
Merge 算法思想: 三路合并
先找出两个分支的公共祖先, 然后两个分支分别与公共祖先diff,指出有冲突的地方。
Git merge 并没有试图智能的去解决冲突,只是指出冲突,然后将merge 交给最合适最高效的人去解决:即引起冲突的开发者。
git merge vs git rebase
场景:使用git merge 命令将 master 分支合并到 feature分支中:
git checkout feature
git merge master
// git merge feature master
git merge 会在 feature 分支中新增一个新的 merge commit,然后将两个分支的历史联系在一起
- 使用 merge 是很好的方式,因为它是一种非破坏性的操作,对现有分支不会以任何方式被更改。
- 另一方面,这也意味着
feature分支每次需要合并上游更改时,它都将产生一个额外的合并提交。 - 如果master 提交非常活跃,这可能会严重污染你的
feature分支历史记录。不过这个问题可以使用高级选项git log来缓解
使用git rebase 命令将 master 分支合并到 feature分支中:
git checkout feature
git rebase master
// git rebase feature master
- rebase 会将整个
feature分支新增的commit 延续到master分支后面,从而有效地整合了所有 master 分支上的提交。 - 但是,与 merge 提交方式不同,rebase 通过为原始分支中的每个提交创建全新的 commits 来重写项目历史记录,特点是仍然会在
feature分支上形成线性提交 - rebase 的主要好处是可以获得更清晰的项目历史。首先,它消除了 git merge 所需的不必要的合并提交;rebase 会产生完美线性的项目历史记录,可以在 feature分支上没有任何分叉的情况下一直追寻到项目的初始提交。
【总结】
- 融合代码到公共分支的时使用
git merge,而不用git rebase - 融合代码到个人分支的时候使用
git rebase,可以不污染分支的提交记录,形成简洁的线性提交历史记录。
git pull —rebase
分别checkout –b 出来两个分支,独立开发互不干扰。正常情况下,如果这两个分支的改动都没有冲突的时候,一切都很顺利的。
在develop_newfeature_authorcheck 里修改了点东西,push 到develop。然后checkout 到develop_newfeature_apiwrapper。
当再git pull,这将会把develop_newfeature_authorcheck 分支的修改直接拉下来于本地代码merge,且产生一个commit,也就是merge commit。
使用git pull –rebase产生的提交结果就完全不一样,rebase 并不会产生一个commit 提交,而是会将你的E commit 附加到D commit 的结尾处。此处的F commmit是无意义的,它只是一个merge commit。
husky + lint-staged
Git Hooks 就是在 Git 执行特定事件(如commit、push、receive等)时触发运行的脚本,类似于“钩子函数”,没有设置可执行的钩子将被忽略。
在项目的 .git/hooks 目录中,有一些 .sample 结尾的钩子示例脚本,如果想启用对应的钩子,只需手动删除后缀即可。
lint-staged 是一个在 git 暂存文件上(也就是被 git add 的文件)运行已配置的 linter(或其他)任务。lint-staged 总是将所有暂存文件的列表传递给任务。
// package.json
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"lint-staged": {
"src/**/*.{js,vue}": "eslint"
}
"devDependencies": {
...
"husky": "^4.0.0",
...
}