Skip to content

Commit e194f1f

Browse files
committed
feat(ui): add branch auto-detect feature
- implement branch type detection logic in commit model - add tests for new selector model with initial types - enhance flow by providing initial context based on branch configuration - update README with documentation and examples - add new configuration option `branch-auto-detect` - modify `ParseBranchType` function to extract commit types from branch names - add test file for `ParseBranchType` function Implementation #28 Signed-off-by: kovacs <mritd@linux.com>
1 parent a8ec402 commit e194f1f

7 files changed

Lines changed: 226 additions & 6 deletions

File tree

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,9 @@ All settings are configured via `~/.gitconfig` under the `[gitflow]` section.
143143

144144
# SSH strict host key checking (default: false)
145145
ssh-strict-host-key = false
146+
147+
# Auto-detect commit type from branch name (default: false)
148+
branch-auto-detect = true
146149
```
147150

148151
### Configuration Reference
@@ -165,6 +168,7 @@ All settings are configured via `~/.gitconfig` under the `[gitflow]` section.
165168
| `llm-commit-prompt-bilingual` | Custom bilingual commit prompt | - |
166169
| `lucky-commit-prefix` | Lucky commit hex prefix (max 12 chars) | - |
167170
| `ssh-strict-host-key` | SSH strict host key checking | `false` |
171+
| `branch-auto-detect` | Auto-detect commit type from branch name | `false` |
168172

169173
### Auto Generate (AI)
170174

@@ -232,6 +236,34 @@ git ci
232236
- Maximum prefix length is 12 characters
233237
- Press Ctrl+C during search to skip and keep original commit
234238

239+
### Branch Auto-Detection
240+
241+
Automatically pre-select the commit type based on your current branch name:
242+
243+
```bash
244+
# Enable the feature
245+
git config --global gitflow.branch-auto-detect true
246+
247+
# Now when you're on a branch like "feat/login" or "feature-new-ui"
248+
git ci # Cursor will auto-select "feat" type
249+
```
250+
251+
**Supported branch prefixes:**
252+
253+
| Branch Prefix | Commit Type |
254+
|---------------|-------------|
255+
| `feat`, `feature` | feat |
256+
| `fix`, `bugfix`, `bug` | fix |
257+
| `docs`, `doc`, `document` | docs |
258+
| `style` | style |
259+
| `refactor`, `refact` | refactor |
260+
| `test`, `testing` | test |
261+
| `chore` | chore |
262+
| `perf`, `performance` | perf |
263+
| `hotfix` | hotfix |
264+
265+
Supports separators: `/`, `-`, `_` (e.g., `feat/login`, `fix-bug-123`, `docs_readme`)
266+
235267
## Uninstall
236268

237269
### Homebrew

config/gitconfig.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const (
2929
GitConfigLLMCommitPromptBilingual = "llm-commit-prompt-bilingual"
3030
GitConfigLuckyCommitPrefix = "lucky-commit-prefix"
3131
GitConfigSSHStrictHostKey = "ssh-strict-host-key"
32+
GitConfigBranchAutoDetect = "branch-auto-detect"
3233
)
3334

3435
// gitConfig runs git config --get and returns the value.

internal/git/branch.go

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,71 @@
11
package git
22

3-
import "fmt"
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/mritd/gitflow-toolkit/v3/consts"
8+
)
9+
10+
// branchAliases maps branch prefixes to commit types.
11+
var branchAliases = map[string]string{
12+
// feat
13+
"feat": consts.Feat,
14+
"feature": consts.Feat,
15+
// fix
16+
"fix": consts.Fix,
17+
"bugfix": consts.Fix,
18+
"bug": consts.Fix,
19+
// docs
20+
"docs": consts.Docs,
21+
"doc": consts.Docs,
22+
"document": consts.Docs,
23+
// style
24+
"style": consts.Style,
25+
// refactor
26+
"refactor": consts.Refactor,
27+
"refact": consts.Refactor,
28+
// test
29+
"test": consts.Test,
30+
"testing": consts.Test,
31+
// chore
32+
"chore": consts.Chore,
33+
// perf
34+
"perf": consts.Perf,
35+
"performance": consts.Perf,
36+
// hotfix
37+
"hotfix": consts.Hotfix,
38+
}
39+
40+
// ParseBranchType extracts the commit type from a branch name.
41+
// Supports formats: type/name, type-name, type_name
42+
// Returns empty string if no match found.
43+
func ParseBranchType(branch string) string {
44+
if branch == "" {
45+
return ""
46+
}
47+
48+
// Find the prefix before /, -, or _
49+
var prefix string
50+
for i, r := range branch {
51+
if r == '/' || r == '-' || r == '_' {
52+
prefix = branch[:i]
53+
break
54+
}
55+
}
56+
57+
// If no separator found, the whole branch name might be the type
58+
if prefix == "" {
59+
prefix = branch
60+
}
61+
62+
prefix = strings.ToLower(prefix)
63+
if commitType, ok := branchAliases[prefix]; ok {
64+
return commitType
65+
}
66+
67+
return ""
68+
}
469

570
// CreateBranch creates a new branch with the given name.
671
func CreateBranch(name string) (string, error) {

internal/git/branch_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package git
2+
3+
import (
4+
"testing"
5+
6+
"github.com/mritd/gitflow-toolkit/v3/consts"
7+
)
8+
9+
func TestParseBranchType(t *testing.T) {
10+
tests := []struct {
11+
name string
12+
branch string
13+
expected string
14+
}{
15+
// Standard formats with /
16+
{"feat with slash", "feat/login", consts.Feat},
17+
{"fix with slash", "fix/bug-123", consts.Fix},
18+
{"docs with slash", "docs/readme", consts.Docs},
19+
{"style with slash", "style/format", consts.Style},
20+
{"refactor with slash", "refactor/cleanup", consts.Refactor},
21+
{"test with slash", "test/unit", consts.Test},
22+
{"chore with slash", "chore/deps", consts.Chore},
23+
{"perf with slash", "perf/optimize", consts.Perf},
24+
{"hotfix with slash", "hotfix/urgent", consts.Hotfix},
25+
26+
// Aliases with /
27+
{"feature alias", "feature/login", consts.Feat},
28+
{"bugfix alias", "bugfix/issue-42", consts.Fix},
29+
{"bug alias", "bug/crash", consts.Fix},
30+
{"doc alias", "doc/api", consts.Docs},
31+
{"document alias", "document/guide", consts.Docs},
32+
{"refact alias", "refact/module", consts.Refactor},
33+
{"testing alias", "testing/e2e", consts.Test},
34+
{"performance alias", "performance/cache", consts.Perf},
35+
36+
// Formats with -
37+
{"feat with dash", "feat-login", consts.Feat},
38+
{"fix with dash", "fix-bug-123", consts.Fix},
39+
{"feature with dash", "feature-new-ui", consts.Feat},
40+
{"bugfix with dash", "bugfix-issue", consts.Fix},
41+
42+
// Formats with _
43+
{"feat with underscore", "feat_login", consts.Feat},
44+
{"fix with underscore", "fix_bug_123", consts.Fix},
45+
46+
// Case insensitivity
47+
{"uppercase FEAT", "FEAT/login", consts.Feat},
48+
{"mixed case Feature", "Feature/login", consts.Feat},
49+
50+
// Non-matching branches
51+
{"main branch", "main", ""},
52+
{"master branch", "master", ""},
53+
{"develop branch", "develop", ""},
54+
{"release branch", "release/v1.0", ""},
55+
{"random branch", "random-branch", ""},
56+
{"empty string", "", ""},
57+
}
58+
59+
for _, tt := range tests {
60+
t.Run(tt.name, func(t *testing.T) {
61+
result := ParseBranchType(tt.branch)
62+
if result != tt.expected {
63+
t.Errorf("ParseBranchType(%q) = %q, want %q", tt.branch, result, tt.expected)
64+
}
65+
})
66+
}
67+
}

internal/ui/commit/model.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
tea "github.com/charmbracelet/bubbletea"
88
"github.com/charmbracelet/lipgloss"
99

10+
"github.com/mritd/gitflow-toolkit/v3/config"
1011
"github.com/mritd/gitflow-toolkit/v3/internal/git"
1112
"github.com/mritd/gitflow-toolkit/v3/internal/ui/common"
1213
)
@@ -26,8 +27,16 @@ type Result struct {
2627
func Run(luckyPrefix string) Result {
2728
var result Result
2829

30+
// Detect commit type from branch name if enabled
31+
var initialType string
32+
if config.GetBool(config.GitConfigBranchAutoDetect, false) {
33+
if branch, err := git.CurrentBranch(); err == nil {
34+
initialType = git.ParseBranchType(branch)
35+
}
36+
}
37+
2938
// Step 1: Select commit type or AI generate
30-
choice, err := runSelector()
39+
choice, err := runSelector(initialType)
3140
if err != nil {
3241
if errors.Is(err, errUserAborted) {
3342
result.Cancelled = true

internal/ui/commit/model_test.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@ func TestNewInputsModel(t *testing.T) {
8888
}
8989

9090
func TestNewSelectorModel(t *testing.T) {
91-
m := newSelectorModel()
91+
// Test without initial type
92+
m := newSelectorModel("")
9293

9394
if m.list.Title != "Select Commit Type" {
9495
t.Errorf("list.Title = %q, want 'Select Commit Type'", m.list.Title)
@@ -98,6 +99,34 @@ func TestNewSelectorModel(t *testing.T) {
9899
if len(m.list.Items()) != 9 {
99100
t.Errorf("len(items) = %d, want 9", len(m.list.Items()))
100101
}
102+
103+
// First item should be selected by default
104+
if m.list.Index() != 0 {
105+
t.Errorf("list.Index() = %d, want 0", m.list.Index())
106+
}
107+
}
108+
109+
func TestNewSelectorModelWithInitialType(t *testing.T) {
110+
// Test with initial type "fix" (should be index 1)
111+
m := newSelectorModel("fix")
112+
113+
if m.list.Index() != 1 {
114+
t.Errorf("list.Index() = %d, want 1 (fix)", m.list.Index())
115+
}
116+
117+
// Test with initial type "docs" (should be index 2)
118+
m = newSelectorModel("docs")
119+
120+
if m.list.Index() != 2 {
121+
t.Errorf("list.Index() = %d, want 2 (docs)", m.list.Index())
122+
}
123+
124+
// Test with invalid type (should default to 0)
125+
m = newSelectorModel("invalid")
126+
127+
if m.list.Index() != 0 {
128+
t.Errorf("list.Index() = %d, want 0 (default)", m.list.Index())
129+
}
101130
}
102131

103132
func TestResultStruct(t *testing.T) {

internal/ui/commit/selector.go

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,18 @@ type selectorModel struct {
109109
width int
110110
}
111111

112-
func newSelectorModel() selectorModel {
112+
// findTypeIndex returns the index of the commit type in CommitTypes.
113+
// Returns 0 if not found.
114+
func findTypeIndex(commitType string) int {
115+
for i, ct := range consts.CommitTypes {
116+
if ct.Name == commitType {
117+
return i
118+
}
119+
}
120+
return 0
121+
}
122+
123+
func newSelectorModel(initialType string) selectorModel {
113124
items := make([]list.Item, len(consts.CommitTypes))
114125
for i, ct := range consts.CommitTypes {
115126
items[i] = selectorItem{
@@ -131,6 +142,11 @@ func newSelectorModel() selectorModel {
131142
l.Styles.Title = selectorTitleStyle
132143
l.Styles.PaginationStyle = lipgloss.NewStyle().PaddingLeft(2)
133144

145+
// Set initial selection based on branch type
146+
if initialType != "" {
147+
l.Select(findTypeIndex(initialType))
148+
}
149+
134150
return selectorModel{list: l, delegate: delegate, aiSelected: false}
135151
}
136152

@@ -243,8 +259,9 @@ func (m selectorModel) View() string {
243259
}
244260

245261
// runSelector shows a selector for commit type using bubbles/list.
246-
func runSelector() (string, error) {
247-
m := newSelectorModel()
262+
// initialType is the commit type to pre-select (can be empty).
263+
func runSelector(initialType string) (string, error) {
264+
m := newSelectorModel(initialType)
248265
p := tea.NewProgram(m)
249266

250267
finalModel, err := p.Run()

0 commit comments

Comments
 (0)