44 "context"
55 "fmt"
66 "os"
7+ "regexp"
8+ "strings"
79
810 "github.com/m7medvision/lazycommit/internal/config"
911 "github.com/m7medvision/lazycommit/internal/git"
@@ -19,82 +21,157 @@ type CommitProvider interface {
1921
2022func init () {
2123 RootCmd .AddCommand (commitCmd )
24+
25+ commitCmd .Flags ().StringVarP (& commitProviderFlag , "provider" , "p" , "" , "Provider override: opencode, openai, copilot, anthropic, gemini" )
26+ commitCmd .Flags ().StringVarP (& commitModelFlag , "model" , "m" , "" , "Model override for selected provider" )
27+ commitCmd .Flags ().IntVarP (& commitGenerateFlag , "generate" , "g" , 0 , "Number of commit message suggestions to generate" )
28+ commitCmd .Flags ().StringVarP (& commitLanguageFlag , "lang" , "l" , "" , "Language override for generated commit messages" )
29+ commitCmd .Flags ().BoolVarP (& commitEmojiFlag , "emoji" , "e" , false , "Prefix generated commit messages with gitmoji" )
30+ commitCmd .Flags ().BoolVarP (& commitMessageOnlyFlag , "message-only" , "o" , false , "Print only the first generated message" )
31+ commitCmd .Flags ().BoolVar (& commitStageAllFlag , "stage-all" , false , "Stage all tracked, deleted, and untracked changes before generating commit messages" )
32+ commitCmd .Flags ().BoolVarP (& commitSilentEmptyFlag , "silent-empty" , "n" , false , "Stay silent when there are no staged changes" )
33+ commitCmd .Flags ().BoolVarP (& commitDebugFlag , "debug" , "d" , false , "Show debug diagnostics" )
2234}
2335
36+ var (
37+ commitProviderFlag string
38+ commitModelFlag string
39+ commitGenerateFlag int
40+ commitLanguageFlag string
41+ commitEmojiFlag bool
42+ commitMessageOnlyFlag bool
43+ commitStageAllFlag bool
44+ commitSilentEmptyFlag bool
45+ commitDebugFlag bool
46+ )
47+
48+ var conventionalTypePattern = regexp .MustCompile (`^([a-z]+)(\([^)]+\))?:\s+` )
49+
2450var commitCmd = & cobra.Command {
2551 Use : "commit" ,
2652 Short : "Generate commit message suggestions" ,
27- Long : `Analyzes your staged changes and generates a list of 10 conventional commit message suggestions.` ,
53+ Long : `Analyzes your staged changes and generates conventional commit message suggestions.` ,
54+ Example : ` lazycommit commit
55+ lazycommit commit --stage-all
56+ lazycommit commit -p opencode -m opencode/minimax-m2.5-free
57+ lazycommit commit -g 3 -l Spanish
58+ lazycommit commit -o` ,
2859 Run : func (cmd * cobra.Command , args []string ) {
60+ if commitStageAllFlag {
61+ hasChanges , err := git .HasChanges ()
62+ if err != nil {
63+ fmt .Fprintf (os .Stderr , "Error checking git status: %v\n " , err )
64+ os .Exit (1 )
65+ }
66+ if ! hasChanges {
67+ if ! commitSilentEmptyFlag {
68+ fmt .Println ("No changes to stage." )
69+ }
70+ return
71+ }
72+ if err := git .StageAll (); err != nil {
73+ fmt .Fprintf (os .Stderr , "Error staging changes: %v\n " , err )
74+ os .Exit (1 )
75+ }
76+ if commitDebugFlag {
77+ fmt .Fprintln (os .Stderr , "debug: staged all changes with git add --all" )
78+ }
79+ }
80+
2981 diff , err := git .GetStagedDiff ()
3082 if err != nil {
3183 fmt .Fprintf (os .Stderr , "Error getting staged diff: %v\n " , err )
3284 os .Exit (1 )
3385 }
3486
3587 if diff == "" {
36- fmt .Println ("No staged changes to commit." )
88+ if ! commitSilentEmptyFlag {
89+ fmt .Println ("No staged changes to commit." )
90+ }
91+ if commitDebugFlag {
92+ fmt .Fprintln (os .Stderr , "debug: staged diff is empty" )
93+ }
3794 return
3895 }
3996
97+ providerName := strings .TrimSpace (config .GetProvider ())
98+ if strings .TrimSpace (commitProviderFlag ) != "" {
99+ providerName = strings .TrimSpace (commitProviderFlag )
100+ }
101+
102+ if providerName == "" {
103+ fmt .Fprintln (os .Stderr , "Provider is empty. Set one with 'lazycommit config set' or use --provider." )
104+ os .Exit (1 )
105+ }
106+ if ! isSupportedCommitProvider (providerName ) {
107+ fmt .Fprintf (os .Stderr , "Unsupported provider: %s\n " , providerName )
108+ os .Exit (1 )
109+ }
110+
40111 var aiProvider CommitProvider
41112
42- providerName := config .GetProvider ()
113+ generateCount := commitGenerateFlag
114+ if generateCount <= 0 {
115+ generateCount = config .GetNumSuggestionsForProvider (providerName )
116+ }
117+ if generateCount <= 0 {
118+ generateCount = 10
119+ }
43120
44121 // API keys are not needed for CLI-backed providers.
45122 var apiKey string
46123 if providerName != "anthropic" && providerName != "gemini" && providerName != "opencode" {
47124 var err error
48- apiKey , err = config .GetAPIKey ( )
125+ apiKey , err = config .GetAPIKeyForProvider ( providerName )
49126 if err != nil {
50127 fmt .Fprintf (os .Stderr , "Error getting API key: %v\n " , err )
51128 os .Exit (1 )
52129 }
53130 }
54131
55132 var model string
56- if providerName == "copilot" || providerName == "openai" || providerName == "anthropic" || providerName == "gemini" || providerName == "opencode" {
133+ if strings .TrimSpace (commitModelFlag ) != "" {
134+ model = strings .TrimSpace (commitModelFlag )
135+ } else if providerName == "copilot" || providerName == "openai" || providerName == "anthropic" || providerName == "gemini" || providerName == "opencode" {
57136 var err error
58- model , err = config .GetModel ( )
137+ model , err = config .GetModelForProvider ( providerName )
59138 if err != nil {
60139 fmt .Fprintf (os .Stderr , "Error getting model: %v\n " , err )
61140 os .Exit (1 )
62141 }
63142 }
64143
65- endpoint , err := config .GetEndpoint ( )
144+ endpoint , err := config .GetEndpointForProvider ( providerName )
66145 if err != nil {
67146 fmt .Fprintf (os .Stderr , "Error getting endpoint: %v\n " , err )
68147 os .Exit (1 )
69148 }
70149
150+ if commitDebugFlag {
151+ fmt .Fprintf (os .Stderr , "debug: provider=%s model=%s generate=%d lang=%q emoji=%t message_only=%t silent_empty=%t\n " ,
152+ providerName , model , generateCount , commitLanguageFlag , commitEmojiFlag , commitMessageOnlyFlag , commitSilentEmptyFlag )
153+ fmt .Fprintf (os .Stderr , "debug: stage_all=%t\n " , commitStageAllFlag )
154+ fmt .Fprintf (os .Stderr , "debug: diff_bytes=%d endpoint=%q\n " , len (diff ), endpoint )
155+ }
156+
157+ provider .SetRuntimeCommitPromptOptions (provider.CommitPromptOptions {
158+ Generate : generateCount ,
159+ Language : strings .TrimSpace (commitLanguageFlag ),
160+ Emoji : commitEmojiFlag ,
161+ })
162+ defer provider .ResetRuntimeCommitPromptOptions ()
163+
71164 switch providerName {
72165 case "copilot" :
73166 aiProvider = provider .NewCopilotProviderWithModel (apiKey , model , endpoint )
74167 case "openai" :
75168 aiProvider = provider .NewOpenAIProvider (apiKey , model , endpoint )
76169 case "anthropic" :
77- // Get num_suggestions from config (default to 10)
78- numSuggestions := config .GetNumSuggestions ()
79- if numSuggestions <= 0 {
80- numSuggestions = 10
81- }
82- aiProvider = provider .NewAnthropicProvider (model , numSuggestions )
170+ aiProvider = provider .NewAnthropicProvider (model , generateCount )
83171 case "gemini" :
84- numSuggestions := config .GetNumSuggestions ()
85- if numSuggestions <= 0 {
86- numSuggestions = 10
87- }
88- aiProvider = provider .NewGeminiProvider (model , numSuggestions )
172+ aiProvider = provider .NewGeminiProvider (model , generateCount )
89173 case "opencode" :
90- numSuggestions := config .GetNumSuggestions ()
91- if numSuggestions <= 0 {
92- numSuggestions = 10
93- }
94- aiProvider = provider .NewOpencodeProvider (model , config .GetFallbackModels (), numSuggestions )
95- default :
96- // Default to copilot if provider is not set or unknown
97- aiProvider = provider .NewCopilotProvider (apiKey , endpoint )
174+ aiProvider = provider .NewOpencodeProvider (model , config .GetFallbackModelsForProvider (providerName ), generateCount )
98175 }
99176
100177 commitMessages , err := aiProvider .GenerateCommitMessages (context .Background (), diff )
@@ -108,8 +185,96 @@ var commitCmd = &cobra.Command{
108185 return
109186 }
110187
188+ if generateCount > 0 && len (commitMessages ) > generateCount {
189+ commitMessages = commitMessages [:generateCount ]
190+ }
191+
192+ commitMessages = applyOutputOverrides (commitMessages , commitEmojiFlag )
193+
194+ if commitDebugFlag {
195+ fmt .Fprintf (os .Stderr , "debug: generated_messages=%d\n " , len (commitMessages ))
196+ }
197+
198+ if commitMessageOnlyFlag {
199+ fmt .Println (commitMessages [0 ])
200+ return
201+ }
202+
111203 for _ , msg := range commitMessages {
112204 fmt .Println (msg )
113205 }
114206 },
115207}
208+
209+ func isSupportedCommitProvider (providerName string ) bool {
210+ switch providerName {
211+ case "copilot" , "openai" , "anthropic" , "gemini" , "opencode" :
212+ return true
213+ default :
214+ return false
215+ }
216+ }
217+
218+ func applyOutputOverrides (messages []string , addEmoji bool ) []string {
219+ out := make ([]string , 0 , len (messages ))
220+ for _ , msg := range messages {
221+ updated := msg
222+ if addEmoji {
223+ updated = ensureGitmojiPrefix (updated )
224+ }
225+ out = append (out , updated )
226+ }
227+ return out
228+ }
229+
230+ func ensureGitmojiPrefix (msg string ) string {
231+ trimmed := strings .TrimSpace (msg )
232+ if trimmed == "" {
233+ return msg
234+ }
235+ if hasLeadingEmoji (trimmed ) {
236+ return msg
237+ }
238+ matches := conventionalTypePattern .FindStringSubmatch (trimmed )
239+ if len (matches ) < 2 {
240+ return msg
241+ }
242+ emojiByType := map [string ]string {
243+ "feat" : "✨" ,
244+ "fix" : "🐛" ,
245+ "refactor" : "♻️" ,
246+ "perf" : "⚡️" ,
247+ "docs" : "📝" ,
248+ "style" : "🎨" ,
249+ "test" : "🧪" ,
250+ "chore" : "🔧" ,
251+ "ci" : "👷" ,
252+ "build" : "📦" ,
253+ "revert" : "⏪️" ,
254+ "security" : "🔒️" ,
255+ }
256+ if emoji , ok := emojiByType [matches [1 ]]; ok {
257+ return emoji + " " + trimmed
258+ }
259+ return msg
260+ }
261+
262+ func hasLeadingEmoji (s string ) bool {
263+ for _ , r := range strings .TrimSpace (s ) {
264+ return isEmojiRune (r )
265+ }
266+ return false
267+ }
268+
269+ func isEmojiRune (r rune ) bool {
270+ switch {
271+ case r >= 0x1F000 && r <= 0x1FAFF :
272+ return true
273+ case r >= 0x2600 && r <= 0x27BF :
274+ return true
275+ case r == 0x00A9 || r == 0x00AE || r == 0x3030 :
276+ return true
277+ default :
278+ return false
279+ }
280+ }
0 commit comments