Skip to content

Commit 4cf6484

Browse files
committed
chore: release v0.1.6
1 parent 6d668d5 commit 4cf6484

6 files changed

Lines changed: 773 additions & 44 deletions

File tree

analyze.go

Lines changed: 140 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -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

2834
var (
@@ -67,6 +73,13 @@ var (
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

7790
func 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+
246350
func 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

334438
func 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
}

branch.go

Lines changed: 99 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,11 @@ func runBranch() {
1515

1616
args := os.Args[2:]
1717
if len(args) == 0 {
18-
runBranchSwitch()
18+
runBranchMenu()
1919
return
2020
}
2121

2222
switch args[0] {
23-
case "switch", "sw":
24-
runBranchSwitch()
2523
case "create", "new":
2624
runBranchCreate()
2725
case "ls", "list":
@@ -30,11 +28,42 @@ func runBranch() {
3028
runBranchDelete()
3129
default:
3230
fmt.Fprintf(os.Stderr, " unknown branch command: %s\n", args[0])
33-
fmt.Fprintf(os.Stderr, " usage: commitdog branch [switch|create|ls|delete]\n")
31+
fmt.Fprintf(os.Stderr, " usage: commitdog branch [create|ls|delete]\n")
3432
os.Exit(1)
3533
}
3634
}
3735

36+
func runBranchMenu() {
37+
fmt.Println()
38+
fmt.Println(" branch:")
39+
fmt.Println()
40+
fmt.Println(" 1 switch")
41+
fmt.Println(" 2 create new")
42+
fmt.Println(" 3 delete")
43+
fmt.Println()
44+
fmt.Printf(" [1-3] pick, [q] quit › ")
45+
46+
for {
47+
input := readLine()
48+
switch input {
49+
case "1":
50+
runBranchSwitch()
51+
return
52+
case "2":
53+
runBranchCreate()
54+
return
55+
case "3":
56+
runBranchDelete()
57+
return
58+
case "q", "quit", "exit":
59+
fmt.Println(" aborted.")
60+
return
61+
default:
62+
fmt.Printf(" 1, 2, 3, or q › ")
63+
}
64+
}
65+
}
66+
3867
func runBranchSwitch() {
3968
all, err := getAllBranches()
4069
if err != nil {
@@ -147,23 +176,77 @@ func askBranchName(all []string, current string) string {
147176

148177
func runBranchCreate() {
149178
fmt.Println()
150-
fmt.Printf(" new branch name › ")
151179

152-
name := ""
153-
for {
154-
input := strings.TrimSpace(readLine())
155-
if input == "" {
156-
fmt.Printf(" name cannot be empty › ")
157-
continue
180+
var suggestions []string
181+
diff, err := getStagedDiff()
182+
if err == nil && diff != "" {
183+
a := analyzeDiffWithBranch(diff, getCurrentBranch())
184+
suggestions = generateBranchNameSuggestions(a)
185+
}
186+
187+
if len(suggestions) > 0 {
188+
fmt.Println(" suggested branch names:")
189+
fmt.Println()
190+
for i, s := range suggestions {
191+
fmt.Printf(" %d %s\n", i+1, s)
158192
}
159-
if !isSafeGitRef(input) {
160-
fmt.Printf(" invalid branch name (use letters, numbers, - _ /) › ")
161-
continue
193+
fmt.Println()
194+
fmt.Printf(" [1-%d] pick, [e] enter name › ", len(suggestions))
195+
196+
input := readLine()
197+
picked := false
198+
name := ""
199+
for i, s := range suggestions {
200+
if input == fmt.Sprintf("%d", i+1) {
201+
name = s
202+
picked = true
203+
break
204+
}
162205
}
163-
name = input
164-
break
206+
if !picked {
207+
fmt.Printf(" new branch name › ")
208+
for {
209+
var raw string
210+
if input != "e" && input != "" {
211+
raw = input
212+
input = ""
213+
} else {
214+
raw = strings.TrimSpace(readLine())
215+
}
216+
if raw == "" {
217+
fmt.Printf(" name cannot be empty › ")
218+
continue
219+
}
220+
if !isSafeGitRef(raw) {
221+
fmt.Printf(" invalid branch name (use letters, numbers, - _ /) › ")
222+
continue
223+
}
224+
name = raw
225+
break
226+
}
227+
}
228+
doCreateBranch(name)
229+
} else {
230+
fmt.Printf(" new branch name › ")
231+
name := ""
232+
for {
233+
input := strings.TrimSpace(readLine())
234+
if input == "" {
235+
fmt.Printf(" name cannot be empty › ")
236+
continue
237+
}
238+
if !isSafeGitRef(input) {
239+
fmt.Printf(" invalid branch name (use letters, numbers, - _ /) › ")
240+
continue
241+
}
242+
name = input
243+
break
244+
}
245+
doCreateBranch(name)
165246
}
247+
}
166248

249+
func doCreateBranch(name string) {
167250
current := getCurrentBranch()
168251
fmt.Printf(" base branch? [enter = %s] › ", current)
169252
base := strings.TrimSpace(readLine())

0 commit comments

Comments
 (0)