summaryrefslogtreecommitdiff
path: root/pkg
diff options
context:
space:
mode:
authorStefan Majewsky <majewsky@gmx.net>2017-05-07 22:46:38 +0200
committerStefan Majewsky <majewsky@gmx.net>2017-05-07 23:02:08 +0200
commit36b903423828b2e35c9c1fde3fdc898fe2ed23ca (patch)
tree22770bbade8c8e20a3b6f4e9f5a91ed9d5580f89 /pkg
parent385645d1a0bb9ae618e92d2e7a2ea6fe256b5ba2 (diff)
downloadgofu-36b903423828b2e35c9c1fde3fdc898fe2ed23ca.tar.gz
refactor towards new cli.Interface
Diffstat (limited to 'pkg')
-rw-r--r--pkg/cli/command.go (renamed from pkg/util/util.go)56
-rw-r--r--pkg/cli/interface.go157
-rw-r--r--pkg/cli/query.go77
-rw-r--r--pkg/cli/ui.go2
-rw-r--r--pkg/rtree/index.go146
-rw-r--r--pkg/rtree/main.go114
-rw-r--r--pkg/rtree/repo.go77
7 files changed, 405 insertions, 224 deletions
diff --git a/pkg/util/util.go b/pkg/cli/command.go
index 8cdf147..5b3ded5 100644
--- a/pkg/util/util.go
+++ b/pkg/cli/command.go
@@ -16,44 +16,48 @@
*
*******************************************************************************/
-package util
+package cli
import (
- "bufio"
"fmt"
- "os"
- "sort"
+ "io"
+ "os/exec"
"strings"
)
-//ShowSorted sorts the given lines and prints them on stdout.
-func ShowSorted(lines []string) {
- sort.Strings(lines)
- fmt.Println(strings.Join(lines, "\n"))
+//Command describes a command that can be run using the methods in the
+//Interface interface.
+type Command struct {
+ Program []string
+ WorkDir string
}
-//ShowError prints the given error on stderr if it is non-nil, or returns false otherwise.
-func ShowError(err error) bool {
- if err == nil {
- return false
- }
- fmt.Fprintf(os.Stderr, "ERROR: %s\n", err.Error())
- return true
+type commandError struct {
+ Cmd Command
+ Err error
}
-//FatalIfError prints the given error on stderr and exits with an error code.
-func FatalIfError(err error) {
- if err != nil {
- fmt.Fprintf(os.Stderr, "FATAL: %s\n", err.Error())
- os.Exit(255)
+func (e commandError) Error() string {
+ cmdline := strings.Join(e.Cmd.Program, " ")
+ if e.Cmd.WorkDir == "" {
+ return fmt.Sprintf("exec `%s`: %s",
+ cmdline, e.Err.Error(),
+ )
}
+ return fmt.Sprintf("exec `%s` in %s: %s",
+ cmdline, e.Cmd.WorkDir, e.Err.Error(),
+ )
}
-var stdin = bufio.NewReader(os.Stdin)
+func (c Command) run(stdout, stderr io.Writer) error {
+ cmd := exec.Command(c.Program[0], c.Program[1:]...)
+ cmd.Stdout = stdout
+ cmd.Stderr = stderr
+ cmd.Dir = c.WorkDir
-//ReadLine reads a line from stdin, with whitespace already trimmed.
-func ReadLine() string {
- input, err := stdin.ReadString('\n')
- FatalIfError(err)
- return strings.TrimSpace(input)
+ err := cmd.Run()
+ if err != nil {
+ err = commandError{c, err}
+ }
+ return err
}
diff --git a/pkg/cli/interface.go b/pkg/cli/interface.go
new file mode 100644
index 0000000..7ebde9d
--- /dev/null
+++ b/pkg/cli/interface.go
@@ -0,0 +1,157 @@
+/*******************************************************************************
+*
+* Copyright 2017 Stefan Majewsky <majewsky@gmx.net>
+*
+* This program is free software: you can redistribute it and/or modify it under
+* the terms of the GNU General Public License as published by the Free Software
+* Foundation, either version 3 of the License, or (at your option) any later
+* version.
+*
+* This program is distributed in the hope that it will be useful, but WITHOUT ANY
+* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+* A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License along with
+* this program. If not, see <http://www.gnu.org/licenses/>.
+*
+*******************************************************************************/
+
+package cli
+
+import (
+ "bufio"
+ "bytes"
+ "io"
+ "os"
+ "sort"
+ "strings"
+)
+
+//NewInterface creates an Interface instance.
+func NewInterface(stdin, stdout, stderr *os.File) (*Interface, error) {
+ //TODO: check terminal.IsTerminal(int(stdin.Fd())) and choose the TUI instance accordingly
+ return &Interface{
+ stdin: stdin,
+ stdout: stdout,
+ stderr: stderr,
+ stdinBuf: bufio.NewReader(stdin),
+ tui: &terminalTUI{},
+ }, nil
+}
+
+//Interface wraps access to the CLI, including input, output and subprocesses.
+type Interface struct {
+ //TODO: flag isStdinTerminal that disables color output and swaps out the TUI instance
+ stdin io.Reader
+ stdout io.Writer
+ stderr io.Writer
+ stdinBuf *bufio.Reader
+ tui TUI
+ //If this flag is set, only ShowResult() will write into stdout; everything
+ //else that usually goes to stdout goes to stderr instead.
+ //
+ //This is useful when gofu is expected to output a certain value to stdout
+ //which is used by the next program in the pipe, and additional output from
+ //subprocesses could confuse the stdout handler.
+ StdoutProtected bool
+}
+
+//TUI provides the interactive parts of the cli.Interface, so that these can be
+//easily swapped out for mock implementations in unit tests.
+type TUI interface {
+ //ReadLine reads a line from stdin (if tty: uses canonical mode).
+ ReadLine(prompt string) (string, error)
+ //Confirm displays a yes/no question and returns whether the user answered "yes".
+ Confirm(question string) (bool, error)
+ //Query displays a question and a set of answers and allows the user to select
+ //one of the answers. Returns the Return attribute of the selected Choice.
+ Query(prompt string, choices ...Choice) (string, error)
+}
+
+func (i *Interface) safeStdout() io.Writer {
+ if i.StdoutProtected {
+ return i.stderr
+ }
+ return i.stdout
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// input
+
+//ReadLine reads a line from stdin (if tty: uses canonical mode).
+func (i *Interface) ReadLine(prompt string) (string, error) {
+ return i.tui.ReadLine(prompt)
+}
+
+//Confirm displays a yes/no question and returns whether the user answered "yes".
+func (i *Interface) Confirm(question string) (bool, error) {
+ return i.tui.Confirm(question)
+}
+
+//Query displays a question and a set of answers and allows the user to select
+//one of the answers. Returns the Return attribute of the selected Choice.
+func (i *Interface) Query(prompt string, choices ...Choice) (string, error) {
+ return i.tui.Query(prompt, choices...)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// subprocesses
+
+//Run executes the given command on the same stdout and stderr.
+func (i *Interface) Run(c Command) error {
+ return c.run(i.safeStdout(), i.stderr)
+}
+
+//CaptureStdout executes the given command on the same stderr and captures its stdout.
+func (i *Interface) CaptureStdout(c Command) (string, error) {
+ var buf bytes.Buffer
+ err := c.run(&buf, i.stderr)
+ return string(buf.Bytes()), err
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// output
+
+//ShowResult displays the result of a computation on stdout.
+func (i *Interface) ShowResult(str string) {
+ str = strings.TrimSpace(str) + "\n"
+ i.stdout.Write([]byte(str))
+}
+
+//ShowResultsSorted calls ShowResult() on each of the results after sorting them.
+func (i *Interface) ShowResultsSorted(strs []string) {
+ sort.Strings(strs)
+ for _, str := range strs {
+ i.ShowResult(str)
+ }
+}
+
+//ShowProgress displays a progress message on stderr.
+func (i *Interface) ShowProgress(str string) {
+ i.stderr.Write(StyledText{
+ Styled(">> ", AnsiNormal, AnsiBold, AnsiCyan),
+ Styled(strings.TrimSpace(str)+"\n", AnsiCyan),
+ }.DisplayString(true))
+}
+
+//ShowWarning displays a warning message on stderr.
+func (i *Interface) ShowWarning(str string) {
+ i.stderr.Write(StyledText{
+ Styled("!! ", AnsiNormal, AnsiBold, AnsiYellow),
+ Styled(strings.TrimSpace(str)+"\n", AnsiYellow),
+ }.DisplayString(true))
+}
+
+//ShowError displays an error message on stderr.
+func (i *Interface) ShowError(err string) {
+ i.stderr.Write(StyledText{
+ Styled("!! ", AnsiNormal, AnsiBold, AnsiRed),
+ Styled(strings.TrimSpace(err)+"\n", AnsiRed),
+ }.DisplayString(true))
+}
+
+//ShowUsage displays a usage synopsis on stderr.
+func (i *Interface) ShowUsage(str string) {
+ str = strings.TrimSpace(str) + "\n"
+ i.stderr.Write([]byte(str))
+}
diff --git a/pkg/cli/query.go b/pkg/cli/query.go
index 40baaec..1f5d16c 100644
--- a/pkg/cli/query.go
+++ b/pkg/cli/query.go
@@ -21,29 +21,47 @@ package cli
import (
"fmt"
"io"
- "os"
"regexp"
"strings"
terminal "golang.org/x/crypto/ssh/terminal"
)
-//Confirm displays a yes/no question and returns whether the user answered "yes".
-func Confirm(question string) bool {
- os.Stdout.Write([]byte(strings.TrimSpace(question) + " [y/n] "))
+//cannot use `var errInterrupted = errors.New("Interrupted!")` because golint
+//complains about the formatting of the error message
+type errInterrupted struct{}
- buf := buffer{Input: os.Stdin}
+func (e errInterrupted) Error() string {
+ return "Interrupted!"
+}
+
+type terminalTUI struct {
+ i *Interface
+}
+
+func (t terminalTUI) ReadLine(prompt string) (string, error) {
+ if prompt != "" {
+ t.i.safeStdout().Write([]byte(strings.TrimSpace(prompt) + " "))
+ }
+ str, err := t.i.stdinBuf.ReadString('\n')
+ return strings.TrimSpace(str), err
+}
+
+func (t terminalTUI) Confirm(question string) (bool, error) {
+ out := t.i.safeStdout()
+ out.Write([]byte(strings.TrimSpace(question) + " [y/n] "))
+
+ buf := buffer{Input: t.i.stdin}
for {
switch string(buf.getNextInput()) {
case "y", "Y":
- os.Stdout.Write([]byte("-> yes\n"))
- return true
+ out.Write([]byte("-> yes\n"))
+ return true, nil
case "n", "N":
- os.Stdout.Write([]byte("-> no\n"))
- return false
+ out.Write([]byte("-> no\n"))
+ return false, nil
case "\x03": // Ctrl-C
- fmt.Fprintln(os.Stderr, "\nInterrupted!")
- os.Exit(255)
+ return false, errInterrupted{}
}
}
}
@@ -55,32 +73,32 @@ type Choice struct {
Shortcut byte
//The display string that describes this choice.
Text string
+ //The string to return from Interface.Query().
+ Return string
}
func (c Choice) hasShortcut() bool {
return c.Shortcut != '\000'
}
-//Query displays a question and a set of answers and allows the user to select
-//one of the answers. Returns the selected Choice instance, as well as its
-//index in the original choices list (starting from 0).
-func Query(prompt string, choices ...Choice) (Choice, int) {
+func (t terminalTUI) Query(prompt string, choices ...Choice) (string, error) {
if len(choices) == 0 {
- return Choice{}, -1
+ panic("no choices")
}
//disable line wrap; unexpected wrapping would confuse our cursor-moving code
- os.Stdout.Write([]byte("\x1B[?7l"))
- defer os.Stdout.Write([]byte("\x1B[?7h"))
+ out := t.i.safeStdout()
+ out.Write([]byte("\x1B[?7l"))
+ defer out.Write([]byte("\x1B[?7h"))
//display question
- os.Stdout.Write([]byte(strings.TrimSuffix(prompt, "\n") + "\n"))
+ out.Write([]byte(strings.TrimSuffix(prompt, "\n") + "\n"))
selected := 0
- buf := buffer{Input: os.Stdin}
+ buf := buffer{Input: t.i.stdin}
OUTER:
for {
- displayChoices(os.Stdout, choices, selected)
+ displayChoices(out, choices, selected)
input := buf.getNextInput()
if len(input) == 1 {
@@ -107,29 +125,28 @@ OUTER:
selected = len(choices) - 1
}
case "\x03": // Ctrl-C
- fmt.Fprintln(os.Stderr, "Interrupted!")
- os.Exit(255)
+ return "", errInterrupted{}
}
//prepare to re-render choices
- removeDisplayLines(len(choices))
+ removeDisplayLines(out, len(choices))
}
//clear query display
- removeDisplayLines(len(choices) + 1)
+ removeDisplayLines(out, len(choices)+1)
//display question + chosen answer
- fmt.Fprintf(os.Stdout, "%s -> %s\n",
+ fmt.Fprintf(out, "%s -> %s\n",
strings.TrimSuffix(prompt, "\n"),
strings.TrimSpace(choices[selected].Text),
)
- return choices[selected], selected
+ return choices[selected].Return, nil
}
-func removeDisplayLines(n int) {
+func removeDisplayLines(stdout io.Writer, n int) {
for idx := 0; idx < n; idx++ {
- os.Stdout.Write([]byte("\x1B[A\x1B[2K"))
+ stdout.Write([]byte("\x1B[A\x1B[2K"))
}
}
@@ -193,7 +210,7 @@ func (b *buffer) getNextInput() []byte {
defer terminal.Restore(0, oldState)
//fill buffer some more
- n, err := os.Stdin.Read(b.buf[b.fill:])
+ n, err := b.Input.Read(b.buf[b.fill:])
if err != nil {
panic(err)
}
diff --git a/pkg/cli/ui.go b/pkg/cli/ui.go
index c4eff69..60ae553 100644
--- a/pkg/cli/ui.go
+++ b/pkg/cli/ui.go
@@ -29,6 +29,8 @@ type AnsiStyle string
const (
//AnsiNormal reverts all styles to default.
AnsiNormal AnsiStyle = "0"
+ //AnsiBold makes the text bold.
+ AnsiBold AnsiStyle = "1"
//AnsiInverse swaps foreground and background color.
AnsiInverse AnsiStyle = "7"
//AnsiBlack is a foreground color.
diff --git a/pkg/rtree/index.go b/pkg/rtree/index.go
index dd1b979..c2c0202 100644
--- a/pkg/rtree/index.go
+++ b/pkg/rtree/index.go
@@ -23,14 +23,12 @@ import (
"fmt"
"io/ioutil"
"os"
- "os/exec"
"path"
"path/filepath"
"sort"
"strings"
"github.com/majewsky/gofu/pkg/cli"
- "github.com/majewsky/gofu/pkg/util"
yaml "gopkg.in/yaml.v2"
)
@@ -102,7 +100,7 @@ func (r reposByAbsPath) Less(i, j int) bool { return r[i].AbsolutePath() < r[j].
func (r reposByAbsPath) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
//Write writes the index file to disk.
-func (i *Index) Write() error {
+func (i *Index) Write(ci *cli.Interface) error {
buf, err := yaml.Marshal(i)
if err != nil {
return err
@@ -123,7 +121,9 @@ func (i *Index) Write() error {
warned := make(map[string]bool)
for _, repo := range i.Repos {
if seen[repo.CheckoutPath] && !warned[repo.CheckoutPath] {
- fmt.Fprintf(os.Stderr, "warning: repo %s appears multiple times in the index file!\n", repo.AbsolutePath())
+ ci.ShowWarning(
+ fmt.Sprintf("repo %s appears multiple times in the index file!", repo.AbsolutePath()),
+ )
warned[repo.CheckoutPath] = true
}
seen[repo.CheckoutPath] = true
@@ -132,8 +132,8 @@ func (i *Index) Write() error {
return nil
}
-//InteractiveRebuild implements the `rtree index` subcommand.
-func (i *Index) InteractiveRebuild() error {
+//Rebuild implements the `rtree index` subcommand.
+func (i *Index) Rebuild(ci *cli.Interface) error {
//check if existing index entries are still checked out
var newRepos []*Repo
for _, repo := range i.Repos {
@@ -161,32 +161,35 @@ func (i *Index) InteractiveRebuild() error {
remoteURLs = append(remoteURLs, remote.URL)
}
- var choice cli.Choice
+ var selection string
if len(remoteURLs) == 0 {
- choice, _ = cli.Query(
+ selection, err = ci.Query(
fmt.Sprintf("repository %s has been deleted; no remote to restore from", filepath.Join(RootPath, repo.CheckoutPath)),
- cli.Choice{Shortcut: 'd', Text: "delete from index"},
- cli.Choice{Shortcut: 's', Text: "skip"},
+ cli.Choice{Return: "d", Shortcut: 'd', Text: "delete from index"},
+ cli.Choice{Return: "s", Shortcut: 's', Text: "skip"},
)
} else {
- choice, _ = cli.Query(
+ selection, err = ci.Query(
fmt.Sprintf("repository %s has been deleted", filepath.Join(RootPath, repo.CheckoutPath)),
- cli.Choice{Shortcut: 'r', Text: "(r)estore from " + strings.Join(remoteURLs, " and ")},
- cli.Choice{Shortcut: 'd', Text: "delete from index"},
- cli.Choice{Shortcut: 's', Text: "skip"},
+ cli.Choice{Return: "r", Shortcut: 'r', Text: "(r)estore from " + strings.Join(remoteURLs, " and ")},
+ cli.Choice{Return: "d", Shortcut: 'd', Text: "delete from index"},
+ cli.Choice{Return: "s", Shortcut: 's', Text: "skip"},
)
}
+ if err != nil {
+ return err
+ }
- switch choice.Shortcut {
- case 'r':
- err := repo.Checkout()
+ switch selection {
+ case "r":
+ err := repo.Checkout(ci)
if err != nil {
return err
}
newRepos = append(newRepos, repo)
- case 'd':
+ case "d":
continue
- case 's':
+ case "s":
newRepos = append(newRepos, repo)
}
}
@@ -197,7 +200,7 @@ func (i *Index) InteractiveRebuild() error {
}
//index new repos
- err := ForeachPhysicalRepo(func(newRepo Repo) error {
+ err := ForeachPhysicalRepo(ci, func(newRepo Repo) error {
repo, exists := existingRepos[newRepo.CheckoutPath]
if exists {
//update the existing index entry with the new remotes
@@ -215,12 +218,10 @@ func (i *Index) InteractiveRebuild() error {
return nil
}
-var tenLetters = []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}
-
-//InteractiveFindRepo locates the repo with the given remote if it exists on
-//disk or (if allowClone is set) clones it and adds it to the index. This
-//is the meat of `rtree get`, and is also used by `rtree drop`.
-func (i *Index) InteractiveFindRepo(remoteURL string, allowClone bool) (*Repo, error) {
+//FindRepo locates the repo with the given remote if it exists on disk or (if
+//allowClone is set) clones it and adds it to the index. This is the meat of
+//`rtree get`, and is also used by `rtree drop`.
+func (i *Index) FindRepo(ci *cli.Interface, remoteURL string, allowClone bool) (*Repo, error) {
//make sure that stdout is not used for prompts
originalStdout := os.Stdout
os.Stdout = os.Stderr
@@ -255,7 +256,7 @@ func (i *Index) InteractiveFindRepo(remoteURL string, allowClone bool) (*Repo, e
if err != nil {
return nil, err
}
- fi, err := os.Stat(newRepo.AbsolutePath())
+ _, err = os.Stat(newRepo.AbsolutePath())
switch {
case err == nil:
return nil, fmt.Errorf(
@@ -272,12 +273,12 @@ func (i *Index) InteractiveFindRepo(remoteURL string, allowClone bool) (*Repo, e
//if no fork candidates found, clone as new repo
if len(candidates) == 0 {
- err := newRepo.Checkout()
+ err := newRepo.Checkout(ci)
if err != nil {
return nil, err
}
i.Repos = append(i.Repos, &newRepo)
- i.Write()
+ i.Write(ci)
return &newRepo, nil
}
@@ -288,49 +289,60 @@ func (i *Index) InteractiveFindRepo(remoteURL string, allowClone bool) (*Repo, e
}
choices := make([]cli.Choice, len(candidates)+1)
for idx, repo := range candidates {
- choices[idx] = cli.Choice{Text: "add as remote to " + repo.AbsolutePath()}
+ choices[idx] = cli.Choice{Text: "add as remote to " + repo.AbsolutePath(), Return: repo.CheckoutPath}
}
choices[len(candidates)] = cli.Choice{
+ Return: "clone",
Shortcut: 'n',
Text: "clone to " + newRepo.AbsolutePath(),
}
- choice, choiceIdx := cli.Query("Found possible fork candidates. What to do?", choices...)
+ selection, err := ci.Query("Found possible fork candidates. What to do?", choices...)
+ if err != nil {
+ return nil, err
+ }
- if choice.Shortcut == 'n' {
- err := newRepo.Checkout()
+ if selection == "clone" {
+ err := newRepo.Checkout(ci)
if err != nil {
return nil, err
}
i.Repos = append(i.Repos, &newRepo)
- i.Write()
+ i.Write(ci)
return &newRepo, nil
}
//find the repo selected by the user
- target := candidates[choiceIdx]
+ var target *Repo
+ for _, repo := range candidates {
+ if target.CheckoutPath == selection {
+ target = repo
+ break
+ }
+ }
//report the existing remotes, and ask for the name of the new remote
- fmt.Println("Existing remotes:")
+ prompt := "Existing remotes:\n"
for _, remote := range target.Remotes {
- fmt.Printf("\t(%s) %s\n", remote.Name, remote.URL)
+ prompt += fmt.Sprintf("\t(%s) %s\n", remote.Name, remote.URL)
+ }
+ prompt += fmt.Sprintf("Enter remote name for %s:", remoteURL)
+ remoteName, err := ci.ReadLine(prompt)
+ if err != nil {
+ return nil, err
}
- fmt.Printf("Enter remote name for %s: ", remoteURL)
- remoteName := util.ReadLine()
- cmd := exec.Command("git", "remote", "add", remoteName, remoteURL)
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- cmd.Dir = target.AbsolutePath()
- err = cmd.Run()
+ err = ci.Run(cli.Command{
+ Program: []string{"git", "remote", "add", remoteName, remoteURL},
+ WorkDir: target.AbsolutePath(),
+ })
if err != nil {
return nil, err
}
- cmd = exec.Command("git", "remote", "update", remoteName)
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- cmd.Dir = target.AbsolutePath()
- err = cmd.Run()
+ err = ci.Run(cli.Command{
+ Program: []string{"git", "remote", "update", remoteName},
+ WorkDir: target.AbsolutePath(),
+ })
if err != nil {
return nil, err
}
@@ -339,18 +351,18 @@ func (i *Index) InteractiveFindRepo(remoteURL string, allowClone bool) (*Repo, e
Name: remoteName,
URL: remoteURL,
})
- i.Write()
+ i.Write(ci)
return target, nil
}
-//InteractiveImportRepo moves the given repo into the rtree and adds it to the index.
-func (i *Index) InteractiveImportRepo(dirPath string) error {
+//ImportRepo moves the given repo into the rtree and adds it to the index.
+func (i *Index) ImportRepo(ci *cli.Interface, dirPath string) error {
//need to make dirPath absolute first
dirPath, err := filepath.Abs(dirPath)
if err != nil {
return err
}
- repo, err := NewRepoFromAbsolutePath(dirPath)
+ repo, err := NewRepoFromAbsolutePath(ci, dirPath)
if err != nil {
return err
}
@@ -373,7 +385,7 @@ func (i *Index) InteractiveImportRepo(dirPath string) error {
checkoutPath = thisPath
break
}
- choices[idx] = cli.Choice{Text: thisPath}
+ choices[idx] = cli.Choice{Return: thisPath, Text: thisPath}
}
//cannot decide myself -> let the user select
@@ -383,8 +395,10 @@ func (i *Index) InteractiveImportRepo(dirPath string) error {
}
question := fmt.Sprintf("Repo has multiple remotes. Where to put below %s?", RootPath)
- choice, _ := cli.Query(question, choices...)
- checkoutPath = choice.Text
+ checkoutPath, err = ci.Query(question, choices...)
+ if err != nil {
+ return err
+ }
}
//double-check that there is no such repo in the rtree yet
@@ -403,18 +417,18 @@ func (i *Index) InteractiveImportRepo(dirPath string) error {
return nil
}
-//InteractiveDropRepo deletes the given repo from the rtree and removes it from
-//the index.
-func (i *Index) InteractiveDropRepo(repo *Repo) error {
- ok := repo.InteractiveExec("git", "status")
- if !ok {
- return nil
+//DropRepo deletes the given repo from the rtree and removes it from the index.
+func (i *Index) DropRepo(ci *cli.Interface, repo *Repo) error {
+ err := repo.Exec(ci, "git", "status")
+ if err != nil {
+ return err
}
- if !cli.Confirm(">> Drop this repo?") {
- return nil
+ ok, err := ci.Confirm(">> Drop this repo?")
+ if !ok || err != nil {
+ return err
}
- err := os.RemoveAll(repo.AbsolutePath())
+ err = os.RemoveAll(repo.AbsolutePath())
if err != nil {
return err
}
@@ -426,5 +440,5 @@ func (i *Index) InteractiveDropRepo(repo *Repo) error {
}
}
i.Repos = reposNew
- return nil
+ return i.Write(ci)
}
diff --git a/pkg/rtree/main.go b/pkg/rtree/main.go
index d57b399..42e540e 100644
--- a/pkg/rtree/main.go
+++ b/pkg/rtree/main.go
@@ -19,150 +19,146 @@
package rtree
import (
- "fmt"
- "os"
+ "strings"
- "github.com/majewsky/gofu/pkg/util"
+ "github.com/majewsky/gofu/pkg/cli"
)
//Exec executes the rtree applet and does not return. The argument is os.Args
//minus the leading "rtree" or "gofu rtree".
-func Exec(args []string) int {
+func Exec(ci *cli.Interface, args []string) int {
if len(args) == 0 {
- return usage()
+ return usage(ci)
}
index, errs := ReadIndex()
if len(errs) > 0 {
for _, err := range errs {
- util.ShowError(err)
+ ci.ShowError(err.Error())
}
- return 255
+ return 1
}
var err error
switch args[0] {
case "get":
if len(args) != 2 {
- return usage()
+ return usage(ci)
}
- err = commandGet(index, args[1])
+ err = commandGet(ci, index, args[1])
case "drop":
if len(args) != 2 {
- return usage()
+ return usage(ci)
}
- err = commandDrop(index, args[1])
+ err = commandDrop(ci, index, args[1])
case "index":
if len(args) != 1 {
- return usage()
+ return usage(ci)
}
- err = commandIndex(index)
+ err = commandIndex(ci, index)
case "repos":
if len(args) != 1 {
- return usage()
+ return usage(ci)
}
- commandRepos(index)
+ commandRepos(ci, index)
case "remotes":
if len(args) != 1 {
- return usage()
+ return usage(ci)
}
- commandRemotes(index)
+ commandRemotes(ci, index)
case "import":
if len(args) != 2 {
- return usage()
+ return usage(ci)
}
- err = commandImport(index, args[1])
+ err = commandImport(ci, index, args[1])
case "each":
if len(args) < 2 {
- return usage()
+ return usage(ci)
}
- return commandEach(index, args[1], args[2:])
+ return commandEach(ci, index, args[1:])
default:
- return usage()
+ return usage(ci)
}
if err == nil {
return 0
}
- util.ShowError(err)
+ ci.ShowError(err.Error())
return 1
}
-func usage() int {
- fmt.Fprintln(os.Stderr, "Usage:")
- fmt.Fprintln(os.Stderr, " rtree [get|drop] <url>")
- fmt.Fprintln(os.Stderr, " rtree [index|repos|remotes]")
- fmt.Fprintln(os.Stderr, " rtree import <path>")
- fmt.Fprintln(os.Stderr, " rtree each <command>")
+var usageStr = strings.TrimSpace(`
+Usage:
+ rtree [get|drop] <url>
+ rtree [index|repos|remotes]
+ rtree import <path>
+ rtree each <command>
+`)
+
+func usage(ci *cli.Interface) int {
+ ci.ShowUsage(usageStr)
return 1
}
-func commandGet(index *Index, url string) error {
- repo, err := index.InteractiveFindRepo(url, true)
+func commandGet(ci *cli.Interface, index *Index, url string) error {
+ repo, err := index.FindRepo(ci, url, true)
if err != nil {
return err
}
- fmt.Println(repo.AbsolutePath())
+ ci.ShowResult(repo.AbsolutePath())
return nil
}
-func commandDrop(index *Index, url string) error {
- repo, err := index.InteractiveFindRepo(url, true)
- if err != nil {
- return err
- }
- err = index.InteractiveDropRepo(repo)
+func commandDrop(ci *cli.Interface, index *Index, url string) error {
+ repo, err := index.FindRepo(ci, url, true)
if err != nil {
return err
}
- return index.Write()
+ return index.DropRepo(ci, repo)
}
-func commandIndex(index *Index) error {
- err := index.InteractiveRebuild()
+func commandIndex(ci *cli.Interface, index *Index) error {
+ err := index.Rebuild(ci)
if err != nil {
return err
}
- return index.Write()
+ return index.Write(ci)
}
-func commandRepos(index *Index) {
+func commandRepos(ci *cli.Interface, index *Index) {
var items []string
for _, repo := range index.Repos {
items = append(items, repo.CheckoutPath)
}
- util.ShowSorted(items)
+ ci.ShowResultsSorted(items)
}
-func commandRemotes(index *Index) {
+func commandRemotes(ci *cli.Interface, index *Index) {
var items []string
for _, repo := range index.Repos {
for _, remote := range repo.Remotes {
items = append(items, remote.URL)
}
}
- util.ShowSorted(items)
+ ci.ShowResultsSorted(items)
}
-func commandEach(index *Index, command string, args []string) int {
- allOK := true
+func commandEach(ci *cli.Interface, index *Index, cmdline []string) (exitCode int) {
+ exitCode = 0
for _, repo := range index.Repos {
- ok := repo.InteractiveExec(command, args...)
- if !ok {
- allOK = false
+ err := repo.Exec(ci, cmdline...)
+ if err != nil {
+ ci.ShowError(err.Error())
+ exitCode = 1
}
}
-
- if allOK {
- return 0
- }
- return 1
+ return
}
-func commandImport(index *Index, dirPath string) error {
- err := index.InteractiveImportRepo(dirPath)
+func commandImport(ci *cli.Interface, index *Index, dirPath string) error {
+ err := index.ImportRepo(ci, dirPath)
if err != nil {
return err
}
- return index.Write()
+ return index.Write(ci)
}
diff --git a/pkg/rtree/repo.go b/pkg/rtree/repo.go
index 12afee0..fba658b 100644
--- a/pkg/rtree/repo.go
+++ b/pkg/rtree/repo.go
@@ -19,14 +19,14 @@
package rtree
import (
- "bytes"
"errors"
"fmt"
"os"
- "os/exec"
"path/filepath"
"regexp"
"strings"
+
+ "github.com/majewsky/gofu/pkg/cli"
)
//RootPath is the directory below which all repositories are located. Its value
@@ -64,23 +64,22 @@ func (r Repo) AbsolutePath() string {
//NewRepoFromAbsolutePath initializes a Repo instance by scanning the existing
//checkout at the given path.
-func NewRepoFromAbsolutePath(path string) (repo Repo, err error) {
+func NewRepoFromAbsolutePath(ci *cli.Interface, path string) (repo Repo, err error) {
repo.CheckoutPath, err = filepath.Rel(RootPath, path)
if err != nil {
return
}
//list remotes
- cmd := exec.Command("git", "-C", path, "config", "-l")
- var buf bytes.Buffer
- cmd.Stdout = &buf
- cmd.Stderr = os.Stderr
- err = cmd.Run()
+ out, err := ci.CaptureStdout(cli.Command{
+ Program: []string{"git", "config", "-l"},
+ WorkDir: path,
+ })
if err != nil {
- return repo, fmt.Errorf("exec `git config -l` in %s: %s", path, err.Error())
+ return
}
- for _, line := range strings.Split(string(buf.Bytes()), "\n") {
+ for _, line := range strings.Split(out, "\n") {
match := remoteConfigRx.FindStringSubmatch(line)
if match == nil {
continue
@@ -113,7 +112,7 @@ var remoteConfigRx = regexp.MustCompile(`remote\.([^=]+)\.url=(.+)`)
//ForeachPhysicalRepo walks over the repository tree, executing the action
//function once for every repo encountered (but *not* for repos contained
//within other repos, e.g. submodules).
-func ForeachPhysicalRepo(action func(repo Repo) error) error {
+func ForeachPhysicalRepo(ci *cli.Interface, action func(repo Repo) error) error {
return filepath.Walk(RootPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
@@ -128,7 +127,7 @@ func ForeachPhysicalRepo(action func(repo Repo) error) error {
}
//appears to be a repo
- repo, err := NewRepoFromAbsolutePath(path)
+ repo, err := NewRepoFromAbsolutePath(ci, path)
if err == nil {
err = action(repo)
}
@@ -142,7 +141,7 @@ func ForeachPhysicalRepo(action func(repo Repo) error) error {
//Checkout creates the repo in the given path with the given remotes. The
//working copy will only be initialized if there is an "origin" remote.
-func (r Repo) Checkout() error {
+func (r Repo) Checkout(ci *cli.Interface) error {
//check if we have an "origin" remote to clone from
var originURL string
for _, remote := range r.Remotes {
@@ -153,19 +152,17 @@ func (r Repo) Checkout() error {
}
if originURL == "" {
- cmd := exec.Command("git", "init", r.AbsolutePath())
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- err := cmd.Run()
+ err := ci.Run(cli.Command{
+ Program: []string{"git", "init", r.AbsolutePath()},
+ })
if err != nil {
return err
}
- fmt.Fprintln(os.Stderr, "warning: will not checkout anything since there is no remote named \"origin\"")
+ ci.ShowWarning(`will not checkout anything since there is no remote named "origin"`)
} else {
- cmd := exec.Command("git", "clone", originURL, r.AbsolutePath())
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- err := cmd.Run()
+ err := ci.Run(cli.Command{
+ Program: []string{"git", "clone", originURL, r.AbsolutePath()},
+ })
if err != nil {
return err
}
@@ -174,10 +171,10 @@ func (r Repo) Checkout() error {
remotesAdded := false
for _, remote := range r.Remotes {
if remote.Name != "origin" {
- cmd := exec.Command("git", "-C", r.AbsolutePath(), "remote", "add", remote.Name, remote.URL)
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- err := cmd.Run()
+ err := ci.Run(cli.Command{
+ Program: []string{"git", "remote", "add", remote.Name, remote.URL},
+ WorkDir: r.AbsolutePath(),
+ })
if err != nil {
return err
}
@@ -185,29 +182,23 @@ func (r Repo) Checkout() error {
}
}
if remotesAdded {
- cmd := exec.Command("git", "-C", r.AbsolutePath(), "remote", "update")
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- return cmd.Run()
+ return ci.Run(cli.Command{
+ Program: []string{"git", "remote", "update"},
+ WorkDir: r.AbsolutePath(),
+ })
}
return nil
}
-//InteractiveExec implements the meat of the `rtree exec` command. It returns
+//Exec implements the meat of the `rtree exec` command. It returns
//true iff the command exited successfully.
-func (r Repo) InteractiveExec(command string, args ...string) (ok bool) {
- fmt.Fprintf(os.Stdout, "\x1B[1;36m>> \x1B[0;36m%s\x1B[0m\n", r.AbsolutePath())
- cmd := exec.Command(command, args...)
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- cmd.Dir = r.AbsolutePath()
- err := cmd.Run()
- if err != nil {
- fmt.Fprintf(os.Stderr, "\x1B[1;31m!! \x1B[0;31m%s\x1B[0m\n", err.Error())
- return false
- }
- return true
+func (r Repo) Exec(ci *cli.Interface, cmdline ...string) error {
+ ci.ShowProgress(r.AbsolutePath())
+ return ci.Run(cli.Command{
+ Program: cmdline,
+ WorkDir: r.AbsolutePath(),
+ })
}
//Move sets the CheckoutPath to the given value and moves the existing repo