├── .gitignore
├── .github
├── dependabot.yml
└── workflows
│ └── ci.yml
├── example_test.go
├── README.md
├── entry.go
├── pseudoroot.go
├── .golangci.yml
├── file.go
├── LICENSE
├── go.mod
├── fs_test.go
├── fs.go
└── go.sum
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .DS_Store
3 | vendor
4 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "weekly"
7 |
8 | - package-ecosystem: "gomod"
9 | directory: "/"
10 | schedule:
11 | interval: "weekly"
12 |
--------------------------------------------------------------------------------
/example_test.go:
--------------------------------------------------------------------------------
1 | package gitfs_test
2 |
3 | import (
4 | "fmt"
5 | "io/fs"
6 |
7 | "github.com/forensicanalysis/gitfs"
8 | )
9 |
10 | func Example() {
11 | // init file system
12 | fsys, _ := gitfs.New("https://github.com/boltdb/bolt")
13 |
14 | // read root directory
15 | data, _ := fs.ReadFile(fsys, "README.md")
16 |
17 | // print files
18 | fmt.Println(string(data)[:4])
19 | // Output: Bolt
20 | }
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
gitfs
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Read a remote git repository as [io/fs.FS](https://golang.org/pkg/io/fs/#FS).
10 |
11 | ## Example
12 |
13 | ``` go
14 | func main() {
15 | // init file system
16 | fsys, _ := gitfs.New("https://github.com/boltdb/bolt")
17 |
18 | // read root directory
19 | data, _ := fs.ReadFile(fsys, "README.md")
20 |
21 | // print files
22 | fmt.Println(string(data)[:4])
23 | // Output: Bolt
24 | }
25 | ```
26 |
--------------------------------------------------------------------------------
/entry.go:
--------------------------------------------------------------------------------
1 | package gitfs
2 |
3 | import (
4 | "io/fs"
5 | "strings"
6 | "time"
7 | )
8 |
9 | type GitEntry struct{ info fs.FileInfo }
10 |
11 | func (g *GitEntry) Name() string {
12 | if g.info.Name() == "" {
13 | panic("empty name")
14 | }
15 |
16 | if g.info.Name() == "/" {
17 | return "."
18 | }
19 |
20 | return strings.Trim(g.info.Name(), "/")
21 | }
22 |
23 | func (g *GitEntry) Mode() fs.FileMode {
24 | if g.IsDir() {
25 | return fs.ModeDir
26 | }
27 |
28 | return 0
29 | }
30 |
31 | func (g *GitEntry) Size() int64 { return g.info.Size() }
32 |
33 | func (g *GitEntry) ModTime() time.Time { return time.Time{} }
34 |
35 | func (g *GitEntry) Sys() any { return nil }
36 |
37 | func (g *GitEntry) IsDir() bool { return g.info.IsDir() }
38 |
39 | func (g *GitEntry) Type() fs.FileMode { return g.Mode() }
40 |
41 | func (g *GitEntry) Info() (fs.FileInfo, error) { return g, nil }
42 |
--------------------------------------------------------------------------------
/pseudoroot.go:
--------------------------------------------------------------------------------
1 | package gitfs
2 |
3 | import (
4 | "io"
5 | "io/fs"
6 | )
7 |
8 | type PseudoDir struct {
9 | fs *GitFS
10 | dirOffset int
11 | path string
12 | }
13 |
14 | func (p *PseudoDir) Stat() (fs.FileInfo, error) {
15 | return p.fs.Stat(p.path)
16 | }
17 |
18 | func (p *PseudoDir) Read([]byte) (int, error) { return 0, fs.ErrInvalid }
19 |
20 | func (p *PseudoDir) Close() error { return nil }
21 |
22 | func (p *PseudoDir) ReadDir(n int) ([]fs.DirEntry, error) {
23 | entries, err := p.fs.ReadDir(p.path)
24 |
25 | // directory already exhausted
26 | if n <= 0 && p.dirOffset >= len(entries) {
27 | return nil, nil
28 | }
29 |
30 | // read till end
31 | if n > 0 && p.dirOffset+n > len(entries) {
32 | err = io.EOF
33 | }
34 |
35 | if n > 0 && p.dirOffset+n <= len(entries) {
36 | entries = entries[p.dirOffset : p.dirOffset+n]
37 | p.dirOffset += n
38 | } else {
39 | entries = entries[p.dirOffset:]
40 | p.dirOffset += len(entries)
41 | }
42 |
43 | return entries, err
44 | }
45 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | version: "2"
2 | run:
3 | go: "1.23"
4 | linters:
5 | default: all
6 | disable:
7 | - depguard
8 | - err113
9 | - errcheck
10 | - exhaustruct
11 | - gochecknoglobals
12 | - gochecknoinits
13 | - gosmopolitan
14 | - ireturn
15 | - mnd
16 | - nlreturn
17 | - nonamedreturns
18 | - perfsprint
19 | - prealloc
20 | - tagliatelle
21 | - testpackage
22 | - varnamelen
23 | - wrapcheck
24 | exclusions:
25 | generated: lax
26 | presets:
27 | - comments
28 | - common-false-positives
29 | - legacy
30 | - std-error-handling
31 | paths:
32 | - third_party$
33 | - builtin$
34 | - examples$
35 | formatters:
36 | enable:
37 | - gci
38 | - gofmt
39 | - gofumpt
40 | - goimports
41 | settings:
42 | gci:
43 | sections:
44 | - standard
45 | - default
46 | - prefix(github.com/forensicanalysis/gitfs)
47 | exclusions:
48 | generated: lax
49 | paths:
50 | - third_party$
51 | - builtin$
52 | - examples$
53 |
--------------------------------------------------------------------------------
/file.go:
--------------------------------------------------------------------------------
1 | package gitfs
2 |
3 | import (
4 | "io"
5 | "io/fs"
6 |
7 | "github.com/go-git/go-billy/v5"
8 | )
9 |
10 | type GitFile struct {
11 | file billy.File
12 | path string
13 | fs *GitFS
14 | dirOffset int
15 | }
16 |
17 | func (g *GitFile) Read(bytes []byte) (int, error) {
18 | return g.file.Read(bytes)
19 | }
20 |
21 | func (g *GitFile) Close() error {
22 | return g.file.Close()
23 | }
24 |
25 | func (g *GitFile) Stat() (fs.FileInfo, error) {
26 | return g.fs.Stat(g.path)
27 | }
28 |
29 | func (g *GitFile) ReadDir(n int) ([]fs.DirEntry, error) {
30 | entries, err := g.fs.ReadDir(g.path)
31 |
32 | // directory already exhausted
33 | if n <= 0 && g.dirOffset >= len(entries) {
34 | return nil, nil
35 | }
36 |
37 | // read till end
38 | if n > 0 && g.dirOffset+n > len(entries) {
39 | err = io.EOF
40 | }
41 |
42 | if n > 0 && g.dirOffset+n <= len(entries) {
43 | entries = entries[g.dirOffset : g.dirOffset+n]
44 | g.dirOffset += n
45 | } else {
46 | entries = entries[g.dirOffset:]
47 | g.dirOffset += len(entries)
48 | }
49 |
50 | return entries, err
51 | }
52 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Jonas Plum
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | push:
4 | branches: [ main ]
5 | pull_request:
6 |
7 | env:
8 | GOTOOLCHAIN: local
9 |
10 | jobs:
11 | lint:
12 | name: Lint
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v6
16 | - uses: actions/setup-go@v6
17 | with: { go-version: '1.25' }
18 |
19 | - uses: golangci/golangci-lint-action@v9
20 | with: { version: v2.6.2 }
21 | gitfs:
22 | name: gitfs
23 | runs-on: ${{ matrix.os }}
24 | strategy:
25 | matrix:
26 | os: [macos-latest, windows-latest, ubuntu-latest]
27 | go-version: [ '1.24', '1.25' ]
28 | steps:
29 | - uses: actions/checkout@v6
30 | - uses: actions/setup-go@v6
31 | with:
32 | go-version: ${{ matrix.go-version }}
33 |
34 | - run: go test -race -coverprofile=coverage.txt -covermode=atomic
35 | shell: bash
36 | - name: Upload coverage
37 | env:
38 | CI: "true"
39 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
40 | run: bash <(curl -s https://codecov.io/bash)
41 | if: matrix.os == 'windows-latest'
42 | shell: bash
43 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/forensicanalysis/gitfs
2 |
3 | go 1.24.0
4 |
5 | require (
6 | github.com/go-git/go-billy/v5 v5.7.0
7 | github.com/go-git/go-git/v5 v5.16.4
8 | )
9 |
10 | require (
11 | dario.cat/mergo v1.0.0 // indirect
12 | github.com/Microsoft/go-winio v0.6.2 // indirect
13 | github.com/ProtonMail/go-crypto v1.1.6 // indirect
14 | github.com/cloudflare/circl v1.6.1 // indirect
15 | github.com/cyphar/filepath-securejoin v0.4.1 // indirect
16 | github.com/emirpasic/gods v1.18.1 // indirect
17 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
18 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
19 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
20 | github.com/kevinburke/ssh_config v1.2.0 // indirect
21 | github.com/pjbgf/sha1cd v0.3.2 // indirect
22 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
23 | github.com/skeema/knownhosts v1.3.1 // indirect
24 | github.com/xanzy/ssh-agent v0.3.3 // indirect
25 | golang.org/x/crypto v0.45.0 // indirect
26 | golang.org/x/net v0.47.0 // indirect
27 | golang.org/x/sys v0.38.0 // indirect
28 | gopkg.in/warnings.v0 v0.1.2 // indirect
29 | )
30 |
--------------------------------------------------------------------------------
/fs_test.go:
--------------------------------------------------------------------------------
1 | package gitfs
2 |
3 | import (
4 | "io/fs"
5 | "reflect"
6 | "sort"
7 | "strings"
8 | "testing"
9 | "testing/fstest"
10 | )
11 |
12 | func TestFS(t *testing.T) {
13 | t.Parallel()
14 |
15 | fsys, err := New("https://github.com/forensicanalysis/fslib")
16 | if err != nil {
17 | t.Fatal(err)
18 | }
19 |
20 | err = fstest.TestFS(fsys, "LICENSE")
21 | if err != nil {
22 | t.Fatal(err)
23 | }
24 | }
25 |
26 | func TestNew(t *testing.T) {
27 | t.Parallel()
28 |
29 | want := strings.Split(".gitignore LICENSE Makefile README.md appveyor.yml "+
30 | "bolt_386.go bolt_amd64.go bolt_arm.go bolt_arm64.go bolt_linux.go bolt_openbsd.go "+
31 | "bolt_ppc.go bolt_ppc64.go bolt_ppc64le.go bolt_s390x.go bolt_unix.go bolt_unix_solaris.go "+
32 | "bolt_windows.go boltsync_unix.go bucket.go bucket_test.go cmd cursor.go cursor_test.go "+
33 | "db.go db_test.go doc.go errors.go freelist.go freelist_test.go node.go node_test.go "+
34 | "page.go page_test.go quick_test.go simulation_test.go tx.go tx_test.go", " ")
35 |
36 | type args struct {
37 | url string
38 | }
39 |
40 | tests := []struct {
41 | name string
42 | args args
43 | want []string
44 | wantErr bool
45 | }{
46 | {"New", args{"https://github.com/boltdb/bolt"}, want, false},
47 | }
48 | for _, tt := range tests {
49 | t.Run(tt.name, func(t *testing.T) {
50 | t.Parallel()
51 |
52 | fsys, err := New(tt.args.url)
53 | if (err != nil) != tt.wantErr {
54 | t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
55 | return
56 | }
57 |
58 | var names []string
59 |
60 | entries, err := fs.ReadDir(fsys, ".")
61 | if err != nil {
62 | t.Errorf("New() error = %v", err)
63 | return
64 | }
65 |
66 | for _, entry := range entries {
67 | names = append(names, entry.Name())
68 | }
69 |
70 | sort.Strings(names)
71 | sort.Strings(tt.want)
72 |
73 | if !reflect.DeepEqual(names, tt.want) {
74 | t.Errorf("New() got = %v, want %v", names, tt.want)
75 | }
76 | })
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/fs.go:
--------------------------------------------------------------------------------
1 | package gitfs
2 |
3 | import (
4 | "fmt"
5 | "io/fs"
6 | "sort"
7 | "strings"
8 |
9 | "github.com/go-git/go-billy/v5"
10 | "github.com/go-git/go-billy/v5/memfs"
11 | "github.com/go-git/go-git/v5"
12 | "github.com/go-git/go-git/v5/storage/memory"
13 | )
14 |
15 | type GitFS struct {
16 | FS billy.Filesystem
17 | }
18 |
19 | func New(url string) (*GitFS, error) {
20 | return NewWithOptions(&git.CloneOptions{URL: url})
21 | }
22 |
23 | func NewWithOptions(options *git.CloneOptions) (*GitFS, error) {
24 | repository, err := git.Clone(memory.NewStorage(), memfs.New(), options)
25 | if err != nil {
26 | return nil, err
27 | }
28 |
29 | worktree, err := repository.Worktree()
30 | if err != nil {
31 | return nil, err
32 | }
33 |
34 | return &GitFS{worktree.Filesystem}, err
35 | }
36 |
37 | func (g *GitFS) Open(name string) (fs.File, error) {
38 | if !fs.ValidPath(name) || strings.Contains(name, `\`) {
39 | return nil, fmt.Errorf("invalid path: %s", name)
40 | }
41 |
42 | info, err := g.Stat(name)
43 | if err != nil {
44 | return nil, err
45 | }
46 |
47 | if name == "." || info.IsDir() {
48 | return &PseudoDir{fs: g, path: name}, nil
49 | }
50 |
51 | file, err := g.FS.Open(name)
52 |
53 | return &GitFile{path: name, fs: g, file: file}, err
54 | }
55 |
56 | func (g *GitFS) Stat(name string) (fs.FileInfo, error) {
57 | if !fs.ValidPath(name) || strings.Contains(name, `\`) {
58 | return nil, fmt.Errorf("invalid path: %s", name)
59 | }
60 |
61 | info, err := g.FS.Lstat(name)
62 |
63 | return &GitEntry{info: info}, err
64 | }
65 |
66 | func (g *GitFS) ReadDir(name string) (entries []fs.DirEntry, err error) {
67 | if !fs.ValidPath(name) || strings.Contains(name, `\`) {
68 | return nil, fmt.Errorf("invalid path: %s", name)
69 | }
70 |
71 | infos, err := g.FS.ReadDir(name)
72 | if err != nil {
73 | return nil, err
74 | }
75 |
76 | for _, info := range infos {
77 | e := &GitEntry{info}
78 | entries = append(entries, e)
79 | }
80 |
81 | sort.Slice(entries, func(i, j int) bool { return entries[i].Name() < entries[j].Name() })
82 |
83 | return entries, err
84 | }
85 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
2 | dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
3 | github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
4 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
5 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
6 | github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
7 | github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
8 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
9 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
10 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
11 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
12 | github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
13 | github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
14 | github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
15 | github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
16 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
17 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
18 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
19 | github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
20 | github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
21 | github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
22 | github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
23 | github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
24 | github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
25 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
26 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
27 | github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM=
28 | github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E=
29 | github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
30 | github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
31 | github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y=
32 | github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
33 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
34 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
35 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
36 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
37 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
38 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
39 | github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
40 | github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
41 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
42 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
43 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
44 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
45 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
46 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
47 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
48 | github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
49 | github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
50 | github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
51 | github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
52 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
53 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
54 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
55 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
56 | github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
57 | github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
58 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
59 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
60 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
61 | github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
62 | github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
63 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
64 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
65 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
66 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
67 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
68 | github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
69 | github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
70 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
71 | golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
72 | golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
73 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
74 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
75 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
76 | golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
77 | golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
78 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
79 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
80 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
81 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
82 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
83 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
84 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
85 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
86 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
87 | golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
88 | golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
89 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
90 | golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
91 | golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
92 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
93 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
94 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
95 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
96 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
97 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
98 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
99 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
100 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
101 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
102 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
103 |
--------------------------------------------------------------------------------