Skip to content

Commit 5dcb6c5

Browse files
feat: new tk tool importers-count (#1232)
* feat: new `tk tool imported-count` This is useful to find which files are used widely and which aren't used at all Example: ``` julienduchesne@triceratops ksonnet % tk tool imported-count lib/meta lib/meta/clusters.libsonnet: 523 lib/meta/certificates.libsonnet: 257 lib/meta/namespaces.libsonnet: 249 lib/meta/teams.libsonnet: 248 lib/meta/environment.libsonnet: 231 lib/meta/envs.libsonnet: 229 lib/meta/functions.libsonnet: 229 lib/meta/remote_write.libsonnet: 71 lib/meta/cells.libsonnet: 70 lib/meta/networking.libsonnet: 5 lib/meta/datasources.libsonnet: 3 lib/meta/alerting.libsonnet: 2 lib/meta/repositories.libsonnet: 1 ``` * Rename command
1 parent 7023072 commit 5dcb6c5

File tree

4 files changed

+175
-0
lines changed

4 files changed

+175
-0
lines changed

cmd/tk/tool.go

+45
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ func toolCmd() *cli.Command {
2828
jpathCmd(),
2929
importsCmd(),
3030
importersCmd(),
31+
importersCountCmd(),
3132
chartsCmd(),
3233
)
3334
return cmd
@@ -184,6 +185,50 @@ if the file is not a vendored (located at <tk-root>/vendor/) or a lib file (loca
184185
return cmd
185186
}
186187

188+
func importersCountCmd() *cli.Command {
189+
cmd := &cli.Command{
190+
Use: "importers-count <dir>",
191+
Short: "for each file in the given directory, list the number of environments that import it",
192+
Long: `for each file in the given directory, list the number of environments that import it
193+
194+
As optimization,
195+
if the file is not a vendored (located at <tk-root>/vendor/) or a lib file (located at <tk-root>/lib/), we assume:
196+
- it is used in a Tanka environment
197+
- it will not be imported by any lib or vendor files
198+
- the environment base (closest main file in parent dirs) will be considered an importer
199+
- if no base is found, all main files in child dirs will be considered importers
200+
`,
201+
Args: cli.Args{
202+
Validator: cli.ArgsExact(1),
203+
Predictor: complete.PredictDirs("*"),
204+
},
205+
}
206+
207+
root := cmd.Flags().String("root", ".", "root directory to search for environments")
208+
recursive := cmd.Flags().Bool("recursive", false, "find files recursively")
209+
filenameRegex := cmd.Flags().String("filename-regex", "", "only count files that match the given regex. Matches only jsonnet files by default")
210+
211+
cmd.Run = func(_ *cli.Command, args []string) error {
212+
dir := args[0]
213+
214+
root, err := filepath.Abs(*root)
215+
if err != nil {
216+
return fmt.Errorf("resolving root: %w", err)
217+
}
218+
219+
result, err := jsonnet.CountImporters(root, dir, *recursive, *filenameRegex)
220+
if err != nil {
221+
return fmt.Errorf("resolving imports: %s", err)
222+
}
223+
224+
fmt.Println(result)
225+
226+
return nil
227+
}
228+
229+
return cmd
230+
}
231+
187232
func gitRoot() (string, error) {
188233
s, err := git("rev-parse", "--show-toplevel")
189234
return strings.TrimRight(s, "\n"), err

pkg/jsonnet/find_importers.go

+82
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"os"
66
"path/filepath"
7+
"regexp"
78
"sort"
89
"strings"
910

@@ -82,6 +83,87 @@ func FindImporterForFiles(root string, files []string) ([]string, error) {
8283
return mapToArray(importers), nil
8384
}
8485

86+
// CountImporters lists all the files in the given directory and for each file counts the number of environments that import it.
87+
func CountImporters(root string, dir string, recursive bool, filenameRegexStr string) (string, error) {
88+
root, err := filepath.Abs(root)
89+
if err != nil {
90+
return "", fmt.Errorf("resolving root: %w", err)
91+
}
92+
93+
if filenameRegexStr == "" {
94+
filenameRegexStr = "^.*\\.(jsonnet|libsonnet)$"
95+
}
96+
filenameRegexp, err := regexp.Compile(filenameRegexStr)
97+
if err != nil {
98+
return "", fmt.Errorf("compiling filename regex: %w", err)
99+
}
100+
var files []string
101+
err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
102+
if err != nil {
103+
return err
104+
}
105+
106+
if info.IsDir() && !recursive && path != dir {
107+
return filepath.SkipDir
108+
}
109+
110+
if !info.Mode().IsRegular() {
111+
return nil
112+
}
113+
114+
if !filenameRegexp.MatchString(path) {
115+
return nil
116+
}
117+
118+
if info.Name() == jpath.DefaultEntrypoint {
119+
return nil
120+
}
121+
122+
if info.IsDir() {
123+
return nil
124+
}
125+
126+
files = append(files, path)
127+
128+
return nil
129+
})
130+
if err != nil {
131+
return "", fmt.Errorf("walking directory: %w", err)
132+
}
133+
134+
importers := map[string]int{}
135+
for _, file := range files {
136+
importersList, err := FindImporterForFiles(root, []string{file})
137+
if err != nil {
138+
return "", fmt.Errorf("resolving imports: %w", err)
139+
}
140+
importers[file] = len(importersList)
141+
}
142+
143+
// Print sorted by count
144+
type importer struct {
145+
File string `json:"file"`
146+
Count int `json:"count"`
147+
}
148+
var importersList []importer
149+
for file, count := range importers {
150+
importersList = append(importersList, importer{File: file, Count: count})
151+
}
152+
sort.Slice(importersList, func(i, j int) bool {
153+
if importersList[i].Count == importersList[j].Count {
154+
return importersList[i].File < importersList[j].File
155+
}
156+
return importersList[i].Count > importersList[j].Count
157+
})
158+
159+
var sb strings.Builder
160+
for _, importer := range importersList {
161+
sb.WriteString(fmt.Sprintf("%s: %d\n", importer.File, importer.Count))
162+
}
163+
164+
return sb.String(), nil
165+
}
166+
85167
// expandSymlinksInFiles takes an array of files and adds to it:
86168
// - all symlinks that point to the files
87169
// - all files that are pointed to by the symlinks

