@@ -3,16 +3,71 @@ package walk
3
3
import (
4
4
"context"
5
5
"fmt"
6
+ "github.com/charmbracelet/log"
7
+ "github.com/go-git/go-git/v5/plumbing/filemode"
8
+ "github.com/go-git/go-git/v5/plumbing/format/index"
6
9
"io/fs"
7
10
"os"
8
11
"path/filepath"
9
-
10
- "github.com/charmbracelet/log"
12
+ "strings"
11
13
12
14
"github.com/go-git/go-git/v5"
13
- "github.com/go-git/go-git/v5/plumbing/filemode"
14
15
)
15
16
17
+ // fileTree represents a hierarchical file structure with directories and files.
18
+ type fileTree struct {
19
+ name string
20
+ entries map [string ]* fileTree
21
+ }
22
+
23
+ // add inserts a file path into the fileTree structure, creating necessary parent directories if they do not exist.
24
+ func (n * fileTree ) add (path []string ) {
25
+ if len (path ) == 0 {
26
+ return
27
+ } else if n .entries == nil {
28
+ n .entries = make (map [string ]* fileTree )
29
+ }
30
+
31
+ name := path [0 ]
32
+ child , ok := n .entries [name ]
33
+ if ! ok {
34
+ child = & fileTree {name : name }
35
+ n .entries [name ] = child
36
+ }
37
+ child .add (path [1 :])
38
+ }
39
+
40
+ // addPath splits the given path by the filepath separator and inserts it into the fileTree structure.
41
+ func (n * fileTree ) addPath (path string ) {
42
+ n .add (strings .Split (path , string (filepath .Separator )))
43
+ }
44
+
45
+ // has returns true if the specified path exists in the fileTree, false otherwise.
46
+ func (n * fileTree ) has (path []string ) bool {
47
+ if len (path ) == 0 {
48
+ return true
49
+ } else if len (n .entries ) == 0 {
50
+ return false
51
+ }
52
+ child , ok := n .entries [path [0 ]]
53
+ if ! ok {
54
+ return false
55
+ }
56
+ return child .has (path [1 :])
57
+ }
58
+
59
+ // hasPath splits the given path by the filepath separator and checks if it exists in the fileTree.
60
+ func (n * fileTree ) hasPath (path string ) bool {
61
+ return n .has (strings .Split (path , string (filepath .Separator )))
62
+ }
63
+
64
+ // readIndex traverses the index entries and adds each file path to the fileTree structure.
65
+ func (n * fileTree ) readIndex (idx * index.Index ) {
66
+ for _ , entry := range idx .Entries {
67
+ n .addPath (entry .Name )
68
+ }
69
+ }
70
+
16
71
type gitWalker struct {
17
72
log * log.Logger
18
73
root string
@@ -25,12 +80,7 @@ func (g gitWalker) Root() string {
25
80
return g .root
26
81
}
27
82
28
- func (g gitWalker ) relPath (path string ) (string , error ) {
29
- // quick optimization for the majority of use cases
30
- if len (path ) >= g .relPathOffset && path [:len (g .root )] == g .root {
31
- return path [g .relPathOffset :], nil
32
- }
33
- // fallback to proper relative path resolution
83
+ func (g gitWalker ) relPath (path string ) (string , error ) { //
34
84
return filepath .Rel (g .root , path )
35
85
}
36
86
@@ -40,12 +90,16 @@ func (g gitWalker) Walk(ctx context.Context, fn WalkFunc) error {
40
90
return fmt .Errorf ("failed to open git index: %w" , err )
41
91
}
42
92
43
- // cache in-memory whether a path is present in the git index
44
- var cache map [string ]bool
93
+ // if we need to walk a path that is not the root of the repository, we will read the directory structure of the
94
+ // git index into memory for faster lookups
95
+ var cache * fileTree
45
96
46
97
for path := range g .paths {
47
98
48
- if path == g .root {
99
+ switch path {
100
+
101
+ case g .root :
102
+
49
103
// we can just iterate the index entries
50
104
for _ , entry := range idx .Entries {
51
105
select {
@@ -86,51 +140,56 @@ func (g gitWalker) Walk(ctx context.Context, fn WalkFunc) error {
86
140
}
87
141
}
88
142
}
89
- continue
90
- }
91
143
92
- // otherwise we ensure the git index entries are cached and then check if they are in the git index
93
- if cache == nil {
94
- cache = make (map [string ]bool )
95
- for _ , entry := range idx .Entries {
96
- cache [entry .Name ] = true
97
- }
98
- }
99
-
100
- relPath , err := filepath .Rel (g .root , path )
101
- if err != nil {
102
- return fmt .Errorf ("failed to find relative path for %v: %w" , path , err )
103
- }
104
-
105
- _ , ok := cache [relPath ]
106
- if ! (path == g .root || ok ) {
107
- log .Debugf ("path %v not found in git index, skipping" , path )
108
- continue
109
- }
144
+ default :
110
145
111
- return filepath .Walk (path , func (path string , info fs.FileInfo , _ error ) error {
112
- if info .IsDir () {
113
- return nil
146
+ // read the git index into memory if it hasn't already
147
+ if cache == nil {
148
+ cache = & fileTree {name : "" }
149
+ cache .readIndex (idx )
114
150
}
115
151
152
+ // git index entries are relative to the repository root, so we need to determine a relative path for the
153
+ // one we are currently processing before checking if it exists within the git index
116
154
relPath , err := g .relPath (path )
117
155
if err != nil {
118
- return fmt .Errorf ("failed to determine a relative path for %s : %w" , path , err )
156
+ return fmt .Errorf ("failed to find root relative path for %v : %w" , path , err )
119
157
}
120
158
121
- if _ , ok := cache [ relPath ]; ! ok {
122
- log .Debugf ("path %v not found in git index, skipping" , path )
123
- return nil
159
+ if ! cache . hasPath ( relPath ) {
160
+ log .Debugf ("path %s not found in git index, skipping" , relPath )
161
+ continue
124
162
}
125
163
126
- file := File {
127
- Path : path ,
128
- RelPath : relPath ,
129
- Info : info ,
130
- }
164
+ err = filepath .Walk (path , func (path string , info fs.FileInfo , _ error ) error {
165
+ // skip directories
166
+ if info .IsDir () {
167
+ return nil
168
+ }
169
+
170
+ // determine a path relative to g.root before checking presence in the git index
171
+ relPath , err := g .relPath (path )
172
+ if err != nil {
173
+ return fmt .Errorf ("failed to determine a relative path for %s: %w" , path , err )
174
+ }
175
+
176
+ if ! cache .hasPath (relPath ) {
177
+ log .Debugf ("path %v not found in git index, skipping" , relPath )
178
+ return nil
179
+ }
131
180
132
- return fn (& file , err )
133
- })
181
+ file := File {
182
+ Path : path ,
183
+ RelPath : relPath ,
184
+ Info : info ,
185
+ }
186
+
187
+ return fn (& file , err )
188
+ })
189
+ if err != nil {
190
+ return fmt .Errorf ("failed to walk %s: %w" , path , err )
191
+ }
192
+ }
134
193
}
135
194
136
195
return nil
0 commit comments