Skip to content

Commit 08f58cf

Browse files
authored
Merge pull request #38 from fcmiranda/feature/new-features
feature: add new features
2 parents 241618b + a10ef65 commit 08f58cf

9 files changed

Lines changed: 526 additions & 39 deletions

File tree

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,30 @@ git add .
6868
lazycommit commit | fzf --prompt='Pick commit> ' | xargs -r -I {} git commit -m "{}"
6969
```
7070

71+
Use gc-style parameters:
72+
73+
```bash
74+
# stage all tracked, deleted, and untracked files first, then generate 3 suggestions in Spanish with emoji
75+
lazycommit commit --stage-all -g 3 -l Spanish -e
76+
77+
# emoji output is normalized even when the upstream model response is inconsistent
78+
79+
# force opencode provider/model for one run
80+
lazycommit commit -p opencode \
81+
-m opencode/minimax-m2.5-free
82+
83+
# print only first generated message (for scripting)
84+
lazycommit commit -o
85+
86+
# debug mode (prints provider/model/diff diagnostics)
87+
lazycommit commit -d
88+
89+
# stage all files first, then generate 3 suggestions with emoji
90+
lazycommit commit --stage-all -g 3 -e
91+
92+
93+
```
94+
7195
Generate PR titles against `main` branch:
7296

7397
```bash

cmd/commit.go

Lines changed: 191 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
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

2022
func 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+
2450
var 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+
}

cmd/commit_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package cmd
2+
3+
import "testing"
4+
5+
func TestApplyOutputOverrides(t *testing.T) {
6+
in := []string{"feat: add provider and model flags"}
7+
out := applyOutputOverrides(in, true)
8+
9+
if len(out) != 1 {
10+
t.Fatalf("expected 1 message, got %d", len(out))
11+
}
12+
if out[0] != "✨ feat: add provider and model flags" {
13+
t.Fatalf("unexpected output: %q", out[0])
14+
}
15+
}
16+
17+
func TestEnsureGitmojiPrefix(t *testing.T) {
18+
got := ensureGitmojiPrefix("fix: handle empty staged diff")
19+
want := "🐛 fix: handle empty staged diff"
20+
if got != want {
21+
t.Fatalf("got %q, want %q", got, want)
22+
}
23+
}
24+
25+
func TestEnsureGitmojiPrefix_NoDoubleEmoji(t *testing.T) {
26+
msg := "✨ feat: add provider override"
27+
if got := ensureGitmojiPrefix(msg); got != msg {
28+
t.Fatalf("emoji should not be duplicated, got %q", got)
29+
}
30+
}
31+
32+
func TestEnsureGitmojiPrefix_NonASCIIDescription(t *testing.T) {
33+
got := ensureGitmojiPrefix("feat: añade validación")
34+
want := "✨ feat: añade validación"
35+
if got != want {
36+
t.Fatalf("got %q, want %q", got, want)
37+
}
38+
}

0 commit comments

Comments
 (0)