pkg/jsonnet/find_importers_test.go

+47
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,53 @@ func TestFindImportersForFiles(t *testing.T) {
239239
}
240240
}
241241

242+
func TestCountImporters(t *testing.T) {
243+
testcases := []struct {
244+
name string
245+
dir string
246+
recursive bool
247+
fileRegexp string
248+
expected string
249+
}{
250+
{
251+
name: "project with no imports",
252+
dir: "testdata/findImporters/environments/no-imports",
253+
recursive: true,
254+
expected: "",
255+
},
256+
{
257+
name: "project with imports",
258+
dir: "testdata/findImporters/environments/imports-locals-and-vendored",
259+
recursive: true,
260+
expected: `testdata/findImporters/environments/imports-locals-and-vendored/local-file1.libsonnet: 1
261+
testdata/findImporters/environments/imports-locals-and-vendored/local-file2.libsonnet: 1
262+
`,
263+
},
264+
{
265+
name: "lib non-recursive",
266+
dir: "testdata/findImporters/lib/lib1",
267+
recursive: false,
268+
expected: `testdata/findImporters/lib/lib1/main.libsonnet: 1
269+
`,
270+
},
271+
{
272+
name: "lib recursive",
273+
dir: "testdata/findImporters/lib/lib1",
274+
recursive: true,
275+
expected: `testdata/findImporters/lib/lib1/main.libsonnet: 1
276+
testdata/findImporters/lib/lib1/subfolder/test.libsonnet: 0
277+
`,
278+
},
279+
}
280+
for _, tc := range testcases {
281+
t.Run(tc.name, func(t *testing.T) {
282+
count, err := CountImporters("testdata/findImporters", tc.dir, tc.recursive, tc.fileRegexp)
283+
require.NoError(t, err)
284+
require.Equal(t, tc.expected, count)
285+
})
286+
}
287+
}
288+
242289
func BenchmarkFindImporters(b *testing.B) {
243290
// Create a very large and complex project
244291
tempDir, err := filepath.EvalSymlinks(b.TempDir())
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

0 commit comments

Comments
 (0)