Skip to content

Commit d02b25d

Browse files
committed
fix: harden url conversion and browser handling
1 parent 8adfe47 commit d02b25d

6 files changed

Lines changed: 123 additions & 65 deletions

File tree

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,17 +39,20 @@ Download the pre-compiled binary for your platform:
3939

4040
```sh
4141
# macOS (Intel)
42-
curl -L https://github.com/zhaochunqi/git-open/releases/latest/download/git-open-darwin-amd64 -o git-open
42+
curl -L https://github.com/zhaochunqi/git-open/releases/latest/download/git-open_Darwin_x86_64.tar.gz -o git-open.tar.gz
43+
tar -xzf git-open.tar.gz
4344
chmod +x git-open
4445
sudo mv git-open /usr/local/bin/
4546

4647
# macOS (Apple Silicon)
47-
curl -L https://github.com/zhaochunqi/git-open/releases/latest/download/git-open-darwin-arm64 -o git-open
48+
curl -L https://github.com/zhaochunqi/git-open/releases/latest/download/git-open_Darwin_arm64.tar.gz -o git-open.tar.gz
49+
tar -xzf git-open.tar.gz
4850
chmod +x git-open
4951
sudo mv git-open /usr/local/bin/
5052

5153
# Linux (x64)
52-
curl -L https://github.com/zhaochunqi/git-open/releases/latest/download/git-open-linux-amd64 -o git-open
54+
curl -L https://github.com/zhaochunqi/git-open/releases/latest/download/git-open_Linux_x86_64.tar.gz -o git-open.tar.gz
55+
tar -xzf git-open.tar.gz
5356
chmod +x git-open
5457
sudo mv git-open /usr/local/bin/
5558
```

cmd/browser.go

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"errors"
55
"os/exec"
66
"runtime"
7+
"strings"
78
)
89

910
// ErrMockBrowser is used for testing browser errors
@@ -17,9 +18,24 @@ var getPlatform = func() string {
1718
return runtime.GOOS
1819
}
1920

