From c50004146a94d6e3619bbc6525d53c1ce6f7b8bd Mon Sep 17 00:00:00 2001 From: leehosu Date: Sat, 10 Jan 2026 18:55:01 +0900 Subject: [PATCH] feat(cmd): enhance commit and config commands Add new functionalities and improvements to command handling in commit and config modules. Update internationalization support with additional translations. --- cmd/commit.go | 54 +++++++++++++++++++++++++++++++++++++-- cmd/config.go | 35 +++++++++++++++++++++++++ cmd/root.go | 12 ++++++--- internal/config/config.go | 12 ++++++--- internal/git/commit.go | 52 ++++++++++++++++++++++++++++++++----- internal/i18n/en.go | 7 +++-- internal/i18n/i18n.go | 3 +++ internal/i18n/ko.go | 3 +++ 8 files changed, 160 insertions(+), 18 deletions(-) diff --git a/cmd/commit.go b/cmd/commit.go index 24bdc7c..950e175 100644 --- a/cmd/commit.go +++ b/cmd/commit.go @@ -39,6 +39,52 @@ func init() { rootCmd.AddCommand(commitCmd) } +// getGPGSignStatus는 현재 GPG 서명 상태를 문자열로 반환합니다 +func getGPGSignStatus(cfg *config.Config, msg i18n.Messages) string { + if noGPGSign { + if cfg.UILanguage == "ko" { + return "비활성화 (--no-gpg-sign)" + } + return "disabled (--no-gpg-sign)" + } else if gpgSign { + if cfg.UILanguage == "ko" { + return "활성화 (-S)" + } + return "enabled (-S)" + } else if cfg.GPGSign { + if cfg.UILanguage == "ko" { + return "활성화 (설정)" + } + return "enabled (config)" + } + if cfg.UILanguage == "ko" { + return "비활성화" + } + return "disabled" +} + +// getCommitOptions는 설정값과 플래그를 조합하여 CommitOptions를 반환합니다 +// 우선순위: 플래그 > 설정 파일 +func getCommitOptions(cfg *config.Config) git.CommitOptions { + opts := git.CommitOptions{ + NoVerify: noVerify, + } + + // GPG 서명 처리: 플래그가 명시적으로 지정되면 플래그 우선 + if noGPGSign { + // --no-gpg-sign 플래그가 지정되면 서명 비활성화 + opts.NoGPGSign = true + } else if gpgSign { + // -S / --gpg-sign 플래그가 지정되면 서명 활성화 + opts.GPGSign = true + } else if cfg.GPGSign { + // 플래그 미지정 시 설정 파일 값 사용 + opts.GPGSign = true + } + + return opts +} + func runCommit() error { // 3. 설정 로드 cfg, err := config.Load() @@ -65,6 +111,10 @@ func runCommit() error { return fmt.Errorf("%s: %w", msg.ErrorNoStagedChanges, err) } + // GPG 서명 상태 표시 + gpgStatus := getGPGSignStatus(cfg, msg) + color.Cyan(msg.GPGSignStatus, gpgStatus) + if diff == "" { return errors.New(msg.NoChanges) } @@ -146,7 +196,7 @@ func runCommit() error { case msg.PromptYes: // 8. 커밋 실행 color.Cyan(msg.Committing) - if err := git.Commit(commitMessage, noVerify); err != nil { + if err := git.Commit(commitMessage, getCommitOptions(cfg)); err != nil { return fmt.Errorf("%s: %w", msg.ErrorCommitFailed, err) } color.Green(msg.CommitSuccess) @@ -206,7 +256,7 @@ func runCommit() error { case msg.EditActionUseMessage: // 현재 메시지로 커밋 color.Cyan(msg.Committing) - if err := git.Commit(commitMessage, noVerify); err != nil { + if err := git.Commit(commitMessage, getCommitOptions(cfg)); err != nil { return fmt.Errorf("%s: %w", msg.ErrorCommitFailed, err) } color.Green(msg.CommitSuccess) diff --git a/cmd/config.go b/cmd/config.go index 158fffa..e3fa6ca 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -150,6 +150,7 @@ var showCmd = &cobra.Command{ color.White(msg.ConfigCommitLanguage, cfg.CommitLanguage) color.White(msg.ConfigUILanguage, cfg.UILanguage) color.White(msg.ConfigTemplate, cfg.Template) + color.White(msg.ConfigGPGSign, cfg.GPGSign) fmt.Println() color.Yellow(msg.OpenAISettings) @@ -251,6 +252,39 @@ func maskAPIKey(key string) string { return key[:4] + "..." + key[len(key)-4:] } +var setGPGSignCmd = &cobra.Command{ + Use: "set-gpg-sign [true|false]", + Short: "GPG 서명 사용 여부를 설정합니다", + Long: `커밋 시 GPG 서명을 기본으로 사용할지 설정합니다.`, + Example: ` commitmate config set-gpg-sign true + commitmate config set-gpg-sign false`, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + value := args[0] + + cfg, err := config.Load() + if err != nil { + cfg = config.Default() + } + + msg := i18n.GetMessages(cfg.UILanguage) + + if value != "true" && value != "false" { + color.Red("❌ " + msg.ErrorInvalidBoolValue) + os.Exit(1) + } + + cfg.GPGSign = value == "true" + + if err := config.Save(cfg); err != nil { + color.Red("❌ "+msg.ErrorSaveConfig, err) + os.Exit(1) + } + + color.Green(msg.GPGSignSet, cfg.GPGSign) + }, +} + func init() { rootCmd.AddCommand(configCmd) configCmd.AddCommand(setKeyCmd) @@ -258,5 +292,6 @@ func init() { configCmd.AddCommand(setModelCmd) configCmd.AddCommand(setCommitLanguageCmd) configCmd.AddCommand(setUILanguageCmd) + configCmd.AddCommand(setGPGSignCmd) configCmd.AddCommand(showCmd) } diff --git a/cmd/root.go b/cmd/root.go index ff1ce6a..21e4f8f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -9,10 +9,12 @@ import ( ) var ( - cfgFile string - provider string - dryRun bool - noVerify bool + cfgFile string + provider string + dryRun bool + noVerify bool + gpgSign bool + noGPGSign bool ) // rootCmd는 인자 없이 호출될 때의 기본 명령어입니다 @@ -48,6 +50,8 @@ func init() { // 로컬 플래그 rootCmd.Flags().BoolVar(&dryRun, "dry-run", false, "커밋 메시지만 생성하고 커밋하지 않음") rootCmd.Flags().BoolVar(&noVerify, "no-verify", false, "git commit hooks 무시") + rootCmd.Flags().BoolVarP(&gpgSign, "gpg-sign", "S", false, "GPG 서명으로 커밋") + rootCmd.Flags().BoolVar(&noGPGSign, "no-gpg-sign", false, "GPG 서명 비활성화") } // initConfig는 설정 파일과 환경변수를 읽습니다 diff --git a/internal/config/config.go b/internal/config/config.go index 5f12569..d55fbde 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -12,8 +12,9 @@ import ( type Config struct { Provider string `mapstructure:"provider"` CommitLanguage string `mapstructure:"commit_language"` // AI가 생성하는 커밋 메시지 언어 - UILanguage string `mapstructure:"ui_language"` // CLI UI 메시지 언어 + UILanguage string `mapstructure:"ui_language"` // CLI UI 메시지 언어 Template string `mapstructure:"template"` + GPGSign bool `mapstructure:"gpg_sign"` // GPG 서명 사용 여부 OpenAI OpenAIConfig `mapstructure:"openai"` Claude ClaudeConfig `mapstructure:"claude"` } @@ -36,9 +37,10 @@ type ClaudeConfig struct { func Default() *Config { return &Config{ Provider: "openai", - CommitLanguage: "en", // 커밋 메시지는 영어가 기본 - UILanguage: "ko", // UI는 한글이 기본 + CommitLanguage: "en", // 커밋 메시지는 영어가 기본 + UILanguage: "ko", // UI는 한글이 기본 Template: "conventional", + GPGSign: false, // GPG 서명은 기본적으로 비활성화 OpenAI: OpenAIConfig{ APIKey: "", Model: "gpt-4o", @@ -94,6 +96,9 @@ func Load() (*Config, error) { if uiLang := os.Getenv("COMMITMATE_UI_LANGUAGE"); uiLang != "" { cfg.UILanguage = uiLang } + if gpgSign := os.Getenv("COMMITMATE_GPG_SIGN"); gpgSign != "" { + cfg.GPGSign = gpgSign == "true" || gpgSign == "1" + } return cfg, nil } @@ -117,6 +122,7 @@ func Save(cfg *Config) error { viper.Set("commit_language", cfg.CommitLanguage) viper.Set("ui_language", cfg.UILanguage) viper.Set("template", cfg.Template) + viper.Set("gpg_sign", cfg.GPGSign) viper.Set("openai.api_key", cfg.OpenAI.APIKey) viper.Set("openai.model", cfg.OpenAI.Model) viper.Set("openai.max_tokens", cfg.OpenAI.MaxTokens) diff --git a/internal/git/commit.go b/internal/git/commit.go index c13acf1..ecb892e 100644 --- a/internal/git/commit.go +++ b/internal/git/commit.go @@ -2,27 +2,65 @@ package git import ( "bytes" + "os" "os/exec" ) +// CommitOptions는 커밋 옵션을 담는 구조체입니다 +type CommitOptions struct { + NoVerify bool + GPGSign bool // true면 -S 플래그 추가 + NoGPGSign bool // true면 --no-gpg-sign 플래그 추가 +} + // Commit은 주어진 메시지로 커밋을 생성합니다 -func Commit(message string, noVerify bool) error { +func Commit(message string, opts CommitOptions) error { args := []string{"commit", "-m", message} - if noVerify { + if opts.NoVerify { args = append(args, "--no-verify") } + // GPG 서명 옵션 처리 + if opts.GPGSign { + args = append(args, "-S") + } else if opts.NoGPGSign { + args = append(args, "--no-gpg-sign") + } + cmd := exec.Command("git", args...) - var stderr bytes.Buffer - cmd.Stderr = &stderr + // TTY 연결로 GPG pinentry 지원 + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - return err + return cmd.Run() +} + +// CommitWithFile은 파일에서 커밋 메시지를 읽어 커밋합니다 (멀티라인 메시지용) +func CommitWithFile(messageFile string, opts CommitOptions) error { + args := []string{"commit", "-F", messageFile} + + if opts.NoVerify { + args = append(args, "--no-verify") + } + + // GPG 서명 옵션 처리 + if opts.GPGSign { + args = append(args, "-S") + } else if opts.NoGPGSign { + args = append(args, "--no-gpg-sign") } - return nil + cmd := exec.Command("git", args...) + + // TTY 연결로 GPG pinentry 지원 + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + return cmd.Run() } // GetLastCommitMessage는 마지막 커밋 메시지를 반환합니다 diff --git a/internal/i18n/en.go b/internal/i18n/en.go index b4941f5..f6eb7f1 100644 --- a/internal/i18n/en.go +++ b/internal/i18n/en.go @@ -5,6 +5,7 @@ func getEnglishMessages() Messages { // Git operations CheckingRepository: "🔍 Checking repository...", AnalyzingStagedChanges: "📝 Analyzing staged changes...", + GPGSignStatus: "🔐 GPG signing: %s", LargeChangesWarning: "⚠️ Large changes detected (%d bytes). Only summarized information will be sent.", NoChanges: "No changes to commit. Please run 'git add' first", @@ -35,13 +36,14 @@ func getEnglishMessages() Messages { EditActionBack: "↩️ Back - go back", // Config - ConfigTitle: "📋 Current configuration:", + ConfigTitle: "📋 Current configuration:", ConfigProvider: "Default provider: %s", ConfigCommitLanguage: "Commit message language: %s", ConfigUILanguage: "UI language: %s", ConfigTemplate: "Template: %s", + ConfigGPGSign: "GPG sign: %v", OpenAISettings: "OpenAI settings:", - ClaudeSettings: "Claude settings:", + ClaudeSettings: "Claude settings:", APIKeyLabel: " API key: %s", APIKeyNotSet: " API key: (not set)", ModelLabel: " Model: %s", @@ -53,6 +55,7 @@ func getEnglishMessages() Messages { ModelSet: "✓ %s model set to %s", CommitLanguageSet: "✓ Commit message language set to %s", UILanguageSet: "✓ UI language set to %s", + GPGSignSet: "✓ GPG sign set to %v", // Error messages ErrorNotGitRepo: "Not a git repository", diff --git a/internal/i18n/i18n.go b/internal/i18n/i18n.go index f10ebfe..6f79293 100644 --- a/internal/i18n/i18n.go +++ b/internal/i18n/i18n.go @@ -5,6 +5,7 @@ type Messages struct { // Git 작업 CheckingRepository string AnalyzingStagedChanges string + GPGSignStatus string LargeChangesWarning string NoChanges string @@ -40,6 +41,7 @@ type Messages struct { ConfigCommitLanguage string ConfigUILanguage string ConfigTemplate string + ConfigGPGSign string OpenAISettings string ClaudeSettings string APIKeyLabel string @@ -53,6 +55,7 @@ type Messages struct { ModelSet string CommitLanguageSet string UILanguageSet string + GPGSignSet string // 에러 메시지 ErrorNotGitRepo string diff --git a/internal/i18n/ko.go b/internal/i18n/ko.go index 98d7754..0b12192 100644 --- a/internal/i18n/ko.go +++ b/internal/i18n/ko.go @@ -5,6 +5,7 @@ func getKoreanMessages() Messages { // Git 작업 CheckingRepository: "🔍 Git 저장소 확인 중...", AnalyzingStagedChanges: "📝 Staged 변경사항 분석 중...", + GPGSignStatus: "🔐 GPG 서명: %s", LargeChangesWarning: "⚠️ 변경사항이 큽니다 (%d 바이트). 요약된 정보만 전달됩니다.", NoChanges: "커밋할 변경사항이 없습니다. 'git add' 명령어를 먼저 실행하세요", @@ -40,6 +41,7 @@ func getKoreanMessages() Messages { ConfigCommitLanguage: "커밋 메시지 언어: %s", ConfigUILanguage: "UI 언어: %s", ConfigTemplate: "템플릿: %s", + ConfigGPGSign: "GPG 서명: %v", OpenAISettings: "OpenAI 설정:", ClaudeSettings: "Claude 설정:", APIKeyLabel: " API 키: %s", @@ -53,6 +55,7 @@ func getKoreanMessages() Messages { ModelSet: "✓ %s 모델이 %s로 설정되었습니다", CommitLanguageSet: "✓ 커밋 메시지 언어가 %s로 설정되었습니다", UILanguageSet: "✓ UI 언어가 %s로 설정되었습니다", + GPGSignSet: "✓ GPG 서명이 %v로 설정되었습니다", // 에러 메시지 ErrorNotGitRepo: "Git 저장소가 아닙니다",