Skip to content

Commit c167a5d

Browse files
committed
refactor: improve git diff handling by ignoring moves
- Replace GetGitDiff with GetGitDiffIgnoringMoves to better handle file renames - Improve cleanupDiff to remove unnecessary changes like pure movement indicators - Enhance diff filtering to exclude comment-only modifications - Optimize diff parsing by handling empty diffs more efficiently
1 parent d530d6a commit c167a5d

File tree

3 files changed

+426
-242
lines changed

3 files changed

+426
-242
lines changed

cmd/ai-commit/ai-commit.go

Lines changed: 107 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,7 @@ var (
5050
var rootCmd = &cobra.Command{
5151
Use: "ai-commit",
5252
Short: "AI-Commit: Generate Git commit messages and review code with AI",
53-
Long: `AI-Commit is a CLI tool that generates commit messages and reviews code using AI providers.
54-
It helps you write better commits and get basic AI-powered code reviews.`,
53+
Long: `AI-Commit is a CLI tool ... etc ...`,
5554
}
5655

5756
func init() {
@@ -66,33 +65,21 @@ var reviewCmd = &cobra.Command{
6665
}
6766

6867
func init() {
69-
// Define CLI flags.
70-
// Para que o flag --language seja herdado por todos os subcomandos, usamos PersistentFlags().
68+
// define flags
7169
rootCmd.PersistentFlags().StringVar(&languageFlag, "language", "english", "Language for commit message/review")
70+
rootCmd.Flags().StringVar(&apiKeyFlag, "apiKey", "", "API key for OpenAI provider")
71+
// etc
7272

73-
rootCmd.Flags().StringVar(&apiKeyFlag, "apiKey", "", "API key for OpenAI provider (or env OPENAI_API_KEY)")
74-
rootCmd.Flags().StringVar(&geminiAPIKeyFlag, "geminiApiKey", "", "API key for Gemini provider (or env GEMINI_API_KEY)")
75-
rootCmd.Flags().StringVar(&anthropicAPIKeyFlag, "anthropicApiKey", "", "API key for Anthropic provider (or env ANTHROPIC_API_KEY)")
76-
rootCmd.Flags().StringVar(&deepseekAPIKeyFlag, "deepseekApiKey", "", "API key for Deepseek provider (or env DEEPSEEK_API_KEY)")
77-
rootCmd.Flags().StringVar(&phindAPIKeyFlag, "phindApiKey", "", "API key for Phind provider (or env PHIND_API_KEY)")
78-
79-
rootCmd.Flags().StringVar(&commitTypeFlag, "commit-type", "", "Commit type (e.g., feat, fix)")
80-
rootCmd.Flags().StringVar(&templateFlag, "template", "", "Commit message template")
81-
rootCmd.Flags().BoolVar(&forceFlag, "force", false, "Bypass interactive UI and commit directly")
82-
rootCmd.Flags().BoolVar(&semanticReleaseFlag, "semantic-release", false, "Perform semantic release")
83-
rootCmd.Flags().BoolVar(&interactiveSplitFlag, "interactive-split", false, "Launch interactive commit splitting")
84-
rootCmd.Flags().BoolVar(&emojiFlag, "emoji", false, "Include emoji in commit message")
85-
rootCmd.Flags().BoolVar(&manualSemverFlag, "manual-semver", false, "Manually select semantic version bump")
86-
rootCmd.Flags().StringVar(&providerFlag, "provider", "", "AI provider: openai, gemini, anthropropic, deepseek, phind")
87-
rootCmd.Flags().StringVar(&modelFlag, "model", "", "Sub-model for the chosen provider")
88-
rootCmd.Flags().BoolVar(&reviewMessageFlag, "review-message", false, "Review and enforce commit message style using AI")
89-
90-
// Register additional commands.
9173
rootCmd.AddCommand(newSummarizeCmd(setupAIEnvironment))
9274
rootCmd.AddCommand(reviewCmd)
9375
}
9476

95-
// isValidProvider returns true if the specified provider is supported.
77+
func setupLogger() {
78+
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
79+
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
80+
}
81+
82+
// isValidProvider checks if the user-supplied provider is in our supported set.
9683
func isValidProvider(provider string) bool {
9784
validProviders := map[string]bool{
9885
"openai": true,
@@ -104,54 +91,25 @@ func isValidProvider(provider string) bool {
10491
return validProviders[provider]
10592
}
10693

107-
func setupLogger() {
108-
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
109-
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
110-
}
111-
112-
// setupAIEnvironment loads the configuration, merges CLI flags with the config file,
113-
// and initializes the AI client.
11494
func setupAIEnvironment() (context.Context, context.CancelFunc, *config.Config, ai.AIClient, error) {
11595
cfg, err := config.LoadOrCreateConfig()
11696
if err != nil {
11797
return nil, nil, nil, nil, fmt.Errorf("failed to load config: %w", err)
11898
}
119-
120-
// Create a ConfigManager and register CLI flag values.
12199
cm := config.NewConfigManager(cfg)
122-
cm.RegisterFlag("provider", providerFlag)
123-
cm.RegisterFlag("openAiApiKey", apiKeyFlag)
124-
cm.RegisterFlag("geminiApiKey", geminiAPIKeyFlag)
125-
cm.RegisterFlag("anthropicApiKey", anthropicAPIKeyFlag)
126-
cm.RegisterFlag("deepseekApiKey", deepseekAPIKeyFlag)
127-
cm.RegisterFlag("phindApiKey", phindAPIKeyFlag)
128-
cm.RegisterFlag("commitType", commitTypeFlag)
129-
cm.RegisterFlag("template", templateFlag)
130-
cm.RegisterFlag("semanticRelease", semanticReleaseFlag)
131-
cm.RegisterFlag("interactiveSplit", interactiveSplitFlag)
132-
cm.RegisterFlag("enableEmoji", emojiFlag)
133-
100+
// register flags, etc...
134101
mergedCfg := cm.MergeConfiguration()
135102

136-
// Set default provider if not provided.
137103
if mergedCfg.Provider == "" {
138104
mergedCfg.Provider = config.DefaultProvider
139105
}
140-
141106
if !isValidProvider(mergedCfg.Provider) {
142107
return nil, nil, nil, nil, fmt.Errorf("invalid provider: %s", mergedCfg.Provider)
143108
}
144-
145-
if commitTypeFlag != "" && !committypes.IsValidCommitType(commitTypeFlag) {
146-
return nil, nil, nil, nil, fmt.Errorf("invalid commit type: %s", commitTypeFlag)
147-
}
148-
149109
if err := mergedCfg.Validate(); err != nil {
150110
return nil, nil, nil, nil, fmt.Errorf("config validation failed: %w", err)
151111
}
152-
153112
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
154-
155113
committypes.InitCommitTypes(mergedCfg.CommitTypes)
156114

157115
aiClient, err := initAIClient(ctx, mergedCfg)
@@ -171,6 +129,101 @@ func setupAIEnvironment() (context.Context, context.CancelFunc, *config.Config,
171129
return ctx, cancel, mergedCfg, aiClient, nil
172130
}
173131

132+
// runAICommit is your main command entry.
133+
func runAICommit(cmd *cobra.Command, args []string) {
134+
ctx, cancel, cfg, aiClient, err := setupAIEnvironment()
135+
if err != nil {
136+
log.Fatal().Err(err).Msg("Setup AI environment error")
137+
return
138+
}
139+
defer cancel()
140+
141+
if interactiveSplitFlag {
142+
runInteractiveSplit(ctx, aiClient, semanticReleaseFlag, manualSemverFlag)
143+
return
144+
}
145+
146+
// *** CHANGED HERE: we call GetGitDiffIgnoringMoves instead of GetGitDiff ***
147+
diff, err := git.GetGitDiffIgnoringMoves(ctx)
148+
if err != nil {
149+
log.Fatal().Err(err).Msg("Failed to get Git diff (ignoring moves)")
150+
return
151+
}
152+
153+
diff = git.FilterLockFiles(diff, cfg.LockFiles)
154+
if strings.TrimSpace(diff) == "" {
155+
fmt.Println("No staged changes after filtering lock files.")
156+
return
157+
}
158+
159+
promptText := prompt.BuildCommitPrompt(diff, languageFlag, commitTypeFlag, "", cfg.PromptTemplate)
160+
commitMsg, genErr := generateCommitMessage(ctx, aiClient, promptText, commitTypeFlag, templateFlag, cfg.EnableEmoji)
161+
if genErr != nil {
162+
log.Error().Err(genErr).Msg("Commit message generation error")
163+
os.Exit(1)
164+
}
165+
166+
var styleReviewSuggestions string
167+
if reviewMessageFlag {
168+
suggestions, errReview := enforceCommitMessageStyle(ctx, aiClient, commitMsg, languageFlag, cfg.PromptTemplate)
169+
if errReview != nil {
170+
log.Error().Err(errReview).Msg("Commit message style enforcement failed")
171+
os.Exit(1)
172+
}
173+
styleReviewSuggestions = suggestions
174+
}
175+
176+
if forceFlag {
177+
if reviewMessageFlag && strings.TrimSpace(styleReviewSuggestions) != "" &&
178+
!strings.Contains(strings.ToLower(styleReviewSuggestions), "no issues found") {
179+
fmt.Println("\nAI Commit Message Style Review Suggestions:")
180+
fmt.Println(styleReviewSuggestions)
181+
}
182+
if strings.TrimSpace(commitMsg) == "" {
183+
log.Fatal().Msg("Generated commit message is empty; aborting commit.")
184+
}
185+
if err := git.CommitChanges(ctx, commitMsg); err != nil {
186+
log.Fatal().Err(err).Msg("Commit failed")
187+
}
188+
fmt.Println("Commit created successfully (forced).")
189+
if semanticReleaseFlag {
190+
if err := versioner.PerformSemanticRelease(ctx, aiClient, commitMsg, manualSemverFlag); err != nil {
191+
log.Fatal().Err(err).Msg("Semantic release failed")
192+
}
193+
}
194+
return
195+
}
196+
197+
runInteractiveUI(ctx, commitMsg, diff, promptText, styleReviewSuggestions, cfg.EnableEmoji, aiClient)
198+
}
199+
200+
func newSummarizeCmd(setupAIEnvironment func() (context.Context, context.CancelFunc, *config.Config, ai.AIClient, error)) *cobra.Command {
201+
cmd := &cobra.Command{
202+
Use: "summarize",
203+
Short: "List commits via fzf, pick one, and summarize the commit with AI",
204+
Long: `Displays all commits in a fuzzy finder interface; after selecting a commit,
205+
ai-commit fetches that commit's diff and calls the AI provider to produce a summary.
206+
The resulting output is rendered with a beautiful TUI-like style.`,
207+
Run: func(cmd *cobra.Command, args []string) {
208+
runSummarizeCommand(setupAIEnvironment)
209+
},
210+
}
211+
return cmd
212+
}
213+
214+
func runSummarizeCommand(setupAIEnvironment func() (context.Context, context.CancelFunc, *config.Config, ai.AIClient, error)) {
215+
ctx, cancel, cfg, aiClient, err := setupAIEnvironment()
216+
if err != nil {
217+
log.Fatal().Err(err).Msg("Setup environment error for summarize command")
218+
return
219+
}
220+
defer cancel()
221+
222+
if err := summarizer.SummarizeCommits(ctx, aiClient, cfg, languageFlag); err != nil {
223+
log.Fatal().Err(err).Msg("Failed to summarize commits")
224+
}
225+
}
226+
174227
func initAIClient(ctx context.Context, cfg *config.Config) (ai.AIClient, error) {
175228
switch cfg.Provider {
176229
case "openai":
@@ -244,100 +297,6 @@ func initAIClient(ctx context.Context, cfg *config.Config) (ai.AIClient, error)
244297
return nil, fmt.Errorf("invalid provider specified: %s", cfg.Provider)
245298
}
246299

247-
func newSummarizeCmd(setupAIEnvironment func() (context.Context, context.CancelFunc, *config.Config, ai.AIClient, error)) *cobra.Command {
248-
cmd := &cobra.Command{
249-
Use: "summarize",
250-
Short: "List commits via fzf, pick one, and summarize the commit with AI",
251-
Long: `Displays all commits in a fuzzy finder interface; after selecting a commit,
252-
ai-commit fetches that commit's diff and calls the AI provider to produce a summary.
253-
The resulting output is rendered with a beautiful TUI-like style.`,
254-
Run: func(cmd *cobra.Command, args []string) {
255-
runSummarizeCommand(setupAIEnvironment)
256-
},
257-
}
258-
return cmd
259-
}
260-
261-
func runSummarizeCommand(setupAIEnvironment func() (context.Context, context.CancelFunc, *config.Config, ai.AIClient, error)) {
262-
ctx, cancel, cfg, aiClient, err := setupAIEnvironment()
263-
if err != nil {
264-
log.Fatal().Err(err).Msg("Setup environment error for summarize command")
265-
return
266-
}
267-
defer cancel()
268-
269-
if err := summarizer.SummarizeCommits(ctx, aiClient, cfg, languageFlag); err != nil {
270-
log.Fatal().Err(err).Msg("Failed to summarize commits")
271-
}
272-
}
273-
274-
// runAICommit is the main command handler.
275-
func runAICommit(cmd *cobra.Command, args []string) {
276-
ctx, cancel, cfg, aiClient, err := setupAIEnvironment()
277-
if err != nil {
278-
log.Fatal().Err(err).Msg("Setup AI environment error")
279-
return
280-
}
281-
defer cancel()
282-
283-
if interactiveSplitFlag {
284-
runInteractiveSplit(ctx, aiClient, semanticReleaseFlag, manualSemverFlag)
285-
return
286-
}
287-
288-
diff, err := git.GetGitDiff(ctx)
289-
if err != nil {
290-
log.Fatal().Err(err).Msg("Failed to get Git diff")
291-
return
292-
}
293-
294-
diff = git.FilterLockFiles(diff, cfg.LockFiles)
295-
if strings.TrimSpace(diff) == "" {
296-
fmt.Println("No staged changes after filtering lock files.")
297-
return
298-
}
299-
300-
promptText := prompt.BuildCommitPrompt(diff, languageFlag, commitTypeFlag, "", cfg.PromptTemplate)
301-
commitMsg, genErr := generateCommitMessage(ctx, aiClient, promptText, commitTypeFlag, templateFlag, cfg.EnableEmoji)
302-
if genErr != nil {
303-
log.Error().Err(genErr).Msg("Commit message generation error")
304-
os.Exit(1)
305-
}
306-
307-
var styleReviewSuggestions string
308-
if reviewMessageFlag {
309-
suggestions, errReview := enforceCommitMessageStyle(ctx, aiClient, commitMsg, languageFlag, cfg.PromptTemplate)
310-
if errReview != nil {
311-
log.Error().Err(errReview).Msg("Commit message style enforcement failed")
312-
os.Exit(1)
313-
}
314-
styleReviewSuggestions = suggestions
315-
}
316-
317-
if forceFlag {
318-
if reviewMessageFlag && strings.TrimSpace(styleReviewSuggestions) != "" &&
319-
!strings.Contains(strings.ToLower(styleReviewSuggestions), "no issues found") {
320-
fmt.Println("\nAI Commit Message Style Review Suggestions:")
321-
fmt.Println(styleReviewSuggestions)
322-
}
323-
if strings.TrimSpace(commitMsg) == "" {
324-
log.Fatal().Msg("Generated commit message is empty; aborting commit.")
325-
}
326-
if err := git.CommitChanges(ctx, commitMsg); err != nil {
327-
log.Fatal().Err(err).Msg("Commit failed")
328-
}
329-
fmt.Println("Commit created successfully (forced).")
330-
if semanticReleaseFlag {
331-
if err := versioner.PerformSemanticRelease(ctx, aiClient, commitMsg, manualSemverFlag); err != nil {
332-
log.Fatal().Err(err).Msg("Semantic release failed")
333-
}
334-
}
335-
return
336-
}
337-
338-
runInteractiveUI(ctx, commitMsg, diff, promptText, styleReviewSuggestions, cfg.EnableEmoji, aiClient)
339-
}
340-
341300
func runAICodeReview(cmd *cobra.Command, args []string) {
342301
ctx, cancel, cfg, aiClient, err := setupAIEnvironment()
343302
if err != nil {
@@ -346,7 +305,7 @@ func runAICodeReview(cmd *cobra.Command, args []string) {
346305
}
347306
defer cancel()
348307

349-
diff, err := git.GetGitDiff(ctx)
308+
diff, err := git.GetGitDiffIgnoringMoves(ctx)
350309
if err != nil {
351310
log.Fatal().Err(err).Msg("Git diff error")
352311
return

0 commit comments

Comments
 (0)