package main import ( "os" "path/filepath" "strings" "github.com/chzyer/readline" ) // completer wires up Tab completion. Command names complete at the start of the // line; cd/open/view complete local project names; clone/show complete // repository names cached from the git server; checkout/tag complete branch and // tag names; dist completes filesystem paths. func completer() *readline.PrefixCompleter { return readline.NewPrefixCompleter( readline.PcItem("cd", readline.PcItemDynamic(dynLocalProjects)), readline.PcItem("open", readline.PcItemDynamic(dynLocalProjects)), readline.PcItem("view", readline.PcItemDynamic(dynLocalProjects)), readline.PcItem("clone", readline.PcItem("-a", readline.PcItemDynamic(dynServerArchives)), readline.PcItemDynamic(dynServerRepos), ), readline.PcItem("cloneall"), readline.PcItem("show", readline.PcItemDynamic(dynServerRepos)), readline.PcItem("list", readline.PcItem("-a")), readline.PcItem("push"), readline.PcItem("pushremote", readline.PcItemDynamic(dynRemoteNames)), readline.PcItem("release", readline.PcItemDynamic(dynRemoteNames)), readline.PcItem("pull"), readline.PcItem("fetch"), readline.PcItem("status", readline.PcItem("-a")), readline.PcItem("overview"), readline.PcItem("diff"), readline.PcItem("init"), readline.PcItem("edit"), readline.PcItem("checkout", readline.PcItemDynamic(dynCheckout)), readline.PcItem("log"), readline.PcItem("archive"), readline.PcItem("dist", readline.PcItemDynamic(dynPaths)), readline.PcItem("login"), readline.PcItem("count"), readline.PcItem("tag", readline.PcItem("add"), readline.PcItem("checkout", readline.PcItemDynamic(dynTags)), readline.PcItem("delete", readline.PcItemDynamic(dynTags)), ), readline.PcItem("alias", readline.PcItemDynamic(dynAliasNames)), readline.PcItem("unalias", readline.PcItemDynamic(dynAliasNames)), readline.PcItem("config", readline.PcItem("-k")), readline.PcItem("rescan"), readline.PcItem("help"), readline.PcItem("quit"), readline.PcItem("exit"), ) } // dynLocalProjects lists project directories under BASE (already name-sorted). func dynLocalProjects(string) []string { entries, err := os.ReadDir(BASE) if err != nil { return nil } var out []string for _, e := range entries { if e.IsDir() && !strings.HasPrefix(e.Name(), ".") { out = append(out, e.Name()) } } return out } var ( serverRepos []string serverArchives []string serverFetched bool ) // fetchServerRepos queries the server once and caches the repository/archive // names for Tab completion. func fetchServerRepos() { if serverFetched { return } repos, err := serverRepoNames() if err != nil { // a transient failure (server down, no network) must not cache an // empty list for the rest of the session — the next Tab tries again return } // a missing ./archive is a permanent, unremarkable state: still cache var archives []string if lines, err := sshOut("/bin/ls archive"); err == nil { for _, ln := range lines { t := strings.TrimSpace(ln) if strings.HasSuffix(t, ".git.tar.gz") { archives = append(archives, strings.TrimSuffix(t, ".git.tar.gz")) } } } serverRepos, serverArchives = repos, archives serverFetched = true } // rescanServer clears the cached server listing so the next completion (or use) // re-fetches it. func rescanServer() { serverFetched = false serverRepos = nil serverArchives = nil } func dynServerRepos(string) []string { fetchServerRepos(); return serverRepos } func dynServerArchives(string) []string { fetchServerRepos(); return serverArchives } // dynRemoteNames offers the configured mirror targets as `@name` selectors for // `pushremote`, resolved against the active project's configuration. func dynRemoteNames(string) []string { targets, _ := cfg.mirrorTargets() var out []string for _, t := range targets { out = append(out, "@"+t.Name) } return out } // dynBranches / dynTags list the active project's local branches / tags. func dynBranches(string) []string { if !isDir(DIR + "/.git") { return nil } out, _ := gitCapture(DIR, "for-each-ref", "--format=%(refname:short)", "refs/heads") return splitLines(out) } func dynTags(string) []string { if !isDir(DIR + "/.git") { return nil } out, _ := gitCapture(DIR, "tag", "-l") return splitLines(out) } // dynCheckout offers both branches and tags for `checkout`. func dynCheckout(line string) []string { return append(dynBranches(line), dynTags(line)...) } // dynPaths completes filesystem paths for the last token on the line. func dynPaths(line string) []string { partial := "" if !strings.HasSuffix(line, " ") { if f := strings.Fields(line); len(f) > 0 { partial = f[len(f)-1] } } dir := "." if partial != "" { if strings.HasSuffix(partial, "/") { dir = partial } else { dir = filepath.Dir(partial) } } entries, err := os.ReadDir(dir) if err != nil { return nil } var out []string for _, e := range entries { if strings.HasPrefix(e.Name(), ".") { continue } p := filepath.Join(dir, e.Name()) if e.IsDir() { p += "/" } out = append(out, p) } return out }