mirror of
https://github.com/x-motemen/ghq.git
synced 2026-09-10 07:26:27 -04:00
add windows symlink & junction support
Signed-off-by: walnuts1018 <r.juglans.1018@gmail.com>
This commit is contained in:
parent
52efddc011
commit
231448ee85
2
.github/workflows/test.yaml
vendored
2
.github/workflows/test.yaml
vendored
|
|
@ -26,6 +26,6 @@ jobs:
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
- name: test
|
- name: test
|
||||||
run: go test -coverprofile coverage.out -covermode atomic ./...
|
run: sudo go test -coverprofile coverage.out -covermode atomic ./...
|
||||||
- name: Send coverage
|
- name: Send coverage
|
||||||
uses: codecov/codecov-action@v1
|
uses: codecov/codecov-action@v1
|
||||||
|
|
|
||||||
3
go.mod
3
go.mod
|
|
@ -9,7 +9,7 @@ require (
|
||||||
github.com/saracen/walker v0.1.4
|
github.com/saracen/walker v0.1.4
|
||||||
github.com/urfave/cli/v2 v2.27.2
|
github.com/urfave/cli/v2 v2.27.2
|
||||||
golang.org/x/net v0.27.0
|
golang.org/x/net v0.27.0
|
||||||
golang.org/x/sync v0.7.0
|
golang.org/x/sync v0.8.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|
@ -23,6 +23,7 @@ require (
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||||
golang.org/x/sys v0.22.0 // indirect
|
golang.org/x/sys v0.22.0 // indirect
|
||||||
|
golang.org/x/text v0.19.0
|
||||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
|
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
||||||
6
go.sum
6
go.sum
|
|
@ -84,8 +84,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
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
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||||
|
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,12 @@
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
import "path/filepath"
|
||||||
|
|
||||||
func toFullPath(s string) (string, error) {
|
func toFullPath(s string) (string, error) {
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func evalSymlinks(path string) (string, error) {
|
||||||
|
return filepath.EvalSymlinks(path)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,12 @@
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import "syscall"
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
func toFullPath(s string) (string, error) {
|
func toFullPath(s string) (string, error) {
|
||||||
p := syscall.StringToUTF16(s)
|
p := syscall.StringToUTF16(s)
|
||||||
|
|
@ -21,3 +26,42 @@ func toFullPath(s string) (string, error) {
|
||||||
b = b[:n]
|
b = b[:n]
|
||||||
return syscall.UTF16ToString(b), nil
|
return syscall.UTF16ToString(b), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func evalSymlinks(path string) (string, error) {
|
||||||
|
_, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
list := filepathSplitAll(path)
|
||||||
|
evaled := list[0]
|
||||||
|
for i := 1; i < len(list); i++ {
|
||||||
|
evaled = filepath.Join(evaled, list[i])
|
||||||
|
|
||||||
|
linkSrc, err := os.Readlink(evaled)
|
||||||
|
if err != nil {
|
||||||
|
// not symlink
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
if filepath.IsAbs(linkSrc) {
|
||||||
|
evaled = linkSrc
|
||||||
|
} else {
|
||||||
|
evaled = filepath.Join(filepath.Dir(evaled), linkSrc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return evaled, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func filepathSplitAll(path string) []string {
|
||||||
|
path = filepath.Clean(path)
|
||||||
|
path = filepath.ToSlash(path)
|
||||||
|
|
||||||
|
vol := filepath.VolumeName(path)
|
||||||
|
|
||||||
|
path = path[len(vol):]
|
||||||
|
list := strings.Split(path, "/")
|
||||||
|
list[0] = vol + string(filepath.Separator) + list[0]
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
|
||||||
115
helpers_windows_test.go
Normal file
115
helpers_windows_test.go
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/text/encoding/japanese"
|
||||||
|
"golang.org/x/text/transform"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testEvalSymlinksMode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
testEvalSymlinksNotLink testEvalSymlinksMode = iota
|
||||||
|
testEvalSymlinksSymbolicLink
|
||||||
|
testEvalSymlinksJunction
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_evalSymlinks(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
mode testEvalSymlinksMode
|
||||||
|
linkBasePath string
|
||||||
|
args args
|
||||||
|
want string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "not link",
|
||||||
|
mode: testEvalSymlinksNotLink,
|
||||||
|
args: args{
|
||||||
|
path: filepath.Join(os.TempDir(), "not_link"),
|
||||||
|
},
|
||||||
|
want: filepath.Join(os.TempDir(), "not_link"),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "symbolic link",
|
||||||
|
mode: testEvalSymlinksSymbolicLink,
|
||||||
|
linkBasePath: filepath.Join(os.TempDir(), "link_base"),
|
||||||
|
args: args{
|
||||||
|
path: filepath.Join(os.TempDir(), "symbolic_link"),
|
||||||
|
},
|
||||||
|
want: filepath.Join(os.TempDir(), "link_base"),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "junction",
|
||||||
|
mode: testEvalSymlinksJunction,
|
||||||
|
linkBasePath: filepath.Join(os.TempDir(), "link_base"),
|
||||||
|
args: args{
|
||||||
|
path: filepath.Join(os.TempDir(), "junction"),
|
||||||
|
},
|
||||||
|
want: filepath.Join(os.TempDir(), "link_base"),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if err := createLink(tt.linkBasePath, tt.args.path, tt.mode); err != nil {
|
||||||
|
t.Errorf("failed to create link: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := evalSymlinks(tt.args.path)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("evalSymlinks() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("evalSymlinks() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createLink(linkBasePath, path string, mode testEvalSymlinksMode) error {
|
||||||
|
if err := os.RemoveAll(path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if mode == testEvalSymlinksNotLink {
|
||||||
|
return os.MkdirAll(path, 0755)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(linkBasePath, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch mode {
|
||||||
|
case testEvalSymlinksSymbolicLink:
|
||||||
|
return os.Symlink(linkBasePath, path)
|
||||||
|
case testEvalSymlinksJunction:
|
||||||
|
output, err := exec.Command("cmd", "/c", "mklink", "/J", path, linkBasePath).CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
output, err := io.ReadAll(transform.NewReader(bytes.NewBuffer(output), japanese.ShiftJIS.NewDecoder()))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to transform output: %w", err)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to create junction: %s, %w", string(output), err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -399,7 +399,7 @@ func localRepositoryRoots(all bool) ([]string, error) {
|
||||||
for _, v := range roots {
|
for _, v := range roots {
|
||||||
path := filepath.Clean(v)
|
path := filepath.Clean(v)
|
||||||
if _, err := os.Stat(path); err == nil {
|
if _, err := os.Stat(path); err == nil {
|
||||||
if path, err = filepath.EvalSymlinks(path); err != nil {
|
if path, err = evalSymlinks(path); err != nil {
|
||||||
_localRepoErr = err
|
_localRepoErr = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue