Skip to content

Commit 5cde752

Browse files
authored
Merge pull request #25 from andev0x/optimization/func
feat(templater): improve template selection with project-aware context
2 parents 959c686 + 532957b commit 5cde752

2 files changed

Lines changed: 173 additions & 28 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ dist/
3939
build/
4040
/bin/
4141
.commit_suggest_history.json
42+
.gitmit.json
43+
4244

4345

4446
# Env

internal/templater/templater.go

Lines changed: 171 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -110,35 +110,43 @@ func NewTemplater(templateFile string, hist *history.CommitHistory) (*Templater,
110110

111111
// GetMessage selects and formats a commit message
112112
func (t *Templater) GetMessage(msg *analyzer.CommitMessage) (string, error) {
113-
// Map analyzer action names (feat, fix, refactor, chore, docs, test, etc.)
114-
// to the template groups used in templates.json (A, M, D, R, DOC, MISC)
115-
actionMap := map[string]string{
116-
"feat": "A",
117-
"add": "A",
118-
"fix": "M",
119-
"bugfix": "M",
120-
"refactor": "R",
121-
"chore": "D",
122-
"test": "M",
123-
"docs": "DOC",
124-
"ci": "M",
125-
"perf": "M",
126-
"style": "MISC",
127-
"build": "MISC",
128-
"security": "SECURITY",
129-
}
130-
131-
// Normalize and resolve action group
132-
actionLower := strings.ToLower(msg.Action)
113+
// Check if this is a special file that needs dedicated handling
114+
specialGroup := resolveSpecialFile(msg)
133115
var actionKey string
134-
if key, ok := actionMap[actionLower]; ok {
135-
actionKey = key
136-
} else if len(msg.Action) == 1 {
137-
// Already a single-letter action like A/M/D/R
138-
actionKey = strings.ToUpper(msg.Action)
116+
117+
if specialGroup != "" {
118+
// Force use of special template group
119+
actionKey = specialGroup
139120
} else {
140-
// fallback to MISC
141-
actionKey = "MISC"
121+
// Map analyzer action names (feat, fix, refactor, chore, docs, test, etc.)
122+
// to the template groups used in templates.json (A, M, D, R, DOC, MISC)
123+
actionMap := map[string]string{
124+
"feat": "A",
125+
"add": "A",
126+
"fix": "M",
127+
"bugfix": "M",
128+
"refactor": "R",
129+
"chore": "D",
130+
"test": "M",
131+
"docs": "DOC",
132+
"ci": "M",
133+
"perf": "M",
134+
"style": "MISC",
135+
"build": "MISC",
136+
"security": "SECURITY",
137+
}
138+
139+
// Normalize and resolve action group
140+
actionLower := strings.ToLower(msg.Action)
141+
if key, ok := actionMap[actionLower]; ok {
142+
actionKey = key
143+
} else if len(msg.Action) == 1 {
144+
// Already a single-letter action like A/M/D/R
145+
actionKey = strings.ToUpper(msg.Action)
146+
} else {
147+
// fallback to MISC
148+
actionKey = "MISC"
149+
}
142150
}
143151

144152
actionTemplates, ok := t.templates[actionKey]
@@ -323,6 +331,23 @@ func (t *Templater) GetMessage(msg *analyzer.CommitMessage) (string, error) {
323331
score -= 1.0
324332
}
325333

334+
// Bonus for templates that match project scope
335+
projectScope := inferProjectScope(msg)
336+
if projectScope != "" {
337+
templateLower := strings.ToLower(tmpl)
338+
scopeLower := strings.ToLower(projectScope)
339+
340+
// Direct scope mention in template
341+
if strings.Contains(templateLower, scopeLower) {
342+
score += 1.5
343+
}
344+
345+
// Topic placeholder with meaningful scope
346+
if strings.Contains(tmpl, "{topic}") && msg.Topic != "" && msg.Topic != "core" {
347+
score += 1.0
348+
}
349+
}
350+
326351
// Small randomness for variety (0-0.5)
327352
score += rand.Float64() * 0.5
328353

@@ -387,12 +412,22 @@ func (t *Templater) GetMessage(msg *analyzer.CommitMessage) (string, error) {
387412

388413
formattedMsg := replacer.Replace(chosen)
389414

390-
// If scope exists, prefer replacing the topic scope pattern when present
415+
// Infer and apply project scope for better context
416+
projectScope := inferProjectScope(msg)
417+
if projectScope != "" {
418+
// Try common scope patterns
419+
formattedMsg = strings.Replace(formattedMsg, "("+msg.Topic+")", "("+projectScope+")", 1)
420+
}
421+
422+
// If scope exists in message, prefer replacing the topic scope pattern when present
391423
if msg.Scope != "" {
392424
// try common patterns
393425
formattedMsg = strings.Replace(formattedMsg, "("+msg.Topic+")", "("+msg.Scope+")", 1)
394426
}
395427

428+
// Clean and normalize the final message
429+
formattedMsg = cleanFinalMessage(formattedMsg)
430+
396431
return formattedMsg, nil
397432
}
398433

@@ -482,6 +517,7 @@ func (t *Templater) GetSuggestions(msg *analyzer.CommitMessage, maxSuggestions i
482517
}
483518

484519
message := replacer.Replace(s.template)
520+
message = cleanFinalMessage(message) // Clean the message
485521

486522
// Skip if we've seen this exact message or it's in history
487523
if usedMessages[message] || t.history.Contains(message) {
@@ -500,6 +536,7 @@ func (t *Templater) GetSuggestions(msg *analyzer.CommitMessage, maxSuggestions i
500536
}
501537

502538
message := replacer.Replace(s.template)
539+
message = cleanFinalMessage(message) // Clean the message
503540
if !usedMessages[message] {
504541
suggestions = append(suggestions, message)
505542
usedMessages[message] = true
@@ -589,6 +626,20 @@ func (t *Templater) scoreTemplate(template string, msg *analyzer.CommitMessage)
589626
// Base score
590627
score += 1.0
591628

629+
// PENALTY MECHANISM: Heavy penalty for templates requiring {item} but no data available
630+
if strings.Contains(template, "{item}") {
631+
// Check if we have any item data
632+
hasItem := msg.Item != ""
633+
hasDetectedStructures := len(msg.DetectedFunctions) > 0 ||
634+
len(msg.DetectedStructs) > 0 ||
635+
len(msg.DetectedMethods) > 0
636+
637+
if !hasItem && !hasDetectedStructures {
638+
// Deduct 50 points - this template will never be selected
639+
score -= 50.0
640+
}
641+
}
642+
592643
// Bonus for templates that match detected patterns
593644
for _, pattern := range msg.ChangePatterns {
594645
if strings.Contains(template, pattern) ||
@@ -641,6 +692,23 @@ func (t *Templater) scoreTemplate(template string, msg *analyzer.CommitMessage)
641692
score += 1.0
642693
}
643694

695+
// Bonus for templates that match the project scope
696+
projectScope := inferProjectScope(msg)
697+
if projectScope != "" {
698+
templateLower := strings.ToLower(template)
699+
scopeLower := strings.ToLower(projectScope)
700+
701+
// Direct scope mention in template
702+
if strings.Contains(templateLower, scopeLower) {
703+
score += 2.0
704+
}
705+
706+
// Scope matches topic placeholder usage
707+
if strings.Contains(template, "{topic}") && msg.Topic != "" {
708+
score += 1.0
709+
}
710+
}
711+
644712
return score
645713
}
646714

@@ -694,6 +762,7 @@ func (t *Templater) GetAlternativeSuggestion(msg *analyzer.CommitMessage, usedSu
694762

695763
for _, tmpl := range candidates {
696764
message := replacer.Replace(tmpl)
765+
message = cleanFinalMessage(message) // Clean the message
697766

698767
// Skip if already used
699768
if usedSuggestions[message] {
@@ -726,6 +795,7 @@ func (t *Templater) GetAlternativeSuggestion(msg *analyzer.CommitMessage, usedSu
726795
// If all have been used, reset and try again with lower standards
727796
for _, tmpl := range candidates {
728797
message := replacer.Replace(tmpl)
798+
message = cleanFinalMessage(message) // Clean the message
729799
score := t.scoreTemplate(tmpl, msg) + rand.Float64()
730800
scored = append(scored, scoredTemplate{tmpl, message, score})
731801
}
@@ -743,6 +813,79 @@ func (t *Templater) GetAlternativeSuggestion(msg *analyzer.CommitMessage, usedSu
743813
return scored[0].message, nil
744814
}
745815

816+
// resolveSpecialFile detects special files like LICENSE, COPYING, .md docs, etc.
817+
// Returns the special template group to use, or empty string if not a special file
818+
func resolveSpecialFile(msg *analyzer.CommitMessage) string {
819+
// Check if all files are markdown documentation files
820+
if msg.IsDocsOnly {
821+
return "DOC"
822+
}
823+
824+
// Check for .md file extensions - these are documentation
825+
for _, ext := range msg.FileExtensions {
826+
if ext == "md" {
827+
return "DOC"
828+
}
829+
}
830+
831+
// Define special file patterns
832+
specialFiles := map[string]string{
833+
"license": "LICENSE",
834+
"copying": "LICENSE",
835+
"copyright": "LICENSE",
836+
"readme": "DOC",
837+
"changelog": "DOC",
838+
"authors": "DOC",
839+
"contributors": "DOC",
840+
}
841+
842+
// Check topic and item for special file indicators
843+
topicLower := strings.ToLower(msg.Topic)
844+
itemLower := strings.ToLower(msg.Item)
845+
846+
for pattern, group := range specialFiles {
847+
if strings.Contains(topicLower, pattern) || strings.Contains(itemLower, pattern) {
848+
return group
849+
}
850+
}
851+
852+
return ""
853+
}
854+
855+
// cleanFinalMessage post-processes the commit message to normalize format
856+
func cleanFinalMessage(message string) string {
857+
// Normalize empty parentheses patterns like "feat():" to "feat:"
858+
message = strings.ReplaceAll(message, "():", ":")
859+
message = strings.ReplaceAll(message, "( ):", ":")
860+
message = strings.ReplaceAll(message, "( ):", ":")
861+
862+
// Remove excessive whitespace
863+
message = strings.TrimSpace(message)
864+
865+
// Normalize multiple spaces to single space
866+
for strings.Contains(message, " ") {
867+
message = strings.ReplaceAll(message, " ", " ")
868+
}
869+
870+
return message
871+
}
872+
873+
// inferProjectScope extracts the project scope from file paths
874+
// This helps match commit messages to the affected module/component
875+
func inferProjectScope(msg *analyzer.CommitMessage) string {
876+
// If the analyzer already detected a scope, use it
877+
if msg.Scope != "" {
878+
return msg.Scope
879+
}
880+
881+
// Use the topic as the scope if it's meaningful
882+
if msg.Topic != "" && msg.Topic != "core" && msg.Topic != "." {
883+
return msg.Topic
884+
}
885+
886+
return ""
887+
}
888+
746889
// calculateSimilarity returns a similarity score between 0.0 (completely different) and 1.0 (identical)
747890
// Uses a hybrid approach combining:
748891
// - Word-level Jaccard similarity (60% weight) - measures semantic overlap

0 commit comments

Comments
 (0)