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) } } }