Skip to content

Commit 125f0b4

Browse files
authored
Merge pull request #11 from andev0x/perf
fix(templater): correct issue related to templater
2 parents 04cb05c + e04dd20 commit 125f0b4

3 files changed

Lines changed: 253 additions & 49 deletions

File tree

cmd/propose.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ var (
2121
summaryFlag bool
2222
autoFlag bool
2323
dryRunFlag bool
24+
debugFlag bool
2425

2526
proposeCmd = &cobra.Command{
2627
Use: "propose",
@@ -36,6 +37,7 @@ func init() {
3637
proposeCmd.Flags().BoolVar(&summaryFlag, "summary", false, "Print short output (summary only)")
3738
proposeCmd.Flags().BoolVar(&autoFlag, "auto", false, "Commit with the generated message")
3839
proposeCmd.Flags().BoolVar(&dryRunFlag, "dry-run", false, "Preview without committing")
40+
proposeCmd.Flags().BoolVar(&debugFlag, "debug", false, "Print debug info (analyzer output + chosen templates)")
3941
}
4042

4143
func runPropose(cmd *cobra.Command, args []string) error {
@@ -70,6 +72,22 @@ func runPropose(cmd *cobra.Command, args []string) error {
7072
return err
7173
}
7274

75+
if debugFlag {
76+
// Print analyzer output
77+
fmt.Printf("Analyzer result: %+v\n", commitMessage)
78+
// Print available templates/action/topic info from templater
79+
if act, tpls := templater.DebugInfo(commitMessage); tpls != nil {
80+
fmt.Printf("Resolved action key: %s\n", act)
81+
fmt.Printf("Candidate templates (first 10):\n")
82+
for i, t := range tpls {
83+
if i >= 10 {
84+
break
85+
}
86+
fmt.Printf(" - %s\n", t)
87+
}
88+
}
89+
}
90+
7391
initialMessage, err := templater.GetMessage(commitMessage)
7492
if err != nil {
7593
return err

internal/templater/templater.go

Lines changed: 204 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import (
44
"embed"
55
"encoding/json"
66
"fmt"
7-
"math/rand/v2"
7+
"math/rand"
88
"os"
99
"path/filepath"
1010
"strings"
@@ -109,95 +109,253 @@ func NewTemplater(templateFile string, hist *history.CommitHistory) (*Templater,
109109

110110
// GetMessage selects and formats a commit message
111111
func (t *Templater) GetMessage(msg *analyzer.CommitMessage) (string, error) {
112-
// First, normalize the action
113-
actionUpper := strings.ToUpper(msg.Action)
114-
actionTemplates, ok := t.templates[actionUpper]
112+
// Map analyzer action names (feat, fix, refactor, chore, docs, test, etc.)
113+
// to the template groups used in templates.json (A, M, D, R, DOC, MISC)
114+
actionMap := map[string]string{
115+
"feat": "A",
116+
"add": "A",
117+
"fix": "M",
118+
"bugfix": "M",
119+
"refactor": "R",
120+
"chore": "D",
121+
"test": "M",
122+
"docs": "DOC",
123+
"ci": "M",
124+
"perf": "M",
125+
"style": "MISC",
126+
"build": "MISC",
127+
}
128+
129+
// Normalize and resolve action group
130+
actionLower := strings.ToLower(msg.Action)
131+
var actionKey string
132+
if key, ok := actionMap[actionLower]; ok {
133+
actionKey = key
134+
} else if len(msg.Action) == 1 {
135+
// Already a single-letter action like A/M/D/R
136+
actionKey = strings.ToUpper(msg.Action)
137+
} else {
138+
// fallback to MISC
139+
actionKey = "MISC"
140+
}
141+
142+
actionTemplates, ok := t.templates[actionKey]
115143
if !ok {
116-
// For offline use, we have a more detailed fallback strategy
117-
fallbackActions := []string{"MISC", "A", "M"} // Priority order for fallbacks
118-
for _, fallback := range fallbackActions {
119-
if templates, exists := t.templates[fallback]; exists {
144+
// Try fallbacks: specific order prefers DOC then A then M then MISC
145+
fallbackActions := []string{"DOC", "A", "M", "R", "D", "MISC"}
146+
for _, fb := range fallbackActions {
147+
if templates, exists := t.templates[fb]; exists {
120148
actionTemplates = templates
121149
ok = true
122150
break
123151
}
124152
}
125153
if !ok {
126-
return "", fmt.Errorf("no suitable templates found for action: %s (tried fallbacks: %v)", msg.Action, fallbackActions)
154+
return "", fmt.Errorf("no suitable templates found for action: %s (resolved key: %s)", msg.Action, actionKey)
127155
}
128156
}
129157

130-
// Topic selection with smart fallback for offline use
158+
// Topic selection with improved matching and weighting
159+
normalizedTopic := strings.ToLower(strings.TrimSpace(msg.Topic))
131160
var topicTemplates []string
132-
normalizedTopic := strings.ToLower(msg.Topic)
133161

134-
// Try exact match first
135-
if templates, exists := actionTemplates[normalizedTopic]; exists && len(templates) > 0 {
136-
topicTemplates = templates
137-
} else {
138-
// Try fuzzy matching for similar topics
162+
// exact match
163+
if normalizedTopic != "" {
164+
if templates, exists := actionTemplates[normalizedTopic]; exists && len(templates) > 0 {
165+
topicTemplates = templates
166+
}
167+
}
168+
169+
// fuzzy match if exact not found
170+
if len(topicTemplates) == 0 {
139171
for topic, templates := range actionTemplates {
140-
if strings.Contains(topic, normalizedTopic) || strings.Contains(normalizedTopic, topic) {
172+
if topic == "_default" {
173+
continue
174+
}
175+
tname := strings.ToLower(topic)
176+
if normalizedTopic != "" && (strings.Contains(tname, normalizedTopic) || strings.Contains(normalizedTopic, tname)) {
141177
topicTemplates = templates
142178
break
143179
}
144180
}
181+
}
145182

146-
// If no match found, fall back to _default
147-
if len(topicTemplates) == 0 {
148-
if defaults, exists := actionTemplates["_default"]; exists && len(defaults) > 0 {
149-
topicTemplates = defaults
150-
} else {
151-
return "", fmt.Errorf("no suitable templates found for topic: %s (action: %s)", msg.Topic, actionUpper)
152-
}
183+
// fall back to _default
184+
if len(topicTemplates) == 0 {
185+
if defaults, exists := actionTemplates["_default"]; exists && len(defaults) > 0 {
186+
topicTemplates = defaults
187+
} else {
188+
return "", fmt.Errorf("no suitable templates found for topic: %s (action: %s)", msg.Topic, actionKey)
153189
}
154190
}
155191

156-
// Prepare replacer for placeholders
192+
// Prepare placeholder values
157193
source := ""
158-
if len(msg.RenamedFiles) > 0 {
159-
source = msg.RenamedFiles[0].Source
160-
}
161194
target := ""
162195
if len(msg.RenamedFiles) > 0 {
196+
source = msg.RenamedFiles[0].Source
163197
target = msg.RenamedFiles[0].Target
164198
}
165199

166-
replacer := strings.NewReplacer(
200+
// Scoring-based selection: prefer templates that use available context
201+
type scored struct {
202+
tmpl string
203+
score int
204+
}
205+
206+
var candidates []scored
207+
208+
for _, tmpl := range topicTemplates {
209+
score := 0
210+
// reward templates that include placeholders we can fill
211+
if strings.Contains(tmpl, "{item}") && msg.Item != "" {
212+
score += 3
213+
}
214+
if strings.Contains(tmpl, "{purpose}") && msg.Purpose != "" && msg.Purpose != "general update" {
215+
score += 2
216+
}
217+
if strings.Contains(tmpl, "{source}") && source != "" {
218+
score += 3
219+
}
220+
if strings.Contains(tmpl, "{target}") && target != "" {
221+
score += 3
222+
}
223+
if strings.Contains(tmpl, "{topic}") && normalizedTopic != "" {
224+
score += 1
225+
}
226+
// small randomness to diversify choices
227+
score += rand.Intn(2)
228+
229+
candidates = append(candidates, scored{tmpl: tmpl, score: score})
230+
}
231+
232+
// sort candidates by score (simple selection of best score)
233+
bestScore := -1
234+
var bestCandidates []string
235+
for _, c := range candidates {
236+
if c.score > bestScore {
237+
bestScore = c.score
238+
bestCandidates = []string{c.tmpl}
239+
} else if c.score == bestScore {
240+
bestCandidates = append(bestCandidates, c.tmpl)
241+
}
242+
}
243+
244+
// Prefer a template that is not in recent history
245+
replacerForCheck := strings.NewReplacer(
167246
"{topic}", msg.Topic,
168247
"{item}", msg.Item,
169248
"{purpose}", msg.Purpose,
170249
"{source}", source,
171250
"{target}", target,
172251
)
173252

174-
// Select a random template, avoiding recent duplicates
175-
var selectedTemplate string
176-
shuffledTemplates := make([]string, len(topicTemplates))
177-
copy(shuffledTemplates, topicTemplates)
178-
rand.Shuffle(len(shuffledTemplates), func(i, j int) {
179-
shuffledTemplates[i], shuffledTemplates[j] = shuffledTemplates[j], shuffledTemplates[i]
180-
})
181-
182-
for _, tmpl := range shuffledTemplates {
183-
potentialMessage := replacer.Replace(tmpl)
184-
if !t.history.Contains(potentialMessage) {
185-
selectedTemplate = tmpl
253+
var chosen string
254+
for _, tmpl := range bestCandidates {
255+
candidateMsg := replacerForCheck.Replace(tmpl)
256+
if !t.history.Contains(candidateMsg) {
257+
chosen = tmpl
186258
break
187259
}
188260
}
189261

190-
if selectedTemplate == "" {
191-
// If all templates are recent duplicates, just pick a random one
192-
selectedTemplate = topicTemplates[rand.IntN(len(topicTemplates))]
262+
// If all best candidates are in history, pick a random best candidate
263+
if chosen == "" {
264+
if len(bestCandidates) > 0 {
265+
chosen = bestCandidates[rand.Intn(len(bestCandidates))]
266+
} else {
267+
// final fallback: random from topicTemplates
268+
chosen = topicTemplates[rand.Intn(len(topicTemplates))]
269+
}
193270
}
194271

195-
formattedMsg := replacer.Replace(selectedTemplate)
272+
// Final replacement
273+
replacer := strings.NewReplacer(
274+
"{topic}", msg.Topic,
275+
"{item}", msg.Item,
276+
"{purpose}", msg.Purpose,
277+
"{source}", source,
278+
"{target}", target,
279+
)
280+
281+
formattedMsg := replacer.Replace(chosen)
196282

197-
// Handle scope
283+
// If scope exists, prefer replacing the topic scope pattern when present
198284
if msg.Scope != "" {
285+
// try common patterns
199286
formattedMsg = strings.Replace(formattedMsg, "("+msg.Topic+")", "("+msg.Scope+")", 1)
200287
}
201288

202289
return formattedMsg, nil
203290
}
291+
292+
// DebugInfo returns the resolved action key and the candidate templates for a CommitMessage
293+
func (t *Templater) DebugInfo(msg *analyzer.CommitMessage) (string, []string) {
294+
// same mapping as in GetMessage
295+
actionMap := map[string]string{
296+
"feat": "A",
297+
"add": "A",
298+
"fix": "M",
299+
"bugfix": "M",
300+
"refactor": "R",
301+
"chore": "D",
302+
"test": "M",
303+
"docs": "DOC",
304+
"ci": "M",
305+
"perf": "M",
306+
"style": "MISC",
307+
"build": "MISC",
308+
}
309+
310+
actionLower := strings.ToLower(msg.Action)
311+
var actionKey string
312+
if key, ok := actionMap[actionLower]; ok {
313+
actionKey = key
314+
} else if len(msg.Action) == 1 {
315+
actionKey = strings.ToUpper(msg.Action)
316+
} else {
317+
actionKey = "MISC"
318+
}
319+
320+
actionTemplates, ok := t.templates[actionKey]
321+
if !ok {
322+
fallbackActions := []string{"DOC", "A", "M", "R", "D", "MISC"}
323+
for _, fb := range fallbackActions {
324+
if templates, exists := t.templates[fb]; exists {
325+
actionTemplates = templates
326+
ok = true
327+
break
328+
}
329+
}
330+
if !ok {
331+
return actionKey, nil
332+
}
333+
}
334+
335+
normalizedTopic := strings.ToLower(strings.TrimSpace(msg.Topic))
336+
var topicTemplates []string
337+
if normalizedTopic != "" {
338+
if templates, exists := actionTemplates[normalizedTopic]; exists && len(templates) > 0 {
339+
topicTemplates = templates
340+
}
341+
}
342+
if len(topicTemplates) == 0 {
343+
for topic, templates := range actionTemplates {
344+
if topic == "_default" {
345+
continue
346+
}
347+
tname := strings.ToLower(topic)
348+
if normalizedTopic != "" && (strings.Contains(tname, normalizedTopic) || strings.Contains(normalizedTopic, tname)) {
349+
topicTemplates = templates
350+
break
351+
}
352+
}
353+
}
354+
if len(topicTemplates) == 0 {
355+
if defaults, exists := actionTemplates["_default"]; exists && len(defaults) > 0 {
356+
topicTemplates = defaults
357+
}
358+
}
359+
360+
return actionKey, topicTemplates
361+
}

0 commit comments

Comments
 (0)