基本准则
- 合理控制
commit
的粒度,每次 commit
只包含一个功能或修复
- 正确设置
commit
时的用户信息,默认 user.name
为姓名拼音、user.email
为公司邮箱
- 在个人开发分支上,通常会有很多临时
commit
,在推送分支前需要使用 git rebase
合并 commit
以保持主分支干净整洁,方便浏览提交记录
- 非国际化项目,因此 Commit Message 均采用中文,避免一些英文语法及翻译理解上的问题
Commit Message 规范
注意:以下规范中的符号(如冒号、括号)均为中文字符
基础格式
<类型>:<标题>
// 空行
<说明> // 选填
类型
限定只能使用以下关键词:
标题
- 通常
commit
列表只显示第一行文字,因此要用一句话概括本次提交的内容
- 如果
commit
与 issue
相关,则在末尾关闭 issue
:(close #id)
说明
选填,对本次 commit
的详细说明,使用 Markdown 无序列表的格式书写
Commit Message 示例
以下均为符合规范的示例:
文档:Commit Message 规范
修复:用户注册验证码发送失败(close
特性:用户中心模块(close #123)
- 用户注册功能
- 用户登录功能
- 用户信息接口
自动生成工具
tool/commitMsg.html
什么是 Git Hooks?
详见:自定义 Git - Git 钩子
commit-msg 脚本
将脚本放入项目 .git/hooks/commit-msg
中,提交代码时会自动执行 Commit Message 格式检查
#!/bin/sh
FILE='.git/COMMIT_EDITMSG'
TYPE=('特性' '修复' '优化' '文档' '重构' '其他')
function ERROR() {
divider=$(printf %.s- {1..150})
echo -e $divider
echo -e "[ 提交信息错误 ] $1"
echo -e $divider
exit 1
}
msg=$(cat $FILE)
if [ "$msg" == 'no message' ]; then
ERROR '不能为空!'
fi
if [ ${#msg} -lt 10 ]; then
ERROR "字数 < 10(当前 ${#msg} 个字)"
fi
title=$(cat $FILE | awk 'NR==1')
item=(${title//':'/' '})
if [[ ${TYPE[*]/${item[0]}/} == ${TYPE[*]} ]]; then
ERROR "类型(${item[0]})错误!(仅限使用:${TYPE[*]})"
fi
blank=$(cat $FILE | awk 'NR==2')
if [[ "$blank" != '' ]]; then
ERROR "第 2 行请留空!"
fi
num=$(cat $FILE | awk 'END{print NR}')
for ((n = 3; n <= $num; n++)); do
desc=$(cat $FILE | awk "NR==$n")
if [[ "$desc" =~ ^(- .+) ]]; then
continue
fi
ERROR "说明($desc)格式错误!"
done
pre-commit 脚本
配置 PHP
参数并将脚本放入项目 .git/hooks/pre-commit
中,提交代码时会自动执行 PHP 语法及文档排版检查
#!/bin/sh
PHP_SYNTAX_CHECK='true'
DOC_TYPE_SETTING_CHECK='true'
read -r -d '' DOC_TYPE_SETTING_CHECK_SCRIPT <<'EOF'
function format($text)
{
$cjk[] = '\x{2e80}-\x{2eff}';
$cjk[] = '\x{2f00}-\x{2fdf}';
$cjk[] = '\x{3040}-\x{309f}';
$cjk[] = '\x{30a0}-\x{30ff}';
$cjk[] = '\x{3100}-\x{312f}';
$cjk[] = '\x{3200}-\x{32ff}';
$cjk[] = '\x{3400}-\x{4dbf}';
$cjk[] = '\x{4e00}-\x{9fff}';
$cjk[] = '\x{f900}-\x{faff}';
$cjk = implode('', $cjk);
$patterns = [
'cjk_quote' => ['(['.$cjk.'])(["\'])', '$1 $2'],
'quote_cjk' => ['(["\'])(['.$cjk.'])', '$1 $2'],
'fix_quote' => ['(["\']+)(\s*)(.+?)(\s*)(["\']+)', '$1$3$5'],
'cjk_hash' => ['(['.$cjk.'])(
'hash_cjk' => ['((\S+)#)(['.$cjk.'])', '$1 $3'],
'cjk_operator_ans' => ['(['.$cjk.'])([A-Za-zΑ-Ωα-ω0-9])([\+\-\*\/=&\\|<>])', '$1 $2 $3'],
'ans_operator_cjk' => ['([\+\-\*\/=&\\|<>])([A-Za-zΑ-Ωα-ω0-9])(['.$cjk.'])', '$1 $2 $3'],
'bracket' => [
['(['.$cjk.'])([<\[\{\(]+(.*?)[>\]\}\)]+)(['.$cjk.'])', '$1 $2 $4'],
[
'cjk_bracket' => ['(['.$cjk.'])([<>\[\]\{\}\(\)])', '$1 $2'],
'bracket_cjk' => ['([<>\[\]\{\}\(\)])(['.$cjk.'])', '$1 $2'],
]
],
'fix_bracket' => ['([<\[\{\(]+)(\s*)(.+?)(\s*)([>\]\}\)]+)', '$1$3$5'],
'cjk_ans' => ['(['.$cjk.'])([A-Za-zΑ-Ωα-ω0-9`@&%\=\$\^\*\-\+\\/|\\\])', '$1 $2'],
'ans_cjk' => ['([A-Za-zΑ-Ωα-ω0-9`~!%&=;\|\,\.\:\?\$\^\*\-\+\/\\\])(['.$cjk.'])', '$1 $2'],
];
foreach ($patterns as $key => $value) {
if ($key !== 'bracket') {
$text = preg_replace('/'.$value[0].'/iu', $value[1], $text);
continue;
}
$old = $text;
$new = preg_replace('/'.$value[0][0].'/iu', $value[0][1], $text);
$text = $new;
if ($old !== $new) {
continue;
}
foreach ($value[1] as $val) {
$text = preg_replace('/'.$val[0].'/iu', $val[1], $text);
}
}
return $text;
}
error_reporting(0);
$file = $argv[1];
$source = file_get_contents($file);
$result = format($source);
if ($source == $result) {
exit;
}
$pos = strspn($source ^ $result, "\0");
$fragment['source'] = mb_strcut($source, $pos - 30, 100);
$fragment['result'] = mb_strcut($result, $pos - 30, 100);
$data = [
sprintf('%s(第 %d 字节)', $file, $pos),
sprintf('原文:%s', str_replace(["\r\n", "\n"], ' ', $fragment['source'])),
sprintf('优化:%s', str_replace(["\r\n", "\n"], ' ', $fragment['result'])),
];
echo implode("\n", $data);
exit(1);
EOF
divider=$(printf %.s- {1..150})
files=$(git diff --cached --name-only --diff-filter=ARM)
for file in ${files[@]}; do
extension=${file##*.}
if [[ $PHP_SYNTAX_CHECK == 'true' && $extension == 'php' ]]; then
result=$(php -l $file)
if [ $? -eq 0 ]; then
continue
fi
echo -e "${divider}\n[ PHP 语法错误 ] ${result}\n${divider}"
exit 1
fi
if [[ $DOC_TYPE_SETTING_CHECK == 'true' && $extension == 'md' ]]; then
result=$($PHP -r "$DOC_TYPE_SETTING_CHECK_SCRIPT" "$file")
if [ $? -eq 0 ]; then
continue
fi
echo -e "${divider}\n[ 文档排版错误 ] ${result}\n${divider}"
exit 1
fi
done
master 分支
包含所有正式发布的版本,在 master
分支上的 commit
都应该打上版本 tag
(此分支只允许合并,不能直接提交)

develop 分支
主开发分支(此分支只允许合并,不能直接提交)
feature 分支
命名规范:feature/功能名称
基于 develop
分支创建,主要用来开发一个新功能,开发完成后合并回 develop
, 合并完成后可删除此 feature
分支

release 分支
命名规范:release/版本号
当需要发布新版本时,基于 develop
分支创建一个 release
分支,并在此分支上测试、修复问题,完成后合并至 master
和 develop
分支

hotfix 分支
命名规范:hotfix/问题名称
当发现问题时,基于 master
分支创建 hotfix
分支,在此分支修复完成后合并至 master
和 develop
分支,同时在 master
分支打上版本 tag

基于 Sourcetree + GitLab 实现
仓库 GitFlow 初始化
在 Sourcetree 进入仓库后,点击右上角 Git工作流
按钮,使用默认配置初始化:

创建 feature 分支
初始化完毕后依次点击 Git工作流
- 建立新的功能
按钮创建分支:

功能开发完成并检查测试后,先切到 develop
分支拉取最新代码,再切回 feature
分支把 develop
分支合并进来
推送至 GitLab 远程分支
由于代码需要审核,此时应当使用 推送
功能,将此分支推送至 GitLab 远程分支:

在 GitLab 上发起 Merge Request
发起合并请求将 feature
分支合并至 develop
分支
