a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
204 lines
5.1 KiB
Go
204 lines
5.1 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"path"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var (
|
|
filesFilter string
|
|
filesPattern string
|
|
filesFormat string
|
|
filesIndex string
|
|
)
|
|
|
|
var filesCmd = &cobra.Command{
|
|
Use: "files",
|
|
Short: "List the indexed files in the repo, filtered and formatted for the shell",
|
|
Long: `List the files the daemon has indexed for the repo, optionally filtered by a
|
|
name substring (--filter) or a path glob (--pattern), in one of three layouts:
|
|
|
|
--format tree a hierarchical directory tree (default)
|
|
--format flat one path per line (pipe-friendly)
|
|
--format grouped paths grouped under their directory`,
|
|
RunE: runFiles,
|
|
}
|
|
|
|
func init() {
|
|
filesCmd.Flags().StringVar(&filesFilter, "filter", "", "only files whose path contains this substring")
|
|
filesCmd.Flags().StringVar(&filesPattern, "pattern", "", "only files matching this path glob (e.g. 'internal/**/*.go')")
|
|
filesCmd.Flags().StringVar(&filesFormat, "format", "tree", "output layout: tree|flat|grouped")
|
|
filesCmd.Flags().StringVar(&filesIndex, "index", "", "repository path the daemon tracks (default: current directory)")
|
|
rootCmd.AddCommand(filesCmd)
|
|
}
|
|
|
|
func runFiles(cmd *cobra.Command, args []string) error {
|
|
switch filesFormat {
|
|
case "tree", "flat", "grouped":
|
|
default:
|
|
return fmt.Errorf("files: --format must be tree, flat, or grouped (got %q)", filesFormat)
|
|
}
|
|
|
|
repoPath := filesIndex
|
|
if repoPath == "" {
|
|
repoPath = "."
|
|
}
|
|
|
|
toolArgs := map[string]any{"limit": 5000}
|
|
if filesFilter != "" {
|
|
toolArgs["query"] = filesFilter
|
|
}
|
|
if filesPattern != "" {
|
|
toolArgs["glob"] = filesPattern
|
|
}
|
|
// find_files needs at least one of query/glob; default to "match every
|
|
// path" so a bare `gortex files` lists the whole tree.
|
|
if filesFilter == "" && filesPattern == "" {
|
|
toolArgs["glob"] = "**"
|
|
}
|
|
|
|
raw, err := requireDaemonTool(repoPath, "find_files", toolArgs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
paths := parseFindFilesPaths(raw)
|
|
fmt.Fprint(cmd.OutOrStdout(), renderFiles(paths, filesFormat))
|
|
return nil
|
|
}
|
|
|
|
// parseFindFilesPaths extracts the file paths from a find_files response
|
|
// (its `files[].path` list).
|
|
func parseFindFilesPaths(raw []byte) []string {
|
|
var resp struct {
|
|
Files []struct {
|
|
Path string `json:"path"`
|
|
} `json:"files"`
|
|
}
|
|
if json.Unmarshal(raw, &resp) != nil {
|
|
return nil
|
|
}
|
|
out := make([]string, 0, len(resp.Files))
|
|
for _, f := range resp.Files {
|
|
if f.Path != "" {
|
|
out = append(out, f.Path)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// renderFiles renders a path list in the requested layout.
|
|
func renderFiles(paths []string, format string) string {
|
|
switch format {
|
|
case "flat":
|
|
return renderFilesFlat(paths)
|
|
case "grouped":
|
|
return renderFilesGrouped(paths)
|
|
default:
|
|
return renderFilesTree(paths)
|
|
}
|
|
}
|
|
|
|
// renderFilesFlat lists every path on its own line, sorted.
|
|
func renderFilesFlat(paths []string) string {
|
|
sorted := append([]string(nil), paths...)
|
|
sort.Strings(sorted)
|
|
var b strings.Builder
|
|
for _, p := range sorted {
|
|
b.WriteString(path.Clean(toSlash(p)))
|
|
b.WriteByte('\n')
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// renderFilesGrouped groups paths under their parent directory, each directory
|
|
// a header followed by its files indented.
|
|
func renderFilesGrouped(paths []string) string {
|
|
groups := make(map[string][]string)
|
|
for _, p := range paths {
|
|
sp := toSlash(p)
|
|
dir := path.Dir(sp)
|
|
if dir == "." {
|
|
dir = "(root)"
|
|
}
|
|
groups[dir] = append(groups[dir], path.Base(sp))
|
|
}
|
|
dirs := make([]string, 0, len(groups))
|
|
for d := range groups {
|
|
dirs = append(dirs, d)
|
|
}
|
|
sort.Strings(dirs)
|
|
var b strings.Builder
|
|
for _, d := range dirs {
|
|
b.WriteString(d)
|
|
b.WriteString("/\n")
|
|
files := groups[d]
|
|
sort.Strings(files)
|
|
for _, f := range files {
|
|
b.WriteString(" ")
|
|
b.WriteString(f)
|
|
b.WriteByte('\n')
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// treeNode is a directory-tree node: a name keyed map of children plus a
|
|
// leaf flag.
|
|
type treeNode struct {
|
|
children map[string]*treeNode
|
|
isFile bool
|
|
}
|
|
|
|
func newTreeNode() *treeNode { return &treeNode{children: map[string]*treeNode{}} }
|
|
|
|
// renderFilesTree renders the paths as an indented directory tree, directories
|
|
// suffixed with "/", every level sorted for determinism.
|
|
func renderFilesTree(paths []string) string {
|
|
root := newTreeNode()
|
|
for _, p := range paths {
|
|
cur := root
|
|
for _, seg := range strings.Split(toSlash(p), "/") {
|
|
if seg == "" {
|
|
continue
|
|
}
|
|
child, ok := cur.children[seg]
|
|
if !ok {
|
|
child = newTreeNode()
|
|
cur.children[seg] = child
|
|
}
|
|
cur = child
|
|
}
|
|
cur.isFile = true
|
|
}
|
|
var b strings.Builder
|
|
renderTreeNode(&b, root, 0)
|
|
return b.String()
|
|
}
|
|
|
|
func renderTreeNode(b *strings.Builder, node *treeNode, depth int) {
|
|
names := make([]string, 0, len(node.children))
|
|
for name := range node.children {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
for _, name := range names {
|
|
child := node.children[name]
|
|
b.WriteString(strings.Repeat(" ", depth))
|
|
b.WriteString(name)
|
|
if len(child.children) > 0 {
|
|
b.WriteByte('/')
|
|
}
|
|
b.WriteByte('\n')
|
|
renderTreeNode(b, child, depth+1)
|
|
}
|
|
}
|
|
|
|
// toSlash normalises a path to forward slashes without depending on the host
|
|
// separator (the graph stores slash paths).
|
|
func toSlash(p string) string { return strings.ReplaceAll(p, "\\", "/") }
|