Skip to content

Commit c9e0c4e

Browse files
authored
Merge pull request #401 from walnuts1018/master
Add Windows Hardlink & Symbolic Link Support
2 parents 52efddc + 83eab51 commit c9e0c4e

6 files changed

+173
-5
lines changed

go.mod

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ require (
99
github.com/saracen/walker v0.1.4
1010
github.com/urfave/cli/v2 v2.27.2
1111
golang.org/x/net v0.27.0
12-
golang.org/x/sync v0.7.0
12+
golang.org/x/sync v0.8.0
1313
)
1414

1515
require (
@@ -23,6 +23,7 @@ require (
2323
github.com/russross/blackfriday/v2 v2.1.0 // indirect
2424
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
2525
golang.org/x/sys v0.22.0 // indirect
26+
golang.org/x/text v0.19.0
2627
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
2728
gopkg.in/yaml.v3 v3.0.1 // indirect
2829
)

go.sum

+4-2
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
8484
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
8585
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
8686
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
87-
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
88-
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
87+
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
88+
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
8989
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
9090
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
9191
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -104,6 +104,8 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn
104104
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
105105
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
106106
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
107+
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
108+
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
107109
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
108110
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
109111
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=

helpers_unix.go

+6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
package main
44

5+
import "path/filepath"
6+
57
func toFullPath(s string) (string, error) {
68
return s, nil
79
}
10+
11+
func evalSymlinks(path string) (string, error) {
12+
return filepath.EvalSymlinks(path)
13+
}

helpers_windows.go

+45-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22

33
package main
44

5-
import "syscall"
5+
import (
6+
"os"
7+
"path/filepath"
8+
"strings"
9+
"syscall"
10+
)
611

712
func toFullPath(s string) (string, error) {
813
p := syscall.StringToUTF16(s)
@@ -21,3 +26,42 @@ func toFullPath(s string) (string, error) {
2126
b = b[:n]
2227
return syscall.UTF16ToString(b), nil
2328
}
29+
30+
func evalSymlinks(path string) (string, error) {
31+
_, err := os.Stat(path)
32+
if err != nil {
33+
return "", err
34+
}
35+
36+
list := filepathSplitAll(path)
37+
evaled := list[0]
38+
for i := 1; i < len(list); i++ {
39+
evaled = filepath.Join(evaled, list[i])
40+
41+
linkSrc, err := os.Readlink(evaled)
42+
if err != nil {
43+
// not symlink
44+
continue
45+
} else {
46+
if filepath.IsAbs(linkSrc) {
47+
evaled = linkSrc
48+
} else {
49+
evaled = filepath.Join(filepath.Dir(evaled), linkSrc)
50+
}
51+
}
52+
}
53+
54+
return evaled, nil
55+
}
56+
57+
func filepathSplitAll(path string) []string {
58+
path = filepath.Clean(path)
59+
path = filepath.ToSlash(path)
60+
61+
vol := filepath.VolumeName(path)
62+
63+
path = path[len(vol):]
64+
list := strings.Split(path, "/")
65+
list[0] = vol + string(filepath.Separator) + list[0]
66+
return list
67+
}

helpers_windows_test.go

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
//go:build windows
2+
3+
package main
4+
5+
import (
6+
"bytes"
7+
"fmt"
8+
"io"
9+
"os"
10+
"os/exec"
11+
"path/filepath"
12+
"testing"
13+
14+
"golang.org/x/text/encoding/japanese"
15+
"golang.org/x/text/transform"
16+
)
17+
18+
type testEvalSymlinksMode int
19+
20+
const (
21+
testEvalSymlinksNotLink testEvalSymlinksMode = iota
22+
testEvalSymlinksSymbolicLink
23+
testEvalSymlinksJunction
24+
)
25+
26+
func Test_evalSymlinks(t *testing.T) {
27+
type args struct {
28+
path string
29+
}
30+
tests := []struct {
31+
name string
32+
mode testEvalSymlinksMode
33+
linkBasePath string
34+
args args
35+
want string
36+
wantErr bool
37+
}{
38+
{
39+
name: "not link",
40+
mode: testEvalSymlinksNotLink,
41+
args: args{
42+
path: filepath.Join(os.TempDir(), "not_link"),
43+
},
44+
want: filepath.Join(os.TempDir(), "not_link"),
45+
wantErr: false,
46+
},
47+
{
48+
name: "symbolic link",
49+
mode: testEvalSymlinksSymbolicLink,
50+
linkBasePath: filepath.Join(os.TempDir(), "link_base"),
51+
args: args{
52+
path: filepath.Join(os.TempDir(), "symbolic_link"),
53+
},
54+
want: filepath.Join(os.TempDir(), "link_base"),
55+
wantErr: false,
56+
},
57+
{
58+
name: "junction",
59+
mode: testEvalSymlinksJunction,
60+
linkBasePath: filepath.Join(os.TempDir(), "link_base"),
61+
args: args{
62+
path: filepath.Join(os.TempDir(), "junction"),
63+
},
64+
want: filepath.Join(os.TempDir(), "link_base"),
65+
wantErr: false,
66+
},
67+
}
68+
for _, tt := range tests {
69+
t.Run(tt.name, func(t *testing.T) {
70+
if err := createLink(tt.linkBasePath, tt.args.path, tt.mode); err != nil {
71+
t.Errorf("failed to create link: %v", err)
72+
return
73+
}
74+
75+
got, err := evalSymlinks(tt.args.path)
76+
if (err != nil) != tt.wantErr {
77+
t.Errorf("evalSymlinks() error = %v, wantErr %v", err, tt.wantErr)
78+
return
79+
}
80+
if got != tt.want {
81+
t.Errorf("evalSymlinks() = %v, want %v", got, tt.want)
82+
}
83+
})
84+
}
85+
}
86+
87+
func createLink(linkBasePath, path string, mode testEvalSymlinksMode) error {
88+
if err := os.RemoveAll(path); err != nil {
89+
return err
90+
}
91+
92+
if mode == testEvalSymlinksNotLink {
93+
return os.MkdirAll(path, 0755)
94+
}
95+
96+
if err := os.MkdirAll(linkBasePath, 0755); err != nil {
97+
return err
98+
}
99+
100+
switch mode {
101+
case testEvalSymlinksSymbolicLink:
102+
return os.Symlink(linkBasePath, path)
103+
case testEvalSymlinksJunction:
104+
output, err := exec.Command("cmd", "/c", "mklink", "/J", path, linkBasePath).CombinedOutput()
105+
if err != nil {
106+
output, err := io.ReadAll(transform.NewReader(bytes.NewBuffer(output), japanese.ShiftJIS.NewDecoder()))
107+
if err != nil {
108+
return fmt.Errorf("failed to transform output: %w", err)
109+
}
110+
return fmt.Errorf("failed to create junction: %s, %w", string(output), err)
111+
}
112+
return nil
113+
}
114+
return nil
115+
}

local_repository.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,7 @@ func localRepositoryRoots(all bool) ([]string, error) {
399399
for _, v := range roots {
400400
path := filepath.Clean(v)
401401
if _, err := os.Stat(path); err == nil {
402-
if path, err = filepath.EvalSymlinks(path); err != nil {
402+
if path, err = evalSymlinks(path); err != nil {
403403
_localRepoErr = err
404404
return
405405
}

0 commit comments

Comments
 (0)