detached 23b35cd

feat(preview): add naming logic — domain generation and branch sanitization

commit
23b35cd433c6834e3dd40641d1cad301c16abaa4
author
brice · · 2026-03-19 04:55 UTC
committer
brice · · 2026-03-19 05:13 UTC
parent
157f3c9
signature
unsigned
2 files · +118 -0
A internal/usecase/auto/preview/naming.go +45 -0
@@ -0,0 +1,45 @@
1+package preview
2+
3+import (
4+ "fmt"
5+ "path/filepath"
6+ "strings"
7+)
8+
9+// ExtractPreviewName strips the matched pattern prefix from a tag.
10+// "preview-login-redesign" with pattern "preview-*" → "login-redesign"
11+func ExtractPreviewName(tag string, patterns []string) string {
12+ for _, pattern := range patterns {
13+ if matched, _ := filepath.Match(pattern, tag); matched {
14+ prefix := strings.TrimSuffix(pattern, "*")
15+ return strings.TrimPrefix(tag, prefix)
16+ }
17+ }
18+ return tag
19+}
20+
21+// GeneratePreviewDomain creates the preview domain from a base route domain.
22+// "myapp.example.com" + "login" + "--" → "myapp--login.example.com"
23+func GeneratePreviewDomain(baseRoute, previewName, separator string) (string, error) {
24+ dot := strings.Index(baseRoute, ".")
25+ if dot < 0 {
26+ return "", fmt.Errorf("invalid base route domain: %s", baseRoute)
27+ }
28+ app := baseRoute[:dot]
29+ rest := baseRoute[dot:] // includes leading dot
30+ return app + separator + previewName + rest, nil
31+}
32+
33+// SanitizeBranchName converts a git branch name to a valid preview name.
34+// "feat/login-redesign" → "login-redesign"
35+func SanitizeBranchName(branch string) string {
36+ branch = strings.ToLower(branch)
37+ for _, prefix := range []string{"feat/", "fix/", "feature/", "hotfix/", "release/", "chore/"} {
38+ if strings.HasPrefix(branch, prefix) {
39+ branch = strings.TrimPrefix(branch, prefix)
40+ break
41+ }
42+ }
43+ branch = strings.ReplaceAll(branch, "/", "-")
44+ return branch
45+}
@@ -0,0 +1,45 @@
1+package preview
2+
3+import (
4+ "fmt"
5+ "path/filepath"
6+ "strings"
7+)
8+
9+// ExtractPreviewName strips the matched pattern prefix from a tag.
10+// "preview-login-redesign" with pattern "preview-*" → "login-redesign"
11+func ExtractPreviewName(tag string, patterns []string) string {
12+ for _, pattern := range patterns {
13+ if matched, _ := filepath.Match(pattern, tag); matched {
14+ prefix := strings.TrimSuffix(pattern, "*")
15+ return strings.TrimPrefix(tag, prefix)
16+ }
17+ }
18+ return tag
19+}
20+
21+// GeneratePreviewDomain creates the preview domain from a base route domain.
22+// "myapp.example.com" + "login" + "--" → "myapp--login.example.com"
23+func GeneratePreviewDomain(baseRoute, previewName, separator string) (string, error) {
24+ dot := strings.Index(baseRoute, ".")
25+ if dot < 0 {
26+ return "", fmt.Errorf("invalid base route domain: %s", baseRoute)
27+ }
28+ app := baseRoute[:dot]
29+ rest := baseRoute[dot:] // includes leading dot
30+ return app + separator + previewName + rest, nil
31+}
32+
33+// SanitizeBranchName converts a git branch name to a valid preview name.
34+// "feat/login-redesign" → "login-redesign"
35+func SanitizeBranchName(branch string) string {
36+ branch = strings.ToLower(branch)
37+ for _, prefix := range []string{"feat/", "fix/", "feature/", "hotfix/", "release/", "chore/"} {
38+ if strings.HasPrefix(branch, prefix) {
39+ branch = strings.TrimPrefix(branch, prefix)
40+ break
41+ }
42+ }
43+ branch = strings.ReplaceAll(branch, "/", "-")
44+ return branch
45+}
A internal/usecase/auto/preview/naming_test.go +73 -0
@@ -0,0 +1,73 @@
1+package preview
2+
3+import (
4+ "testing"
5+
6+ "github.com/stretchr/testify/assert"
7+)
8+
9+func TestExtractPreviewName(t *testing.T) {
10+ tests := []struct {
11+ name string
12+ tag string
13+ patterns []string
14+ want string
15+ }{
16+ {"simple prefix", "preview-login", []string{"preview-*"}, "login"},
17+ {"pr prefix", "pr-42", []string{"pr-*"}, "42"},
18+ {"multi-word", "preview-login-redesign", []string{"preview-*"}, "login-redesign"},
19+ {"second pattern matches", "pr-99", []string{"preview-*", "pr-*"}, "99"},
20+ {"no match", "v1.0.0", []string{"preview-*"}, "v1.0.0"},
21+ }
22+ for _, tt := range tests {
23+ t.Run(tt.name, func(t *testing.T) {
24+ assert.Equal(t, tt.want, ExtractPreviewName(tt.tag, tt.patterns))
25+ })
26+ }
27+}
28+
29+func TestGeneratePreviewDomain(t *testing.T) {
30+ tests := []struct {
31+ name string
32+ baseRoute string
33+ previewName string
34+ separator string
35+ want string
36+ wantErr bool
37+ }{
38+ {"flat default", "myapp.example.com", "login", "--", "myapp--login.example.com", false},
39+ {"flat with dashes", "my-app.example.com", "feat-123", "--", "my-app--feat-123.example.com", false},
40+ {"multi-level base", "myapp.sub.example.com", "feat", "--", "myapp--feat.sub.example.com", false},
41+ {"invalid domain", "nodot", "feat", "--", "", true},
42+ }
43+ for _, tt := range tests {
44+ t.Run(tt.name, func(t *testing.T) {
45+ got, err := GeneratePreviewDomain(tt.baseRoute, tt.previewName, tt.separator)
46+ if tt.wantErr {
47+ assert.Error(t, err)
48+ } else {
49+ assert.NoError(t, err)
50+ assert.Equal(t, tt.want, got)
51+ }
52+ })
53+ }
54+}
55+
56+func TestSanitizeBranchName(t *testing.T) {
57+ tests := []struct {
58+ name string
59+ branch string
60+ want string
61+ }{
62+ {"feature branch", "feat/login-redesign", "login-redesign"},
63+ {"fix branch", "fix/bug-123", "bug-123"},
64+ {"simple", "main", "main"},
65+ {"nested", "feat/ui/button", "ui-button"},
66+ {"uppercase", "Feat/MyBranch", "mybranch"},
67+ }
68+ for _, tt := range tests {
69+ t.Run(tt.name, func(t *testing.T) {
70+ assert.Equal(t, tt.want, SanitizeBranchName(tt.branch))
71+ })
72+ }
73+}
@@ -0,0 +1,73 @@
1+package preview
2+
3+import (
4+ "testing"
5+
6+ "github.com/stretchr/testify/assert"
7+)
8+
9+func TestExtractPreviewName(t *testing.T) {
10+ tests := []struct {
11+ name string
12+ tag string
13+ patterns []string
14+ want string
15+ }{
16+ {"simple prefix", "preview-login", []string{"preview-*"}, "login"},
17+ {"pr prefix", "pr-42", []string{"pr-*"}, "42"},
18+ {"multi-word", "preview-login-redesign", []string{"preview-*"}, "login-redesign"},
19+ {"second pattern matches", "pr-99", []string{"preview-*", "pr-*"}, "99"},
20+ {"no match", "v1.0.0", []string{"preview-*"}, "v1.0.0"},
21+ }
22+ for _, tt := range tests {
23+ t.Run(tt.name, func(t *testing.T) {
24+ assert.Equal(t, tt.want, ExtractPreviewName(tt.tag, tt.patterns))
25+ })
26+ }
27+}
28+
29+func TestGeneratePreviewDomain(t *testing.T) {
30+ tests := []struct {
31+ name string
32+ baseRoute string
33+ previewName string
34+ separator string
35+ want string
36+ wantErr bool
37+ }{
38+ {"flat default", "myapp.example.com", "login", "--", "myapp--login.example.com", false},
39+ {"flat with dashes", "my-app.example.com", "feat-123", "--", "my-app--feat-123.example.com", false},
40+ {"multi-level base", "myapp.sub.example.com", "feat", "--", "myapp--feat.sub.example.com", false},
41+ {"invalid domain", "nodot", "feat", "--", "", true},
42+ }
43+ for _, tt := range tests {
44+ t.Run(tt.name, func(t *testing.T) {
45+ got, err := GeneratePreviewDomain(tt.baseRoute, tt.previewName, tt.separator)
46+ if tt.wantErr {
47+ assert.Error(t, err)
48+ } else {
49+ assert.NoError(t, err)
50+ assert.Equal(t, tt.want, got)
51+ }
52+ })
53+ }
54+}
55+
56+func TestSanitizeBranchName(t *testing.T) {
57+ tests := []struct {
58+ name string
59+ branch string
60+ want string
61+ }{
62+ {"feature branch", "feat/login-redesign", "login-redesign"},
63+ {"fix branch", "fix/bug-123", "bug-123"},
64+ {"simple", "main", "main"},
65+ {"nested", "feat/ui/button", "ui-button"},
66+ {"uppercase", "Feat/MyBranch", "mybranch"},
67+ }
68+ for _, tt := range tests {
69+ t.Run(tt.name, func(t *testing.T) {
70+ assert.Equal(t, tt.want, SanitizeBranchName(tt.branch))
71+ })
72+ }
73+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close