package external import ( "bytes" "fmt" "io" "os" "os/exec" "strings" "git.hypertheory-labs.dev/loom/loom-cli/internal/config" ) // PublishedDir is the only directory in somebody else's repository that list // looks at. What is not exported is not hidden — it is simply not what you // depend on. const PublishedDir = ".loom/published" // List enumerates a publisher's published surface. // // It shells out to git rather than using a host's REST API, because git is the // one interface gitea, GitHub and GitLab all speak the same way: their contents // APIs have three different URL shapes, three JSON shapes and three auth // schemes, and a private repository refuses the anonymous ones. The cost is that // git must be on PATH. func List(repoURL string, out io.Writer) error { cfg, err := config.Load() if err != nil { return err } dir, err := os.MkdirTemp("", "loomctl-list-") if err != nil { return err } defer os.RemoveAll(dir) clone := exec.Command("git", "clone", "--filter=blob:none", // trees only: we want names, not contents "--depth=1", // and bound the damage if the server ignores the filter "--no-checkout", "--quiet", repoURL, dir, ) clone.Env = gitEnv(cfg, repoURL) var stderr bytes.Buffer clone.Stderr = &stderr if err := clone.Run(); err != nil { return fmt.Errorf("git clone: %w\n%s", err, strings.TrimSpace(stderr.String())) } // git's fallback when a server refuses the filter is silent apart from this // warning, and the fallback is to download everything. if strings.Contains(stderr.String(), "filtering not recognized by server") { fmt.Fprintf(out, "warning: %s ignored --filter, so this fetched every blob at HEAD\n\n", repoURL) } ls := exec.Command("git", "-C", dir, "ls-tree", "--name-only", "HEAD:"+PublishedDir) var names, lsErr bytes.Buffer ls.Stdout, ls.Stderr = &names, &lsErr if err := ls.Run(); err != nil { fmt.Fprintf(out, "%s publishes nothing — no %s\n", repoURL, PublishedDir) return nil } for _, n := range strings.Split(strings.TrimSpace(names.String()), "\n") { if n != "" { fmt.Fprintln(out, n) } } return nil } // gitEnv passes credentials to git through the environment rather than through // -c on the command line, because argv is visible to every process on the // machine and an environment is not. func gitEnv(cfg *config.Config, repoURL string) []string { env := append(os.Environ(), "GIT_TERMINAL_PROMPT=0") host := hostOf(repoURL) if host == "" { return env } t := cfg.TokenFor(host) if t == "" { return env } return append(env, "GIT_CONFIG_COUNT=1", "GIT_CONFIG_KEY_0=http.extraHeader", "GIT_CONFIG_VALUE_0=Authorization: token "+t, ) } func hostOf(raw string) string { i := strings.Index(raw, "://") if i < 0 { return "" } rest := raw[i+3:] if at := strings.Index(rest, "@"); at >= 0 { rest = rest[at+1:] } if s := strings.IndexAny(rest, "/:"); s >= 0 { rest = rest[:s] } return rest }