@@ -15,6 +15,11 @@ type analysis struct {
1515 commitType string
1616 addedFuncs []string
1717 removedFuncs []string
18+ renamedFuncs [][2 ]string
19+ addedVars []string
20+ removedVars []string
21+ renamedFiles [][2 ]string
22+ branchHint string
1823 patterns []string
1924 isMigration bool
2025 isDepUpdate bool
@@ -23,6 +28,7 @@ type analysis struct {
2328 isConfigOnly bool
2429 isDeleteOnly bool
2530 isNewFiles bool
31+ isRenameOnly bool
2632}
2733
2834var (
6773 reRmFuncPy = regexp .MustCompile (`^-def\s+(\w+)\s*\(` )
6874 reRmFuncRs = regexp .MustCompile (`^-(?:pub\s+)?fn\s+(\w+)\s*[<(]` )
6975
76+ reVarGoAdd = regexp .MustCompile (`^\+(?:var|const)\s+(\w{2,})\b` )
77+ reVarGoRm = regexp .MustCompile (`^-(?:var|const)\s+(\w{2,})\b` )
78+ reVarJSAdd = regexp .MustCompile (`^\+(?:const|let|var)\s+(\w{2,})\s*[=;]` )
79+ reVarJSRm = regexp .MustCompile (`^-(?:const|let|var)\s+(\w{2,})\s*[=;]` )
80+ reVarPyAdd = regexp .MustCompile (`^\+([A-Z][A-Z0-9_]{2,})\s*=` )
81+ reVarPyRm = regexp .MustCompile (`^-([A-Z][A-Z0-9_]{2,})\s*=` )
82+
7083 reErrorHandling = regexp .MustCompile (`^\+.*(?:err|error|Error|exception|Exception|catch|rescue)\b` )
7184 reLogging = regexp .MustCompile (`^\+.*(?:log\.|logger\.|console\.log|fmt\.Print|println!|logging\.)` )
7285 reRmLogging = regexp .MustCompile (`^-.*(?:console\.log|fmt\.Print|println!|log\.Debug)` )
@@ -75,13 +88,19 @@ var (
7588)
7689
7790func analyzeDiff (diff string ) analysis {
91+ return analyzeDiffWithBranch (diff , "" )
92+ }
93+
94+ func analyzeDiffWithBranch (diff string , branch string ) analysis {
7895 a := analysis {}
96+ a .branchHint = branch
7997
8098 lines := strings .Split (diff , "\n " )
8199 currentFile := ""
82100 isDeleted := false
83101 isNew := false
84102 isRename := false
103+ renameFrom := ""
85104
86105 for _ , line := range lines {
87106 if strings .HasPrefix (line , "deleted file mode" ) {
@@ -105,33 +124,36 @@ func analyzeDiff(diff string) analysis {
105124 isDeleted = false
106125 isNew = false
107126 isRename = false
127+ renameFrom = ""
108128 currentFile = parseDiffGitPath (line )
109129 continue
110130 }
111131
112- if strings .HasPrefix (line , "rename to " ) {
113- isRename = true
114- path := strings .TrimPrefix (line , "rename to " )
115- path = strings .TrimSpace (path )
116- a .filesModified = appendUnique (a .filesModified , path )
117- currentFile = path
132+ if strings .HasPrefix (line , "rename from " ) {
133+ renameFrom = strings .TrimSpace (strings .TrimPrefix (line , "rename from " ))
118134 continue
119135 }
120- if strings .HasPrefix (line , "rename from " ) {
136+
137+ if strings .HasPrefix (line , "rename to " ) {
138+ isRename = true
139+ renameTo := strings .TrimSpace (strings .TrimPrefix (line , "rename to " ))
140+ a .filesModified = appendUnique (a .filesModified , renameTo )
141+ if renameFrom != "" {
142+ a .renamedFiles = append (a .renamedFiles , [2 ]string {renameFrom , renameTo })
143+ }
144+ currentFile = renameTo
121145 continue
122146 }
123147
124148 if strings .HasPrefix (line , "--- a/" ) && isDeleted {
125- path := strings .TrimPrefix (line , "--- a/" )
126- path = strings .TrimSpace (path )
149+ path := strings .TrimSpace (strings .TrimPrefix (line , "--- a/" ))
127150 a .filesDeleted = appendUnique (a .filesDeleted , path )
128151 currentFile = path
129152 continue
130153 }
131154
132155 if strings .HasPrefix (line , "+++ b/" ) {
133- currentFile = strings .TrimPrefix (line , "+++ b/" )
134- currentFile = strings .TrimSpace (currentFile )
156+ currentFile = strings .TrimSpace (strings .TrimPrefix (line , "+++ b/" ))
135157 if isDeleted || isRename {
136158 continue
137159 }
@@ -161,6 +183,13 @@ func analyzeDiff(diff string) analysis {
161183 a .removedFuncs = appendUnique (a .removedFuncs , funcs )
162184 }
163185
186+ if v := extractVarName (line , currentFile , true ); v != "" {
187+ a .addedVars = appendUnique (a .addedVars , v )
188+ }
189+ if v := extractVarName (line , currentFile , false ); v != "" {
190+ a .removedVars = appendUnique (a .removedVars , v )
191+ }
192+
164193 if reErrorHandling .MatchString (line ) {
165194 a .patterns = appendUnique (a .patterns , "error-handling" )
166195 }
@@ -186,12 +215,19 @@ func analyzeDiff(diff string) analysis {
186215 a .filesDeleted = appendUnique (a .filesDeleted , currentFile )
187216 }
188217
218+ detectRenamedFuncs (& a )
219+
189220 a .filesChanged = len (a .filesAdded ) + len (a .filesDeleted ) + len (a .filesModified )
190221
191222 if len (a .filesDeleted ) > 0 && len (a .filesAdded ) == 0 && len (a .filesModified ) == 0 {
192223 a .isDeleteOnly = true
193224 }
194225
226+ if len (a .renamedFiles ) > 0 && len (a .filesAdded ) == 0 && len (a .filesDeleted ) == 0 &&
227+ len (a .addedFuncs ) == 0 && len (a .removedFuncs ) == 0 {
228+ a .isRenameOnly = true
229+ }
230+
195231 a .primaryScope = inferScope (a )
196232 a .commitType = inferType (a )
197233
@@ -243,6 +279,74 @@ func categorizeFile(a *analysis, path string) {
243279 a .isConfigOnly = false
244280}
245281
282+ func extractVarName (line , file string , added bool ) string {
283+ ext := strings .ToLower (filepath .Ext (file ))
284+ var re * regexp.Regexp
285+
286+ if added {
287+ switch ext {
288+ case ".go" :
289+ re = reVarGoAdd
290+ case ".js" , ".ts" , ".jsx" , ".tsx" :
291+ re = reVarJSAdd
292+ case ".py" :
293+ re = reVarPyAdd
294+ }
295+ } else {
296+ switch ext {
297+ case ".go" :
298+ re = reVarGoRm
299+ case ".js" , ".ts" , ".jsx" , ".tsx" :
300+ re = reVarJSRm
301+ case ".py" :
302+ re = reVarPyRm
303+ }
304+ }
305+
306+ if re == nil {
307+ return ""
308+ }
309+ if m := re .FindStringSubmatch (line ); len (m ) > 1 {
310+ name := m [1 ]
311+ if len (name ) <= 1 {
312+ return ""
313+ }
314+ return name
315+ }
316+ return ""
317+ }
318+
319+ func detectRenamedFuncs (a * analysis ) {
320+ if len (a .addedFuncs ) == 0 || len (a .removedFuncs ) == 0 {
321+ return
322+ }
323+ used := map [string ]bool {}
324+ for _ , added := range a .addedFuncs {
325+ for _ , removed := range a .removedFuncs {
326+ if used [removed ] {
327+ continue
328+ }
329+ if isSimilarName (removed , added ) {
330+ a .renamedFuncs = append (a .renamedFuncs , [2 ]string {removed , added })
331+ used [removed ] = true
332+ break
333+ }
334+ }
335+ }
336+ }
337+
338+ func isSimilarName (a , b string ) bool {
339+ al := strings .ToLower (a )
340+ bl := strings .ToLower (b )
341+ if strings .Contains (al , bl ) || strings .Contains (bl , al ) {
342+ return true
343+ }
344+ if len (al ) > 3 && len (bl ) > 3 && al [:3 ] == bl [:3 ] {
345+ return true
346+ }
347+ return false
348+ }
349+
246350func extractFuncName (line , file string , added bool ) string {
247351 ext := strings .ToLower (filepath .Ext (file ))
248352 var patterns []* regexp.Regexp
@@ -332,6 +436,28 @@ func inferScope(a analysis) string {
332436}
333437
334438func inferType (a analysis ) string {
439+ if a .branchHint != "" {
440+ hint := strings .ToLower (a .branchHint )
441+ if strings .HasPrefix (hint , "fix/" ) || strings .HasPrefix (hint , "bugfix/" ) || strings .HasPrefix (hint , "hotfix/" ) {
442+ return "fix"
443+ }
444+ if strings .HasPrefix (hint , "feat/" ) || strings .HasPrefix (hint , "feature/" ) {
445+ return "feat"
446+ }
447+ if strings .HasPrefix (hint , "docs/" ) {
448+ return "docs"
449+ }
450+ if strings .HasPrefix (hint , "refactor/" ) || strings .HasPrefix (hint , "chore/" ) {
451+ return "refactor"
452+ }
453+ if strings .HasPrefix (hint , "test/" ) {
454+ return "test"
455+ }
456+ }
457+
458+ if a .isRenameOnly {
459+ return "refactor"
460+ }
335461 if a .isDeleteOnly {
336462 return "chore"
337463 }
@@ -356,6 +482,9 @@ func inferType(a analysis) string {
356482 if contains (a .patterns , "remove-debug-logs" ) && len (a .filesModified ) <= 3 {
357483 return "chore"
358484 }
485+ if len (a .renamedFuncs ) > 0 && len (a .addedFuncs ) == len (a .renamedFuncs ) {
486+ return "refactor"
487+ }
359488 if len (a .addedFuncs ) > 0 && len (a .removedFuncs ) == 0 {
360489 return "feat"
361490 }
0 commit comments