Skip to content

Commit 00257da

Browse files
fiftinCopilotCopilot
authored
fix: windows abs path support (#3720)
* fix: windows abs path support * Update util/local_path.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * test: add unit tests for repositoryUrl.js getRepositoryUrlType and isLocalRepositoryPath Co-authored-by: fiftin <914224+fiftin@users.noreply.github.com> Agent-Logs-Url: https://github.com/semaphoreui/semaphore/sessions/bbce1987-8c41-40ba-bd52-87b87cce5d48 * fix(repo): validate windows path --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: fiftin <914224+fiftin@users.noreply.github.com>
1 parent c9e2fe0 commit 00257da

File tree

10 files changed

+31044
-30786
lines changed

10 files changed

+31044
-30786
lines changed

db/Repository.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ func (r Repository) GetFullPath(templateID int) string {
6666
func (r Repository) GetGitURL(secure bool) string {
6767
url := r.GitURL
6868

69+
if r.GetType() == RepositoryLocal {
70+
return util.NormalizeLocalFilesystemPath(url)
71+
}
72+
6973
if secure {
7074
return url
7175
}
@@ -105,6 +109,10 @@ func (r Repository) GetType() RepositoryType {
105109
return RepositoryLocal
106110
}
107111

112+
if util.IsWindowsLocalRepositoryPath(r.GitURL) {
113+
return RepositoryLocal
114+
}
115+
108116
re := regexp.MustCompile(`^(\w+)://`)
109117
m := re.FindStringSubmatch(r.GitURL)
110118
if m == nil {

db/Repository_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ func TestRepository_GetSchema(t *testing.T) {
1717
assert.Equal(t, RepositoryHTTP, schema)
1818
}
1919

20+
func TestRepository_GetType_WindowsLocalPath(t *testing.T) {
21+
assert.Equal(t, RepositoryLocal, Repository{GitURL: `D:\repo`}.GetType())
22+
assert.Equal(t, RepositoryLocal, Repository{GitURL: `D:/repo`}.GetType())
23+
assert.Equal(t, RepositoryLocal, Repository{GitURL: `D:`}.GetType())
24+
assert.Equal(t, RepositoryLocal, Repository{GitURL: `\\server\share`}.GetType())
25+
}
26+
2027
func TestRepository_ClearCache(t *testing.T) {
2128
util.Config = &util.ConfigType{
2229
TmpPath: path.Join(os.TempDir(), util.RandString(rand.Intn(10-4)+4)),

services/tasks/LocalJob.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -819,8 +819,9 @@ func (t *LocalJob) prepareRun(installingArgs db_lib.LocalAppInstallingArgs) erro
819819
}
820820

821821
if t.Repository.GetType() == db.RepositoryLocal {
822-
if _, err := os.Stat(t.Repository.GitURL); err != nil {
823-
t.Log("Failed in finding static repository at " + t.Repository.GitURL + ": " + err.Error())
822+
localPath := t.Repository.GetGitURL(true)
823+
if _, err := os.Stat(localPath); err != nil {
824+
t.Log("Failed in finding static repository at " + localPath + ": " + err.Error())
824825
return err
825826
}
826827
} else {
@@ -879,8 +880,9 @@ func (t *LocalJob) prepareRunTerraform(tfApp *db_lib.TerraformApp, installingArg
879880
}
880881

881882
if t.Repository.GetType() == db.RepositoryLocal {
882-
if _, err := os.Stat(t.Repository.GitURL); err != nil {
883-
t.Log("Failed in finding static repository at " + t.Repository.GitURL + ": " + err.Error())
883+
localPath := t.Repository.GetGitURL(true)
884+
if _, err := os.Stat(localPath); err != nil {
885+
t.Log("Failed in finding static repository at " + localPath + ": " + err.Error())
884886
return err
885887
}
886888
} else {

util/local_path.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package util
2+
3+
import (
4+
"path/filepath"
5+
"runtime"
6+
"strings"
7+
)
8+
9+
// NormalizeLocalFilesystemPath converts paths that Git Bash / MSYS often produce
10+
// into a form native Windows APIs accept. Other paths are returned unchanged.
11+
func NormalizeLocalFilesystemPath(p string) string {
12+
if runtime.GOOS != "windows" {
13+
return p
14+
}
15+
// "/D:/path" -> "D:/path", "/d:\path" -> "d:\path" (strip leading "/")
16+
if len(p) >= 3 && p[0] == '/' && isDriveLetter(p[1]) && p[2] == ':' {
17+
return p[1:]
18+
}
19+
// "/d/path" (MSYS) -> "D:\path"
20+
if len(p) >= 3 && p[0] == '/' && isDriveLetter(p[1]) && p[2] == '/' {
21+
rest := p[3:]
22+
drive := strings.ToUpper(string(p[1]))
23+
if rest == "" {
24+
return drive + ":\\"
25+
}
26+
return drive + ":" + string(filepath.Separator) + filepath.FromSlash(rest)
27+
}
28+
return p
29+
}
30+
31+
func isDriveLetter(c byte) bool {
32+
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
33+
}
34+
35+
// IsWindowsLocalRepositoryPath reports whether p is a Windows drive-letter or UNC
36+
// path used as a local (non-Git) repository URL.
37+
func IsWindowsLocalRepositoryPath(p string) bool {
38+
if len(p) < 2 {
39+
return false
40+
}
41+
if p[0] == '\\' && p[1] == '\\' {
42+
return len(p) > 2
43+
}
44+
if !isDriveLetter(p[0]) || p[1] != ':' {
45+
return false
46+
}
47+
if len(p) == 2 {
48+
return true
49+
}
50+
if p[2] != '\\' && p[2] != '/' {
51+
return false
52+
}
53+
if len(p) > 3 && p[2] == '/' && p[3] == '/' {
54+
return false
55+
}
56+
return true
57+
}

util/local_path_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package util
2+
3+
import (
4+
"path/filepath"
5+
"runtime"
6+
"testing"
7+
)
8+
9+
func TestIsWindowsLocalRepositoryPath(t *testing.T) {
10+
tests := []struct {
11+
path string
12+
want bool
13+
}{
14+
{`D:\repo`, true},
15+
{`D:/repo`, true},
16+
{`D:`, true},
17+
{`D:repo`, false},
18+
{`a://example/repo.git`, false},
19+
{`c:\`, true},
20+
{`\\server\share`, true},
21+
{`\\`, false},
22+
{``, false},
23+
{`/usr/src`, false},
24+
{`https://x`, false},
25+
}
26+
for _, tt := range tests {
27+
if got := IsWindowsLocalRepositoryPath(tt.path); got != tt.want {
28+
t.Errorf("IsWindowsLocalRepositoryPath(%q) = %v, want %v", tt.path, got, tt.want)
29+
}
30+
}
31+
}
32+
33+
func TestNormalizeLocalFilesystemPath(t *testing.T) {
34+
if runtime.GOOS == "windows" {
35+
t.Run("leading_slash_before_drive", func(t *testing.T) {
36+
got := NormalizeLocalFilesystemPath("/D:/ps-demo-script")
37+
want := "D:/ps-demo-script"
38+
if got != want {
39+
t.Fatalf("got %q want %q", got, want)
40+
}
41+
})
42+
t.Run("msys_drive_path", func(t *testing.T) {
43+
got := NormalizeLocalFilesystemPath("/d/ps-demo-script/extra")
44+
want := "D:" + string(filepath.Separator) + filepath.FromSlash("ps-demo-script/extra")
45+
if got != want {
46+
t.Fatalf("got %q want %q", got, want)
47+
}
48+
})
49+
} else {
50+
p := "/D:/ps-demo-script"
51+
if got := NormalizeLocalFilesystemPath(p); got != p {
52+
t.Fatalf("non-Windows should leave path unchanged: got %q", got)
53+
}
54+
}
55+
}

0 commit comments

Comments
 (0)