git commitizen
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
gcz/main.go

542 lines
17 KiB

package main
import (
"bytes"
"errors"
"fmt"
"git.yaojiankang.top/hupeh/gcz/survey"
"os"
"os/exec"
"strings"
)
type Emoji string
const (
emojiTada = Emoji("🎉") // , "tada", "庆祝", "初次提交")
emojiNew = Emoji("🆕") // , "new", "全新", "引入新功能")
emojiBookmark = Emoji("🔖") // , "bookmark", "书签", "发行/版本标签")
emojiBug = Emoji("🐛") // , "bug", "bug", "修复 bug")
emojiAmbulance = Emoji("🚑") // , "ambulance", "急救车", "重要补丁")
emojiGlobeWithMeridians = Emoji("🌐") // , "globe_with_meridians", "地球", "国际化与本地化")
emojiLipstick = Emoji("💄") // , "lipstick", "口红", "更新 UI 和样式文件")
emojiClapper = Emoji("🎬") // , "clapper", "场记板", "更新演示/示例")
emojiRotatingLight = Emoji("🚨") // , "rotating_light", "警车灯", "移除 linter 警告")
emojiWrench = Emoji("🔧") // , "wrench", "扳手", "修改配置文件")
emojiHeavyPlusSign = Emoji("➕") // , "heavy_plus_sign", "加号", "增加一个依赖")
emojiHeavyMinusSign = Emoji("➖") // , "heavy_minus_sign", "减号", "减少一个依赖")
emojiArrowUp = Emoji("⬆") // , "arrow_up", "上升箭头", "升级依赖")
emojiArrowDown = Emoji("⬇") // , "arrow_down", "下降箭头", "降级依赖")
emojiZap = Emoji("⚡") // , "zap", "闪电", "提升性能")
emojiRacehorse = Emoji("🐎") // , "racehorse", "赛马", "提升性能")
emojiChartWithUpwardsTrend = Emoji("📈") // , "chart_with_upwards_trend", "上升趋势图", "添加分析或跟踪代码")
emojiRocket = Emoji("🚀") // , "rocket", "火箭", "部署功能")
emojiWhiteCheckMark = Emoji("✅") // , "white_check_mark", "白色复选框", "增加测试")
emojiMemo = Emoji("📝") // , "memo", "备忘录", "撰写文档")
emojiBook = Emoji("📖") // , "book", "书", "撰写文档")
emojiHammer = Emoji("🔨") // , "hammer", "锤子", "重大重构")
emojiArt = Emoji("🎨") // , "art", "调色板", "改进代码结构/代码格式")
emojiFire = Emoji("🔥") // , "fire", "火焰", "移除代码或文件")
emojiPencil2 = Emoji("✏") // , "pencil2", "铅笔", "修复 typo")
emojiConstruction = Emoji("🚧") // , "construction", "施工", "工作进行中")
emojiWastebasket = Emoji("🗑") // , "wastebasket", "垃圾桶", "废弃或删除")
emojiWheelchair = Emoji("♿") // , "wheelchair", "轮椅", "可访问性")
emojiConstructionWorker = Emoji("👷") // , "construction_worker", "工人", "添加 CI 构建系统")
emojiGreenHeart = Emoji("💚") // , "green_heart", "绿心", "修复 CI 构建问题")
emojiLock = Emoji("🔒") // , "lock", "锁", "修复安全问题")
emojiWhale = Emoji("🐳") // , "whale", "鲸鱼", "Docker 相关工作")
emojiApple = Emoji("🍎") // , "apple", "苹果", "修复在 MacOS 下的问题")
emojiGreenApple = Emoji("🍏") // , "green_apple", "IOS", "修复在 iOS 下的问题。")
emojiPenguin = Emoji("🐧") // , "penguin", "企鹅", "修复 Linux 下的问题")
emojiRobot = Emoji("🤖") // , "robot", "安卓", "修复 Android 下的问题")
emojiCheckeredFlag = Emoji("🏁") // , "checkered_flag", "旗帜", "修复 Windows 下的问题")
emojiTwistedRightwardsArrows = Emoji("🔀") // , "twisted_rightwards_arrows", "交叉箭头", "分支合并")
emojiSparkles = Emoji("✨") // , "sparkles", "新特性", "引入新特性")
)
type emojiInfo struct {
flag, title, desc string
}
var emojiInfos map[Emoji]*emojiInfo
func (e Emoji) Flag() string {
if info, ok := emojiInfos[e]; ok {
return info.flag
} else {
return ""
}
}
func (e Emoji) Title() string {
if info, ok := emojiInfos[e]; ok {
return info.title
} else {
return ""
}
}
func (e Emoji) Description() string {
if info, ok := emojiInfos[e]; ok {
return info.desc
} else {
return ""
}
}
type Type string
const (
typeFeat = Type("feat") // "新功能(feature)"
typeFix = Type("fix") // "自动修复问题 (适合于一次提交直接修复问题)"
typeTo = Type("to") // "不自动修复问题 (适合于多次提交,最终修复问题提交时使用fix)"
typeDocs = Type("docs") // "文档(documentation)"
typeStyle = Type("style") // "格式(不影响代码运行的变动)"
typeRefactor = Type("refactor") // "重构(即不是新增功能,也不是修改bug的代码变动)"
typePerf = Type("perf") // "优化相关,比如提升性能、体验"
typeTest = Type("test") // "增加测试"
typeChore = Type("chore") // "构建过程或辅助工具的变动"
typeRevert = Type("revert") // "回滚到上一个版本"
typeMerge = Type("merge") // "代码合并"
)
var typeInfos map[Type]string
func (e Type) Description() string {
if desc, ok := typeInfos[e]; ok {
return desc
} else {
return ""
}
}
var emojisInType map[Type][]Emoji
func init() {
typeInfos = map[Type]string{
typeFeat: "新功能(feature)",
typeFix: "自动修复问题 (适合于一次提交直接修复问题)",
typeTo: "不自动修复问题 (适合于多次提交,最终修复问题提交时使用fix)",
typeDocs: "文档(documentation)",
typeStyle: "格式(不影响代码运行的变动)",
typeRefactor: "重构(即不是新增功能,也不是修改bug的代码变动)",
typePerf: "优化相关,比如提升性能、体验",
typeTest: "增加测试",
typeChore: "构建过程或辅助工具的变动",
typeRevert: "回滚到上一个版本",
typeMerge: "代码合并",
}
emojiInfos = map[Emoji]*emojiInfo{
emojiTada: {"tada", "庆祝", "初次提交"},
emojiNew: {"new", "全新", "引入新功能"},
emojiBookmark: {"bookmark", "书签", "发行/版本标签"},
emojiBug: {"bug", "bug", "修复 bug"},
emojiAmbulance: {"ambulance", "急救车", "重要补丁"},
emojiGlobeWithMeridians: {"globe_with_meridians", "地球", "国际化与本地化"},
emojiLipstick: {"lipstick", "口红", "更新 UI 和样式文件"},
emojiClapper: {"clapper", "场记板", "更新演示/示例"},
emojiRotatingLight: {"rotating_light", "警车灯", "移除 linter 警告"},
emojiWrench: {"wrench", "扳手", "修改配置文件"},
emojiHeavyPlusSign: {"heavy_plus_sign", "加号", "增加一个依赖"},
emojiHeavyMinusSign: {"heavy_minus_sign", "减号", "减少一个依赖"},
emojiArrowUp: {"arrow_up", "上升箭头", "升级依赖"},
emojiArrowDown: {"arrow_down", "下降箭头", "降级依赖"},
emojiZap: {"zap", "闪电", "提升性能"},
emojiRacehorse: {"racehorse", "赛马", "提升性能"},
emojiChartWithUpwardsTrend: {"chart_with_upwards_trend", "上升趋势图", "添加分析或跟踪代码"},
emojiRocket: {"rocket", "火箭", "部署功能"},
emojiWhiteCheckMark: {"white_check_mark", "白色复选框", "增加测试"},
emojiMemo: {"memo", "备忘录", "撰写文档"},
emojiBook: {"book", "书", "撰写文档"},
emojiHammer: {"hammer", "锤子", "重大重构"},
emojiArt: {"art", "调色板", "改进代码结构/代码格式"},
emojiFire: {"fire", "火焰", "移除代码或文件"},
emojiPencil2: {"pencil2", "铅笔", "修复 typo"},
emojiConstruction: {"construction", "施工", "工作进行中"},
emojiWastebasket: {"wastebasket", "垃圾桶", "废弃或删除"},
emojiWheelchair: {"heelchair", "轮椅", "可访问性"},
emojiConstructionWorker: {"construction_worker", "工人", "添加 CI 构建系统"},
emojiGreenHeart: {"green_heart", "绿心", "修复 CI 构建问题"},
emojiLock: {"lock", "锁", "修复安全问题"},
emojiWhale: {"whale", "鲸鱼", "Docker 相关工作"},
emojiApple: {"apple", "苹果", "修复在 MacOS 下的问题"},
emojiGreenApple: {"green_apple", "IOS", "修复在 iOS 下的问题。"},
emojiPenguin: {"penguin", "企鹅", "修复 Linux 下的问题"},
emojiRobot: {"robot", "安卓", "修复 Android 下的问题"},
emojiCheckeredFlag: {"checkered_flag", "旗帜", "修复 Windows 下的问题"},
emojiTwistedRightwardsArrows: {"twisted_rightwards_arrows", "交叉箭头", "分支合并"},
emojiSparkles: {"sparkles", "新特性", "引入新特性"},
}
var platformEmojis = []Emoji{
emojiGreenHeart,
emojiGreenApple,
emojiLock,
emojiWhale,
emojiApple,
emojiPenguin,
emojiRobot,
emojiCheckeredFlag,
}
platform := func(res ...Emoji) []Emoji {
return append(res, platformEmojis...)
}
emojisInType = map[Type][]Emoji{
typeFeat: {emojiNew, emojiSparkles},
typeFix: platform(emojiBug, emojiAmbulance, emojiGlobeWithMeridians, emojiWrench, emojiPencil2),
typeTo: platform(emojiBug, emojiAmbulance, emojiGlobeWithMeridians, emojiWrench, emojiPencil2),
typeDocs: {emojiGlobeWithMeridians, emojiLipstick, emojiClapper, emojiWrench, emojiHeavyPlusSign, emojiHeavyMinusSign, emojiZap, emojiRacehorse, emojiMemo, emojiBook, emojiConstruction, emojiSparkles},
typeStyle: platform(emojiTada, emojiGlobeWithMeridians, emojiLipstick, emojiClapper, emojiArt, emojiWheelchair),
typeRefactor: platform(emojiRotatingLight, emojiHeavyPlusSign, emojiHeavyMinusSign, emojiArt, emojiWastebasket, emojiConstructionWorker),
typePerf: platform(emojiGlobeWithMeridians, emojiLipstick, emojiArrowUp, emojiArrowDown, emojiZap, emojiRacehorse, emojiChartWithUpwardsTrend, emojiHammer, emojiConstructionWorker, emojiWheelchair),
typeTest: platform(emojiGlobeWithMeridians, emojiWrench, emojiHeavyPlusSign, emojiHeavyMinusSign, emojiArrowUp, emojiArrowDown, emojiWheelchair, emojiConstructionWorker),
typeChore: platform(emojiGlobeWithMeridians, emojiHeavyPlusSign, emojiHeavyMinusSign, emojiArrowUp, emojiArrowDown, emojiWheelchair),
typeRevert: platform(emojiBookmark, emojiRotatingLight),
typeMerge: {emojiBookmark, emojiTwistedRightwardsArrows},
}
}
type Message struct {
Type Type
Emoji Emoji
Scope string
Subject string
Files []string
News []string
}
func (m *Message) String() string {
var str string
if len(m.Type) > 0 {
str += string(m.Type)
if len(m.Scope) > 0 {
str += "(" + m.Scope + ")"
} else {
str += "(*)"
}
str += ": "
}
if len(m.Emoji) > 0 {
str += ":" + m.Emoji.Flag() + ": "
}
str += m.Subject
return str
}
func (m *Message) Preview() string {
var str string
if len(m.Type) > 0 {
str += string(m.Type)
if len(m.Scope) > 0 {
str += "(" + m.Scope + ")"
} else {
str += "(*)"
}
str += ": "
}
if len(m.Emoji) > 0 {
str += string(m.Emoji) + " "
}
str += m.Subject
return str
}
func (m *Message) Valid() bool {
return len(m.Type) > 0 && len(m.Subject) > 0 && len(m.Files) > 0
}
func readFiles(path string, f func(string)) error {
entries, err := os.ReadDir(path)
if err != nil {
return err
}
for _, entry := range entries {
if entry.IsDir() {
err = readFiles(path+"/"+entry.Name(), f)
if err != nil {
return err
}
} else {
f(path + "/" + entry.Name())
}
}
return nil
}
func (m *Message) AskFiles() error {
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd := exec.Command("git", "status", "-s", "-uall")
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return err
}
if stderr.Len() > 0 {
return errors.New(stderr.String())
}
infos := make(map[string]string)
var opts []string
var defs []string
lines := strings.Split(stdout.String(), "\n")
for _, line := range lines {
if len(line) < 3 {
continue
}
// 目录表示里面文件全名新增
if strings.HasSuffix(line, "/") {
err := readFiles(strings.TrimSpace(line[2:]), func(s string) {
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd := exec.Command("git", "check-ignore", s)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
fmt.Println(err)
return
}
if stderr.Len() > 0 {
return
}
if strings.TrimSpace(stdout.String()) == s {
return
}
opts = append(opts, s)
infos[s] = "新增"
m.News = append(m.News, s)
})
if err != nil {
return err
}
} else {
name := strings.TrimSpace(line[2:])
opts = append(opts, name)
switch line[1] {
case 'D':
infos[name] = "删除"
case 'A':
defs = append(defs, name)
case 'M':
infos[name] = "修改"
case 'R':
infos[name] = "重命名"
case 'T':
infos[name] = "修改文件类型"
case 'C':
infos[name] = "复制"
case 'U':
infos[name] = "更新但未合并"
case '?':
infos[name] = "新增"
m.News = append(m.News, name)
}
}
}
fmt.Println(infos)
var sels []string
err := survey.AskOne(&survey.MultiSelect{
Message: "选择提交的文件",
Options: opts,
Default: defs,
Description: func(value string, index int) string {
if desc, ok := infos[value]; ok {
return desc
}
return ""
},
}, &sels)
if err != nil {
return err
}
m.Files = sels
return nil
}
func (m *Message) AskType() error {
var types []Type
var options []string
for entry, info := range typeInfos {
types = append(types, entry)
options = append(options, string(entry)+" "+info)
}
qs := []*survey.Question{{
Name: "value",
Prompt: &survey.Select{
Message: "请选择提交类型:",
Options: options,
},
}}
var res = struct {
Value int
}{}
err := survey.Ask(qs, &res, nil)
if err != nil {
return err
}
m.Type = types[res.Value]
return nil
}
func (m *Message) AskScope() error {
qs := []*survey.Question{
{
Name: "value",
Prompt: &survey.Input{Message: "影响范围:"},
Transform: survey.ToLower,
},
}
var res = struct {
Value string
}{}
err := survey.Ask(qs, &res, nil)
if err != nil {
return err
}
m.Scope = res.Value
return nil
}
func (m *Message) AskEmoji() error {
if len(m.Type) == 0 {
err := m.AskType()
if err != nil {
return err
}
}
emojis, ok := emojisInType[m.Type]
if !ok || len(emojis) == 0 {
return nil
}
var es []Emoji
var opts []string
for _, e := range emojis {
es = append(es, e)
opts = append(opts, fmt.Sprintf("%s\x1b[36m (%s)\x1b[0m \x1b[2m- %s\x1b[0m", e, e.Title(), e.Description()))
}
// the questions to ask
var qs = []*survey.Question{{
Name: "value",
Prompt: &survey.Select{
Message: "设置 Emoji 符号:",
Options: opts,
},
}}
var res = struct {
Value int
}{}
err := survey.Ask(qs, &res, nil)
if err != nil {
return err
}
m.Emoji = es[res.Value]
return nil
}
func (m *Message) AskSubject() error {
if len(m.Type) == 0 {
err := m.AskType()
if err != nil {
return err
}
}
qs := []*survey.Question{{
Name: "value",
Prompt: &survey.Input{Message: "日志内容,不超过50个字符"},
}}
var res = struct {
Value string
}{}
err := survey.Ask(qs, &res, nil)
if err != nil {
return err
}
if len(res.Value) == 0 {
return m.AskSubject()
}
m.Subject = res.Value
return nil
}
func (m *Message) Ask() error {
if err := m.AskFiles(); err != nil {
return err
}
if err := m.AskType(); err != nil {
return err
}
if err := m.AskScope(); err != nil {
return err
}
if err := m.AskEmoji(); err != nil {
return err
}
if err := m.AskSubject(); err != nil {
return err
}
return nil
}
func (m *Message) Confirm() (bool, error) {
for !m.Valid() {
err := m.Ask()
if err != nil {
return false, err
}
}
ok := false
prompt := &survey.Confirm{
Message: "输入结果如下:\n\n \x1b[4m" + m.Preview() + "\x1b[0m\n\n\r立即提交?",
}
err := survey.AskOne(prompt, &ok)
if err != nil {
return false, err
}
if !ok {
return false, nil
}
return true, nil
}
func (m *Message) AddNews() error {
args := append([]string{"add"}, m.News...)
cmd := exec.Command("git", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
func (m *Message) Commit() error {
if len(m.News) > 0 {
err := m.AddNews()
if err != nil {
return err
}
}
args := []string{"commit", "-o"}
for _, f := range m.Files {
args = append(args, "./"+f)
}
args = append(args, "-m", m.String())
cmd := exec.Command("git", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
func main() {
var msg Message
ok, err := msg.Confirm()
if err == nil && ok {
err = msg.Commit()
}
if err != nil {
if err.Error() == "interrupt" {
fmt.Println("\nCtrl+C pressed in Terminal")
} else {
fmt.Println(err)
}
}
}