21+
var commandRunner = func(name string, args ...string) error {
22+
cmd := exec.Command(name, args...)
23+
// Redirect stdout and stderr to /dev/null to suppress output
24+
cmd.Stdout = nil
25+
cmd.Stderr = nil
26+
return cmd.Start()
27+
}
28+
29+
var BrowserCommand string
30+
2031
func openURLInBrowser(url string) error {
2132
platform := getPlatform()
22-
33+
34+
customBrowser := strings.TrimSpace(BrowserCommand)
35+
if customBrowser != "" {
36+
return commandRunner(customBrowser, url)
37+
}
38+
2339
// On Linux, use xdg-open with output redirection to suppress messages
2440
if platform == "linux" {
2541
return openWithXdgOpen(url)
@@ -37,27 +53,15 @@ func openURLInBrowser(url string) error {
3753
}
3854

3955
func openWithXdgOpen(url string) error {
40-
cmd := exec.Command("xdg-open", url)
41-
// Redirect stdout and stderr to /dev/null to suppress output
42-
cmd.Stdout = nil
43-
cmd.Stderr = nil
44-
return cmd.Start()
56+
return commandRunner("xdg-open", url)
4557
}
4658

4759
func openWithMacOSOpen(url string) error {
48-
cmd := exec.Command("open", url)
49-
// Redirect stdout and stderr to /dev/null to suppress output
50-
cmd.Stdout = nil
51-
cmd.Stderr = nil
52-
return cmd.Start()
60+
return commandRunner("open", url)
5361
}
5462

5563
func openWithWindowsStart(url string) error {
56-
cmd := exec.Command("cmd", "/c", "start", "", url)
57-
// Redirect stdout and stderr to /dev/null to suppress output
58-
cmd.Stdout = nil
59-
cmd.Stderr = nil
60-
return cmd.Start()
64+
return commandRunner("cmd", "/c", "start", "", url)
6165
}
6266

6367
func openURLInBrowserFunc(url string) error {

cmd/browser_test.go

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,30 @@
11
package cmd
22

33
import (
4+
"os/exec"
45
"runtime"
56
"testing"
67
)
78

89
// mockOpenURL is a mock function for testing
910
var mockOpenURL func(string) error
1011

12+
func withNoopCommandRunner(t *testing.T) {
13+
t.Helper()
14+
t.Cleanup(func() {
15+
commandRunner = func(name string, args ...string) error {
16+
cmd := exec.Command(name, args...)
17+
// Redirect stdout and stderr to /dev/null to suppress output
18+
cmd.Stdout = nil
19+
cmd.Stderr = nil
20+
return cmd.Start()
21+
}
22+
})
23+
commandRunner = func(name string, args ...string) error {
24+
return nil
25+
}
26+
}
27+
1128
func Test_openURLInBrowser(t *testing.T) {
1229
// Save original function
1330
original := OpenURLInBrowser
@@ -76,6 +93,7 @@ func Test_openWithXdgOpen(t *testing.T) {
7693

7794
for _, tt := range tests {
7895
t.Run(tt.name, func(t *testing.T) {
96+
withNoopCommandRunner(t)
7997
err := openWithXdgOpen(tt.url)
8098
if (err != nil) != tt.wantErr {
8199
t.Errorf("openWithXdgOpen() error = %v, wantErr %v", err, tt.wantErr)
@@ -104,6 +122,7 @@ func Test_openWithMacOSOpen(t *testing.T) {
104122

105123
for _, tt := range tests {
106124
t.Run(tt.name, func(t *testing.T) {
125+
withNoopCommandRunner(t)
107126
err := openWithMacOSOpen(tt.url)
108127
if (err != nil) != tt.wantErr {
109128
t.Errorf("openWithMacOSOpen() error = %v, wantErr %v", err, tt.wantErr)
@@ -139,6 +158,7 @@ func Test_openURLInBrowser_PlatformSpecific(t *testing.T) {
139158
for _, tt := range tests {
140159
t.Run(tt.name, func(t *testing.T) {
141160
// Test the actual platform-specific implementation
161+
withNoopCommandRunner(t)
142162
err := openURLInBrowser(tt.url)
143163
if (err != nil) != tt.expectError {
144164
t.Errorf("openURLInBrowser() error = %v, expectError %v", err, tt.expectError)
@@ -175,11 +195,12 @@ func Test_openURLInBrowser_AllPlatforms(t *testing.T) {
175195
// Save the original runtime.GOOS
176196
originalGOOS := runtime.GOOS
177197
// We can't actually change runtime.GOOS, but we can test the functions directly
178-
198+
179199
switch tt.platform {
180200
case "linux":
181201
// Test openWithXdgOpen directly if not on Linux
182202
if runtime.GOOS != "linux" {
203+
withNoopCommandRunner(t)
183204
err := openWithXdgOpen(tt.url)
184205
// On non-Linux systems, this should fail as xdg-open doesn't exist
185206
if err == nil {
@@ -189,6 +210,7 @@ func Test_openURLInBrowser_AllPlatforms(t *testing.T) {
189210
case "darwin":
190211
// Test openWithMacOSOpen directly if not on macOS
191212
if runtime.GOOS != "darwin" {
213+
withNoopCommandRunner(t)
192214
err := openWithMacOSOpen(tt.url)
193215
// On non-macOS systems, this should fail as open command might not exist
194216
if err == nil {
@@ -198,6 +220,7 @@ func Test_openURLInBrowser_AllPlatforms(t *testing.T) {
198220
case "windows":
199221
// Test openWithWindowsStart directly if not on Windows
200222
if runtime.GOOS != "windows" {
223+
withNoopCommandRunner(t)
201224
err := openWithWindowsStart(tt.url)
202225
// On non-Windows systems, this should fail as cmd doesn't exist
203226
if err == nil {
@@ -258,14 +281,14 @@ func Test_openURLInBrowser_UnsupportedPlatform(t *testing.T) {
258281
wantErr: false, // May fail in CI but function should be called
259282
},
260283
{
261-
name: "macOS platform",
284+
name: "macOS platform",
262285
platform: "darwin",
263286
url: "https://github.com/test/repo",
264287
wantErr: false, // May fail in CI but function should be called
265288
},
266289
{
267290
name: "Windows platform",
268-
platform: "windows",
291+
platform: "windows",
269292
url: "https://github.com/test/repo",
270293
wantErr: false, // May fail in CI but function should be called
271294
},
@@ -291,9 +314,10 @@ func Test_openURLInBrowser_UnsupportedPlatform(t *testing.T) {
291314
getPlatform = func() string {
292315
return tt.platform
293316
}
317+
withNoopCommandRunner(t)
294318

295319
err := openURLInBrowser(tt.url)
296-
320+
297321
if tt.wantErr {
298322
if err == nil {
299323
t.Errorf("openURLInBrowser() expected error for platform %s, got nil", tt.platform)

cmd/git.go

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package cmd
33
import (
44
"errors"
55
"fmt"
6+
"net/url"
7+
"regexp"
68
"strings"
79

810
"github.com/go-git/go-git/v5"
@@ -22,7 +24,7 @@ const (
2224
// getCurrentGitDirectoryFunc is a variable that can be replaced for testing
2325
var getCurrentGitDirectoryFunc = func() (*git.Repository, error) {
2426
repo, err := git.PlainOpenWithOptions(".", &git.PlainOpenOptions{
25-
DetectDotGit: true,
27+
DetectDotGit: true,
2628
EnableDotGitCommonDir: true,
2729
})
2830
if err != nil {
@@ -56,26 +58,45 @@ func getRemoteURL(repo *git.Repository) (string, error) {
5658
return getRemoteURLFunc(repo)
5759
}
5860

59-
func convertToWebURL(url string) string {
60-
// Validate URL format
61-
if !strings.Contains(url, "://") && !strings.Contains(url, "@") {
61+
var scpRemoteURLPattern = regexp.MustCompile(`^(?:[^@]+@)?([^:]+):(.+)$`)
62+
63+
func convertToWebURL(rawURL string) string {
64+
raw := strings.TrimSpace(rawURL)
65+
if raw == "" {
6266
return ""
6367
}
6468

65-
// If the URL starts with "https://" or "http://", remove the ".git" suffix
66-
if strings.HasPrefix(url, "https://") || strings.HasPrefix(url, "http://") {
67-
url = strings.TrimSuffix(url, ".git")
68-
} else {
69-
// Otherwise, assume it's an SSH URL
70-
// Remove "ssh://" prefix
71-
url = strings.TrimPrefix(url, "ssh://")
72-
url = strings.Replace(url, ":", "/", 1)
73-
// Replace "git@" or "ssh://git@" with "https://"
74-
url = strings.Replace(url, "git@", "https://", 1)
75-
// Remove the ".git" suffix
76-
url = strings.TrimSuffix(url, ".git")
69+
parsedURL, err := url.Parse(raw)
70+
if err == nil && parsedURL.Host != "" && parsedURL.Scheme != "" {
71+
// URL style: https://, http://, ssh:// or git+ssh://
72+
path := strings.TrimPrefix(parsedURL.Path, "/")
73+
if path == "" {
74+
return ""
75+
}
76+
77+
switch parsedURL.Scheme {
78+
case "http", "https":
79+
host := parsedURL.Host
80+
return fmt.Sprintf("%s://%s/%s", parsedURL.Scheme, strings.TrimSuffix(host, "/"), strings.TrimSuffix(path, ".git"))
81+
case "ssh", "git+ssh":
82+
return fmt.Sprintf("https://%s/%s", parsedURL.Hostname(), strings.TrimSuffix(path, ".git"))
83+
}
84+
85+
return ""
7786
}
78-
return url
87+
88+
matches := scpRemoteURLPattern.FindStringSubmatch(raw)
89+
if len(matches) != 3 {
90+
return ""
91+
}
92+
93+
host := matches[1]
94+
path := strings.TrimPrefix(matches[2], "/")
95+
if host == "" || path == "" {
96+
return ""
97+
}
98+
99+
return fmt.Sprintf("https://%s/%s", host, strings.TrimSuffix(path, ".git"))
79100
}
80101

81102
// getBranchNameFunc is a variable that can be replaced for testing

cmd/root.go

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cmd
33
import (
44
"fmt"
55
"os"
6+
"strings"
67

78
"github.com/spf13/cobra"
89
"github.com/spf13/viper"
@@ -42,25 +43,23 @@ and converts it to a web URL. The web URL is then printed to the console.`,
4243
return fmt.Errorf("unsupported remote URL format: %s", remoteURL)
4344
}
4445

46+
branchName, err := getBranchName(repo)
47+
if err == nil && shouldAppendBranch(branchName) {
48+
// For now, we only append branch name if it's not 'main' or 'master'.
49+
// This can be improved later to fetch default branch from remote or allow configuration.
50+
webURL = buildBranchURL(webURL, branchName, remoteURL)
51+
}
52+
4553
// Open the web URL in the browser if the -o flag is provided
4654
plain, _ := cmd.Flags().GetBool("plain")
4755
if plain {
4856
fmt.Fprintf(cmd.OutOrStdout(), "Web URL: %s\n", webURL)
4957
return nil
5058
}
5159

52-
branchName, err := getBranchName(repo)
53-
if err == nil {
54-
// For now, we only append branch name if it's not 'main' or 'master'.
55-
// This can be improved later to fetch default branch from remote or allow configuration.
56-
if branchName != "main" && branchName != "master" {
57-
webURL = buildBranchURL(webURL, branchName, remoteURL)
58-
}
59-
}
60-
6160
err = openURLInBrowserFunc(webURL)
6261
if err != nil {
63-
fmt.Fprintf(cmd.OutOrStderr(), "Error opening URL in browser: %v\n", err)
62+
return fmt.Errorf("error opening URL in browser: %w", err)
6463
}
6564
return nil
6665
},
@@ -86,7 +85,6 @@ func init() {
8685

8786
// Cobra also supports local flags, which will only run
8887
// when this action is called directly.
89-
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
9088
rootCmd.Flags().BoolP("plain", "p", false, "Just print the web url without opening.")
9189
rootCmd.Flags().BoolP("version", "v", false, "Show version information")
9290
}
@@ -113,4 +111,10 @@ func initConfig() {
113111
if err := viper.ReadInConfig(); err == nil {
114112
fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
115113
}
114+
115+
BrowserCommand = strings.TrimSpace(viper.GetString("browser"))
116+
}
117+
118+
func shouldAppendBranch(branchName string) bool {
119+
return branchName != "main" && branchName != "master"
116120
}

0 commit comments

Comments
 (0)