From ed2b9374599cc00467b1c27121318748cc0d3f49 Mon Sep 17 00:00:00 2001 From: Stefan Majewsky Date: Sat, 28 Mar 2020 13:53:10 +0100 Subject: rename pkg/ to internal/ --- pkg/cli/command.go | 70 -------- pkg/cli/interface.go | 168 ------------------ pkg/cli/query.go | 295 -------------------------------- pkg/i3status/battery.go | 75 --------- pkg/i3status/main.go | 169 ------------------- pkg/i3status/net.go | 61 ------- pkg/prompt/cloud.go | 83 --------- pkg/prompt/git.go | 138 --------------- pkg/prompt/login.go | 66 -------- pkg/prompt/main.go | 123 -------------- pkg/prompt/misc.go | 43 ----- pkg/prompt/pwd.go | 157 ----------------- pkg/rtree/get_test.go | 140 --------------- pkg/rtree/index.go | 431 ----------------------------------------------- pkg/rtree/init.go | 99 ----------- pkg/rtree/main.go | 171 ------------------- pkg/rtree/remote.go | 75 --------- pkg/rtree/repo.go | 225 ------------------------- pkg/rtree/shared_test.go | 204 ---------------------- 19 files changed, 2793 deletions(-) delete mode 100644 pkg/cli/command.go delete mode 100644 pkg/cli/interface.go delete mode 100644 pkg/cli/query.go delete mode 100644 pkg/i3status/battery.go delete mode 100644 pkg/i3status/main.go delete mode 100644 pkg/i3status/net.go delete mode 100644 pkg/prompt/cloud.go delete mode 100644 pkg/prompt/git.go delete mode 100644 pkg/prompt/login.go delete mode 100644 pkg/prompt/main.go delete mode 100644 pkg/prompt/misc.go delete mode 100644 pkg/prompt/pwd.go delete mode 100644 pkg/rtree/get_test.go delete mode 100644 pkg/rtree/index.go delete mode 100644 pkg/rtree/init.go delete mode 100644 pkg/rtree/main.go delete mode 100644 pkg/rtree/remote.go delete mode 100644 pkg/rtree/repo.go delete mode 100644 pkg/rtree/shared_test.go (limited to 'pkg') diff --git a/pkg/cli/command.go b/pkg/cli/command.go deleted file mode 100644 index a9938d1..0000000 --- a/pkg/cli/command.go +++ /dev/null @@ -1,70 +0,0 @@ -/******************************************************************************* -* -* Copyright 2017 Stefan Majewsky -* -* 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 . -* -*******************************************************************************/ - -package cli - -import ( - "fmt" - "io" - "os/exec" - "strings" -) - -//Command describes a command that can be run using the methods in the -//Implementation interface. -type Command struct { - Program []string - WorkDir string -} - -type commandError struct { - Cmd Command - Err error -} - -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(), - ) -} - -//CommandRunner is a function that can execute commands given to it. -//This interface is only useful for unit tests; the default CommandRunner -//suffices for all regular operation. -type CommandRunner func(c Command, stdin io.Reader, stdout, stderr io.Writer) error - -//DefaultCommandRunner is a CommandRunner that actually executes the command. -func DefaultCommandRunner(c Command, stdin io.Reader, stdout, stderr io.Writer) error { - cmd := exec.Command(c.Program[0], c.Program[1:]...) - cmd.Stdin = stdin - cmd.Stdout = stdout - cmd.Stderr = stderr - cmd.Dir = c.WorkDir - - err := cmd.Run() - if err != nil { - err = commandError{c, err} - } - return err -} diff --git a/pkg/cli/interface.go b/pkg/cli/interface.go deleted file mode 100644 index a88212b..0000000 --- a/pkg/cli/interface.go +++ /dev/null @@ -1,168 +0,0 @@ -/******************************************************************************* -* -* Copyright 2017 Stefan Majewsky -* -* 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 . -* -*******************************************************************************/ - -package cli - -import ( - "bufio" - "bytes" - "fmt" - "io" - "os" - "sort" - "strings" - - "golang.org/x/crypto/ssh/terminal" -) - -//Interface wraps access to the CLI, including input, output and subprocesses. -var Interface *Implementation - -func init() { - SetupInterface(os.Stdin, os.Stdout, os.Stderr, DefaultCommandRunner) -} - -//SetupInterface prepares the Interface instance with nonstandard file streams -//or a nonstandard CommandRunner. This is only required for unit tests. -func SetupInterface(stdin io.Reader, stdout, stderr io.Writer, commandRunner CommandRunner) { - Interface = &Implementation{ - stdin: stdin, - stdout: stdout, - stderr: stderr, - stdinBuf: bufio.NewReader(stdin), - commandRunner: commandRunner, - } - - if stdinFile, ok := stdin.(*os.File); ok && terminal.IsTerminal(int(stdinFile.Fd())) { - Interface.tui = &terminalTUI{Interface} - } else { - Interface.tui = &pipeTUI{Interface} - } -} - -//Implementation wraps access to the CLI, including input, output and subprocesses. -type Implementation struct { - stdin io.Reader - stdout io.Writer - stderr io.Writer - stdinBuf *bufio.Reader - tui TUI - commandRunner CommandRunner - //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.Implementation, 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) - //Print writes the given string (potentially including ANSI escape codes) to - //the given writer. At this point, it can be decided whether to strip out the - //ANSI escape codes. - Print(w io.Writer, msg string) -} - -func (i *Implementation) 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 *Implementation) 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 *Implementation) 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 *Implementation) 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 *Implementation) Run(c Command) error { - return i.commandRunner(c, nil, i.safeStdout(), i.stderr) -} - -//CaptureStdout executes the given command on the same stderr and captures its stdout. -func (i *Implementation) CaptureStdout(c Command) (string, error) { - var buf bytes.Buffer - err := i.commandRunner(c, nil, &buf, i.stderr) - return string(buf.Bytes()), err -} - -//////////////////////////////////////////////////////////////////////////////// -// output - -//ShowResult displays the result of a computation on stdout. -func (i *Implementation) 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 *Implementation) ShowResultsSorted(strs []string) { - sort.Strings(strs) - for _, str := range strs { - i.ShowResult(str) - } -} - -//ShowProgress displays a progress message on stderr. -func (i *Implementation) ShowProgress(str string) { - i.tui.Print(i.stderr, fmt.Sprintf("\x1B[0;1;36m>>\x1B[0;36m %s\x1B[0m\n", strings.TrimSpace(str))) -} - -//ShowWarning displays a warning message on stderr. -func (i *Implementation) ShowWarning(str string) { - i.tui.Print(i.stderr, fmt.Sprintf("\x1B[0;1;33m!!\x1B[0;33m %s\x1B[0m\n", strings.TrimSpace(str))) -} - -//ShowError displays an error message on stderr. -func (i *Implementation) ShowError(str string) { - i.tui.Print(i.stderr, fmt.Sprintf("\x1B[0;1;31m!!\x1B[0;31m %s\x1B[0m\n", strings.TrimSpace(str))) -} - -//ShowUsage displays a usage synopsis on stderr. -func (i *Implementation) 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 deleted file mode 100644 index add170c..0000000 --- a/pkg/cli/query.go +++ /dev/null @@ -1,295 +0,0 @@ -/******************************************************************************* -* -* Copyright 2017 Stefan Majewsky -* -* 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 . -* -*******************************************************************************/ - -package cli - -import ( - "errors" - "fmt" - "io" - "regexp" - "strconv" - "strings" - - terminal "golang.org/x/crypto/ssh/terminal" -) - -//cannot use `var errInterrupted = errors.New("Interrupted!")` because golint -//complains about the formatting of the error message -type errInterrupted struct{} - -func (e errInterrupted) Error() string { - return "Interrupted!" -} - -//////////////////////////////////////////////////////////////////////////////// -// TUI implementation for when stdin is a terminal - -type terminalTUI struct { - i *Implementation -} - -func (t terminalTUI) Print(w io.Writer, msg string) { - w.Write([]byte(msg)) -} - -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": - out.Write([]byte("-> yes\n")) - return true, nil - case "n", "N": - out.Write([]byte("-> no\n")) - return false, nil - case "\x03": // Ctrl-C - return false, errInterrupted{} - } - } -} - -//Choice is a thing that the user can choose during Query(). -type Choice struct { - //If given, the choice can be selected by pressing the key that - //produces this character. - Shortcut byte - //The display string that describes this choice. - Text string - //The string to return from Implementation.Query(). - Return string -} - -func (c Choice) hasShortcut() bool { - return c.Shortcut != '\000' -} - -func (t terminalTUI) Query(prompt string, choices ...Choice) (string, error) { - if len(choices) == 0 { - panic("no choices") - } - - //disable line wrap; unexpected wrapping would confuse our cursor-moving code - out := t.i.safeStdout() - out.Write([]byte("\x1B[?7l")) - defer out.Write([]byte("\x1B[?7h")) - - //display question - out.Write([]byte(strings.TrimSuffix(prompt, "\n") + "\n")) - selected := 0 - - buf := buffer{Input: t.i.stdin} -OUTER: - for { - displayChoices(out, choices, selected) - - input := buf.getNextInput() - if len(input) == 1 { - //single character entered - check if a shortcut matches - for idx, choice := range choices { - if choice.Shortcut == input[0] { - selected = idx - break OUTER - } - } - } - - switch string(input) { - case "\r", "\n": - break OUTER - case "\x1B[A": // Up arrow key - selected-- - if selected < 0 { - selected = 0 - } - case "\x1B[B": // Down arrow key - selected++ - if selected >= len(choices) { - selected = len(choices) - 1 - } - case "\x03": // Ctrl-C - return "", errInterrupted{} - } - - //prepare to re-render choices - removeDisplayLines(out, len(choices)) - } - - //clear query display - removeDisplayLines(out, len(choices)+1) - - //display question + chosen answer - fmt.Fprintf(out, "%s -> %s\n", - strings.TrimSuffix(prompt, "\n"), - strings.TrimSpace(choices[selected].Text), - ) - - return choices[selected].Return, nil -} - -func removeDisplayLines(stdout io.Writer, n int) { - for idx := 0; idx < n; idx++ { - stdout.Write([]byte("\x1B[A\x1B[2K")) - } -} - -func displayChoices(out io.Writer, choices []Choice, selectedIndex int) { - hasShortcuts := false - for _, choice := range choices { - if choice.hasShortcut() { - hasShortcuts = true - break - } - } - - for idx, choice := range choices { - text := " " + strings.TrimSpace(choice.Text) + " \n" - if hasShortcuts { - shortcut := choice.Shortcut - if !choice.hasShortcut() { - shortcut = ' ' - } - text = fmt.Sprintf(" [%c]%s", shortcut, text) - } - - if idx == selectedIndex { - fmt.Fprintf(out, "\x1B[0;7m%s\x1B[0m", text) - } else { - out.Write([]byte(text)) - } - } -} - -var ansiEscapeRx = regexp.MustCompile(`^\x1B\[[\x20-\x3F]*[\x40-\x7E]`) - -type buffer struct { - Input io.Reader - buf [128]byte - fill int -} - -func (b *buffer) getNextInput() []byte { - //do we have a simple input character? - if b.fill > 0 && b.buf[0] != '\x1B' { - result := append([]byte(nil), b.buf[0]) - copy(b.buf[0:], b.buf[1:]) - b.fill-- - return result - } - - //do we have a full ANSI escape sequence? - match := ansiEscapeRx.Find(b.buf[0:b.fill]) - if match != nil { - result := append([]byte(nil), match...) - copy(b.buf[0:], b.buf[len(match):]) - b.fill -= len(match) - return result - } - - oldState, err := terminal.MakeRaw(0) - if err != nil { - panic(err) - } - defer terminal.Restore(0, oldState) - - //fill buffer some more - n, err := b.Input.Read(b.buf[b.fill:]) - if err != nil { - panic(err) - } - b.fill += n - - return b.getNextInput() //restart -} - -//////////////////////////////////////////////////////////////////////////////// -// TUI implementation for when stdin is a pipe (also used in unit tests) - -type pipeTUI struct { - i *Implementation -} - -//AnsiColorCodeRx is a regexp that matches ANSI escape sequences of the type SGR. -var AnsiColorCodeRx = regexp.MustCompile("\x1B" + `\[[0-9;]*m`) - -func (t *pipeTUI) Print(w io.Writer, msg string) { - w.Write([]byte(AnsiColorCodeRx.ReplaceAllString(msg, ""))) -} - -func (t *pipeTUI) ReadLine(prompt string) (string, error) { - str, err := t.i.stdinBuf.ReadString('\n') - str = strings.TrimSpace(str) - if err == nil { - fmt.Fprintf(t.i.stderr, "%s %s\n", strings.TrimSpace(prompt), str) - } - return str, err -} - -func (t *pipeTUI) Confirm(question string) (bool, error) { - question = strings.TrimSpace(question) - str, err := t.ReadLine(question) - if err != nil { - return false, err - } - //recognize /[01tTfF]|true|false/ - ok, err := strconv.ParseBool(str) - if err != nil { - //recognize /[yY]|yes|no/ - ok = strings.HasPrefix(question, "y") || strings.HasPrefix(question, "Y") - } - fmt.Fprintf(t.i.stderr, "%s -> %v (%s)\n", question, ok, str) - return ok, nil -} - -//Query for the pipe TUI is limited to exact matches of the choice text, or -//matching by choice shortcut. -func (t *pipeTUI) Query(prompt string, choices ...Choice) (string, error) { - str, err := t.i.stdinBuf.ReadString('\n') - if err != nil { - return str, err - } - str = strings.TrimSpace(str) - - //prefer exact match on choice.Text - for _, choice := range choices { - if choice.Text == str { - fmt.Fprintf(t.i.stderr, "%s -> %s\n", prompt, choice.Text) - return choice.Return, nil - } - } - //allow match on choice.Shortcut - for _, choice := range choices { - if string(choice.Shortcut) == str { - fmt.Fprintf(t.i.stderr, "%s -> %s\n", prompt, choice.Text) - return choice.Return, nil - } - } - fmt.Fprintf(t.i.stderr, "%s -> [%s]\n", prompt, str) - return "", errors.New("cannot match input with available choices") -} diff --git a/pkg/i3status/battery.go b/pkg/i3status/battery.go deleted file mode 100644 index e41f189..0000000 --- a/pkg/i3status/battery.go +++ /dev/null @@ -1,75 +0,0 @@ -/******************************************************************************* -* -* Copyright 2017 Stefan Majewsky -* -* 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 . -* -*******************************************************************************/ - -package i3status - -import ( - "fmt" - "io/ioutil" - "strconv" - "strings" -) - -const ( - batteryFullPath = "/sys/class/power_supply/BAT0/energy_full" - batteryNowPath = "/sys/class/power_supply/BAT0/energy_now" - powerOnlinePath = "/sys/class/power_supply/AC0/online" -) - -func getBatteryStatus() []Block { - //TODO: What's with /sys/class/power_supply/BAT0/uevent? Can this be used to - //remove all the polling overhead here? - - energyFull, err := readNumberFromFile(batteryFullPath) - if err != nil { - return nil - } - energyNow, err := readNumberFromFile(batteryNowPath) - if err != nil { - return nil - } - powerOnline, err := readNumberFromFile(powerOnlinePath) - if err != nil { - return nil - } - - energyPerc := energyNow * 100 / energyFull - charging := powerOnline > 0 - color := "#AAAA00" - if charging { - color = "#00AA00" - } else if energyPerc < 10 { - color = "#AA0000" - } - - return section("bat", Block{ - Name: "battery", - Position: PositionBattery, - FullText: fmt.Sprintf("%d%%", energyPerc), - Urgent: energyPerc < 10 && !charging, - Color: color, - }) -} - -func readNumberFromFile(path string) (int64, error) { - buf, err := ioutil.ReadFile(path) - if err != nil { - return 0, err - } - return strconv.ParseInt(strings.TrimSpace(string(buf)), 0, 64) -} diff --git a/pkg/i3status/main.go b/pkg/i3status/main.go deleted file mode 100644 index 4af726a..0000000 --- a/pkg/i3status/main.go +++ /dev/null @@ -1,169 +0,0 @@ -/******************************************************************************* -* -* Copyright 2017 Stefan Majewsky -* -* 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 . -* -*******************************************************************************/ - -package i3status - -import ( - "encoding/json" - "fmt" - "os" - "sort" - "time" -) - -//Exec executes the i3status applet and returns an exit code (0 for -//success, >0 for error). -func Exec(args []string) int { - //start protocol (we handle errors here for once because if stdout is - //working, we have nothing left to do) - _, err := os.Stdout.Write([]byte("{\"version\":1}\n[\n")) - if err != nil { - fmt.Fprintf(os.Stderr, err.Error()) - return 1 - } - - //main loop - currentBlocks := make(map[string][]Block) - for { - //prepare clock (this is inline instead of split into a separate function - //because the wallclock also drives the main loop's clock, see below) - now := time.Now() - currentBlocks["clock"] = []Block{ - { - Name: "clock", - Instance: "date", - Position: PositionClock, - FullText: now.Format("2006-01-02"), - ShortText: " ", - Color: "#AAAAAA", - SeparatorBlockWidth: 6, - }, - { - Name: "clock", - Instance: "time", - Position: PositionClock, - FullText: now.Format("15:04:05"), - }, - } - - //prepare other blocks - currentBlocks["battery"] = getBatteryStatus() - currentBlocks["network"] = getNetworkStatus() - - //put blocks in rendering order - var allBlocks []Block - for _, blocks := range currentBlocks { - allBlocks = append(allBlocks, blocks...) - } - sort.Sort(byPositionAndInstance(allBlocks)) - - //write output - buf, err := json.Marshal(allBlocks) - if err == nil { - os.Stdout.Write(append(buf, ',', '\n')) - } else { - //this should not happen, but if it does, fall back to just encoding the - //error string (which always works, otherwise wtf) - buf, _ = json.Marshal(err.Error()) - fmt.Printf( - `[{"name":"error","urgent":true,"color":"#FF0000","full_text":%s}],`, - string(buf), - ) - } - - //sleep until the next second starts, so that the clock is always on time - nsec := 1000000000 - now.Nanosecond() - time.Sleep(time.Duration(nsec) * time.Nanosecond) - } - - return 0 -} - -//Block is a block of text as used in the i3status protocol. -//(Not all attributes are represented.) -type Block struct { - Position Position `json:"-"` - Name string `json:"name"` //REQUIRED - Instance string `json:"instance,omitempty"` - FullText string `json:"full_text"` //REQUIRED - ShortText string `json:"short_text"` - Color string `json:"color,omitempty"` //CSS hex syntax, e.g. #123456 - BackgroundColor string `json:"background,omitempty"` - MinWidth uint `json:"min_width,omitempty"` - Alignment Alignment `json:"align,omitempty"` //only plausible with MinWidth - Urgent bool `json:"urgent,omitempty"` - Separator bool `json:"separator"` - SeparatorBlockWidth int `json:"separator_block_width,omitempty"` -} - -type byPositionAndInstance []Block - -func (b byPositionAndInstance) Len() int { return len(b) } -func (b byPositionAndInstance) Swap(i, j int) { b[i], b[j] = b[j], b[i] } -func (b byPositionAndInstance) Less(i, j int) bool { - if b[i].Position == b[j].Position { - if b[i].Instance == "_caption" { - return true - } - if b[j].Instance == "_caption" { - return false - } - return b[i].Instance < b[j].Instance - } - return b[i].Position < b[j].Position -} - -//Alignment is the alignment of a Block. -type Alignment string - -//Acceptable values for Alignment. -const ( - AlignmentLeft Alignment = "left" - AlignmentCenter Alignment = "center" - AlignmentRight Alignment = "right" -) - -//Position defines how blocks are ordered. -type Position int - -//Acceptable values for Position, from left to right. -const ( - PositionNone Position = iota - PositionNetwork - PositionBattery - PositionClock -) - -//Order the given blocks byPositionAndInstance, then add a separator to the last one, then add a caption block of the same style in front. -func section(caption string, blocks ...Block) []Block { - if len(blocks) == 0 { - return nil - } - sort.Sort(byPositionAndInstance(blocks)) - last := len(blocks) - 1 - blocks[last].Separator = true - blocks[last].SeparatorBlockWidth = 15 - - return append([]Block{{ - Name: blocks[0].Name, - Position: blocks[0].Position, - Instance: "_caption", - FullText: caption, - Color: blocks[0].Color, - }}, blocks...) -} diff --git a/pkg/i3status/net.go b/pkg/i3status/net.go deleted file mode 100644 index 7367b28..0000000 --- a/pkg/i3status/net.go +++ /dev/null @@ -1,61 +0,0 @@ -/******************************************************************************* -* -* Copyright 2017 Stefan Majewsky -* -* 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 . -* -*******************************************************************************/ - -package i3status - -import ( - "net" - "regexp" - "strings" -) - -var networkPortRx = regexp.MustCompile(`:\d+$`) -var networkMaskRx = regexp.MustCompile(`/\d+$`) - -func getNetworkStatus() []Block { - addrs, err := net.InterfaceAddrs() - if err != nil { - return nil - } - addrStrs := make([]string, 0, len(addrs)) - for _, addr := range addrs { - str := addr.String() - //remove uninteresting parts - str = networkPortRx.ReplaceAllString(str, "") - str = networkMaskRx.ReplaceAllString(str, "") - //ignore IPv6 for now - if strings.ContainsRune(str, ':') { - continue - } - //ignore uninteresting addrs - if strings.HasPrefix(str, "127.") { - continue - } - addrStrs = append(addrStrs, str) - } - - if len(addrStrs) == 0 { - return nil - } - return section("ip", Block{ - Name: "network", - Position: PositionNetwork, - FullText: strings.Join(addrStrs, " "), - Color: "#00AAAA", - }) -} diff --git a/pkg/prompt/cloud.go b/pkg/prompt/cloud.go deleted file mode 100644 index 72bd6f9..0000000 --- a/pkg/prompt/cloud.go +++ /dev/null @@ -1,83 +0,0 @@ -/******************************************************************************* -* -* Copyright 2017 Stefan Majewsky -* -* 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 . -* -*******************************************************************************/ - -package prompt - -import ( - "io/ioutil" - "os" - "path/filepath" - "strings" - - yaml "gopkg.in/yaml.v2" -) - -func getKubernetesField() string { - configPaths := filepath.SplitList(os.Getenv("KUBECONFIG")) - - var context string - for _, configPath := range configPaths { - context = getKubernetesContext(configPath) - if context != "" { - break - } - } - if context == "" { - return "" - } - - namespaceBytes, err := ioutil.ReadFile(filepath.Join(os.Getenv("HOME"), ".kubectl-namespace")) - namespace := strings.TrimSpace(string(namespaceBytes)) - if err != nil { - if !os.IsNotExist(err) { - handleError(err) - } - namespace = "" - } - if namespace != "" { - context += "/" + namespace - } - - return withType("kube", context) -} - -func getKubernetesContext(configPath string) string { - buf, err := ioutil.ReadFile(configPath) - if err != nil { - //non-existence is acceptable, just make the caller continue with the next configPath - if !os.IsNotExist(err) { - handleError(err) - } - return "" - } - - var data struct { - CurrentContext string `yaml:"current-context"` - } - err = yaml.Unmarshal(buf, &data) - handleError(err) - return strings.TrimSpace(data.CurrentContext) -} - -func getOpenstackField() string { - cloudName := os.Getenv("CURRENT_OS_CLOUD") - if cloudName == "" { - return "" - } - return withType("cloud", cloudName) -} diff --git a/pkg/prompt/git.go b/pkg/prompt/git.go deleted file mode 100644 index 8393c3e..0000000 --- a/pkg/prompt/git.go +++ /dev/null @@ -1,138 +0,0 @@ -/******************************************************************************* -* -* Copyright 2017 Stefan Majewsky -* -* 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 . -* -*******************************************************************************/ - -package prompt - -import ( - "fmt" - "io/ioutil" - "os" - "path/filepath" - "strings" -) - -type gitRepo struct { - RootPath string - GitDir string -} - -//Returns two empty strings if `path` is not inside a Git repo. -func findRepo(path string) (*gitRepo, error) { - //find .git directory or file - gitEntry := filepath.Join(path, ".git") - fi, err := os.Stat(gitEntry) - switch { - case err == nil: - //found - continue below with further checks - case !os.IsNotExist(err): - return nil, err - case path == "/": - return nil, nil - default: - return findRepo(filepath.Dir(path)) - } - - //found .git - what is it? - if fi.Mode().IsDir() { - //normal case - .git is a directory - return &gitRepo{RootPath: path, GitDir: gitEntry}, nil - } - - //.git is a file (e.g. for submodules) - it contains a line like "gitdir: path/to/gitdir" - bytes, err := ioutil.ReadFile(gitEntry) - if err != nil { - return nil, err - } - for _, line := range strings.Split(string(bytes), "\n") { - line = strings.TrimSpace(line) - if strings.HasPrefix(line, "gitdir:") { - return &gitRepo{ - RootPath: path, - GitDir: filepath.Join(path, strings.TrimSpace(strings.TrimPrefix(line, "gitdir:"))), - }, nil - } - } - - return nil, fmt.Errorf("read %s: missing gitdir directive", gitEntry) -} - -func getRepoStatusField(repo *gitRepo) string { - if repo == nil { - return "" - } - - bytes, err := ioutil.ReadFile(filepath.Join(repo.GitDir, "HEAD")) - if err != nil { - handleError(err) - return withType("git", withColor("1;41", "unknown")) - } - refSpec := strings.TrimSpace(string(bytes)) - - //is current HEAD detached? - if !strings.HasPrefix(refSpec, "ref: refs/") { - return formatRepoStatusField(withColor("1;41", "detached"), refSpec) - } - - //current HEAD is a ref - refSpec = strings.TrimPrefix(refSpec, "ref: ") - refSpecDisplay := strings.TrimPrefix(refSpec, "refs/") - refSpecDisplay = strings.TrimPrefix(refSpecDisplay, "heads/") - - //read file corresponding to refspec to find commit ID - bytes, err = ioutil.ReadFile(filepath.Join(repo.GitDir, refSpec)) - commitID := strings.TrimSpace(string(bytes)) - if err != nil { - if os.IsNotExist(err) { - commitID = tryReadFromPackedRefs(repo, refSpec) - if commitID == "" { - commitID = withColor("37", "blank") - } - } else { - handleError(err) - commitID = withColor("1;41", "unknown") - } - } - - return formatRepoStatusField(refSpecDisplay, commitID) -} - -func tryReadFromPackedRefs(repo *gitRepo, refSpec string) string { - bytes, err := ioutil.ReadFile(filepath.Join(repo.GitDir, "packed-refs")) - if err != nil { - return "" - } - for _, line := range strings.Split(string(bytes), "\n") { - line = strings.TrimSpace(line) - if line == "" || strings.HasPrefix(line, "#") { - continue - } - fields := strings.Fields(line) - if len(fields) == 2 && fields[1] == refSpec { - return fields[0] - } - } - return "" -} - -func formatRepoStatusField(refSpec, commitID string) string { - //shorten plain commit IDs from 40 to 10 bytes - if len(commitID) == 40 && !strings.Contains(commitID, "\x1B") { - commitID = commitID[0:10] - } - return withType("git", refSpec+"/"+commitID) -} diff --git a/pkg/prompt/login.go b/pkg/prompt/login.go deleted file mode 100644 index 92cc270..0000000 --- a/pkg/prompt/login.go +++ /dev/null @@ -1,66 +0,0 @@ -/******************************************************************************* -* -* Copyright 2017 Stefan Majewsky -* -* 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 . -* -*******************************************************************************/ - -package prompt - -import "os" - -//#include -//#include -//#include -import "C" - -func getLoginField() string { - var result string - - //show user name - userName := getUserName() - commonUser := getenvOrDefault("PRETTYPROMPT_COMMONUSER", "stefan") - if commonUser != userName { - color := "0" - if userName == "root" { - color = "1;41" - } - result = withColor(color, userName) + "@" - } - - //show hostname - hostname, err := os.Hostname() - if err != nil { - handleError(err) - hostname = "" - } - return result + withColor( - getenvOrDefault("PRETTYPROMPT_HOSTCOLOR", "0;33"), - hostname, - ) -} - -func getUserName() string { - //try to find username via getpwuid(getuid()) - pw, err := C.getpwuid(C.getuid()) - if err == nil { - return C.GoString(pw.pw_name) - } - //fallback to $USER, if set - if name := os.Getenv("USER"); name != "" { - return name - } - handleError(err) - return "" -} diff --git a/pkg/prompt/main.go b/pkg/prompt/main.go deleted file mode 100644 index 1fc60d9..0000000 --- a/pkg/prompt/main.go +++ /dev/null @@ -1,123 +0,0 @@ -/******************************************************************************* -* -* Copyright 2017 Stefan Majewsky -* -* 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 . -* -*******************************************************************************/ - -package prompt - -import ( - "fmt" - "os" - "strings" - - "golang.org/x/crypto/ssh/terminal" - - "github.com/majewsky/gofu/pkg/cli" -) - -//Exec executes the prettyprompt applet and returns an exit code (0 for -//success, >0 for error). -func Exec(args []string) int { - fields := []string{ - getLoginField(), - } - cwd := CurrentDirectory() - fields = appendUnlessEmpty(fields, getDirectoryField(cwd)) - fields = appendUnlessEmpty(fields, getDeletedMessageField(cwd)) - fields = appendUnlessEmpty(fields, getRepoStatusField(cwd.Repo)) - fields = appendUnlessEmpty(fields, getTerminalField()) - fields = appendUnlessEmpty(fields, getOpenstackField()) - fields = appendUnlessEmpty(fields, getKubernetesField()) - //this field should always be last - if len(args) > 0 { - fields = appendUnlessEmpty(fields, getExitCodeField(args[0])) - } - - line := strings.Join(fields, " ") - lineWidth := getPrintableLength(line) - - //add dashes to expand `line` to fill the terminal's width - termWidth, _, err := terminal.GetSize(0) - if err != nil { - termWidth = 80 - } - if termWidth > lineWidth { - line += " " - lineWidth++ - } - if termWidth > lineWidth { - dashes := make([]byte, termWidth-lineWidth) - for idx := range dashes { - dashes[idx] = '-' - } - line += withColor("1", string(dashes)) - } - - os.Stdout.Write([]byte(line + "\n")) - - //print second line: a letter identifying the shell, and the final "$ ") - shellIdent := "" - switch os.Getenv("PRETTYPROMPT_SHELL") { - case "zsh": - shellIdent = "Z" - case "bash": - shellIdent = "B" - } - os.Stdout.Write([]byte(shellIdent + "$ ")) - - return 0 -} - -func getenvOrDefault(key, defaultValue string) (value string) { - value = os.Getenv(key) - if value == "" { - value = defaultValue - } - return -} - -func appendUnlessEmpty(list []string, val string) []string { - if val == "" { - return list - } - return append(list, val) -} - -func handleError(err error) { - if err != nil { - os.Stderr.Write([]byte("\x1B[1;31mPrompt error: " + err.Error() + "\x1B[0m\n")) - } -} - -func getPrintableLength(text string) int { - return len(cli.AnsiColorCodeRx.ReplaceAllString(text, "")) -} - -//withColor adds ANSI escape sequences to the string to display it with a -//certain color. The color is given as the semicolon-separated list of -//arguments to the ANSI escape sequence SGR, e.g. "1;41" for bold with red -//background. -func withColor(color, text string) string { - if color == "0" { - return text - } - return fmt.Sprintf("\x1B[%sm%s\x1B[0m", color, text) -} - -//withType adds a type annotation with a standardized format to the text. -func withType(typeStr, text string) string { - return fmt.Sprintf("\x1B[37m%s:\x1B[0m%s", typeStr, text) -} diff --git a/pkg/prompt/misc.go b/pkg/prompt/misc.go deleted file mode 100644 index 124015a..0000000 --- a/pkg/prompt/misc.go +++ /dev/null @@ -1,43 +0,0 @@ -/******************************************************************************* -* -* Copyright 2017 Stefan Majewsky -* -* 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 . -* -*******************************************************************************/ - -package prompt - -import ( - "os" - "strconv" -) - -func getTerminalField() string { - termName := os.Getenv("TERM") - if termName == "xterm-256color" { - return "" - } - if termName == "" { - termName = withColor("1;41", "not set") - } - return withType("term", termName) -} - -func getExitCodeField(arg string) string { - exitCode, err := strconv.Atoi(arg) - if err == nil && exitCode > 0 { - return withColor("1;31", "exit:"+arg) - } - return "" -} diff --git a/pkg/prompt/pwd.go b/pkg/prompt/pwd.go deleted file mode 100644 index 1b93987..0000000 --- a/pkg/prompt/pwd.go +++ /dev/null @@ -1,157 +0,0 @@ -/******************************************************************************* -* -* Copyright 2017 Stefan Majewsky -* -* 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 . -* -*******************************************************************************/ - -package prompt - -import ( - "os" - "path/filepath" - "strings" -) - -//Directory contains all data about a directory that the prompt needs. -type Directory struct { - Path string - DisplayPath string - InBuildTree bool - InRepoTree bool - Repo *gitRepo - NearestAccessiblePath string -} - -//CurrentDirectory prepares a Directory struct for the current working -//directory. -func CurrentDirectory() Directory { - cwd, err := os.Getwd() - if err != nil { - cwd = filepath.Clean(os.Getenv("PWD")) - } - return NewDirectory(cwd) -} - -//NewDirectory prepares a Directory struct for the given path. -func NewDirectory(path string) (dir Directory) { - dir.Path = path - dir.DisplayPath = path - - //check if path actually exists - dir.NearestAccessiblePath = findNearestAccessiblePath(dir.Path) - if dir.NearestAccessiblePath == dir.Path { - //marks that everything is okay existence-wise - dir.NearestAccessiblePath = "" - - //display tag if below /x/build - if buildPath := os.Getenv("BUILD_ROOT"); buildPath != "" { - rel, _ := filepath.Rel(buildPath, dir.DisplayPath) - if !strings.HasPrefix(rel, "..") && rel != "." { - dir.InBuildTree = true - dir.DisplayPath = filepath.Join("/", rel) - } - } - - //display tag if below /x/src - if gopath := os.Getenv("GOPATH"); gopath != "" { - repoPath := filepath.Join(gopath, "src") - rel, _ := filepath.Rel(repoPath, dir.DisplayPath) - if !strings.HasPrefix(rel, "..") && rel != "." { - dir.InRepoTree = true - dir.DisplayPath = rel - } - } - - //strip $HOME prefix if applicable and desirable - dir.stripHomeDirFromDisplay() - - //check if we are inside a Git repository - var err error - dir.Repo, err = findRepo(dir.Path) - handleError(err) - } - - return -} - -//This part can benefit from a "return" in the middle, so it's in a separate function. -func (dir *Directory) stripHomeDirFromDisplay() { - if !strings.HasPrefix(dir.DisplayPath, "/") { - return - } - - homePath := os.Getenv("HOME") - if homePath == "" { - return - } - - rel, _ := filepath.Rel(homePath, dir.DisplayPath) - if rel == "." { - //do not display an empty DisplayPath if tags are displayed - if dir.InBuildTree || dir.InRepoTree { - return - } - rel = "" - } - if !strings.HasPrefix(rel, "..") { - dir.DisplayPath = rel - } -} - -func findNearestAccessiblePath(path string) string { - _, err := os.Stat(path) - if err == nil { - return path - } - return findNearestAccessiblePath(filepath.Dir(path)) -} - -func getDirectoryField(dir Directory) string { - if dir.DisplayPath == "" { - return "" - } - - txt := withColor("1;36", dir.DisplayPath) - if dir.NearestAccessiblePath == "" { - //cwd accessible -> highlight path elements inside the repo (if any) - if dir.Repo != nil && dir.Repo.RootPath != dir.Path { - rel, _ := filepath.Rel(dir.Repo.RootPath, dir.Path) - if strings.HasSuffix(dir.DisplayPath, rel) { - base := strings.TrimSuffix(dir.DisplayPath, rel) - txt = withColor("0;36", base) + withColor("1;36", rel) - } - } - } else { - //cwd inaccessible -> highlight inaccessible path elements - rel, _ := filepath.Rel(dir.NearestAccessiblePath, dir.Path) - txt = withColor("1;36", dir.NearestAccessiblePath+"/") + withColor("1;31", rel) - } - - //apply tags - if dir.InRepoTree { - txt = withType("repo", txt) - } - if dir.InBuildTree { - txt = withType("build", txt) - } - return txt -} - -func getDeletedMessageField(dir Directory) string { - if dir.NearestAccessiblePath == "" { - return "" - } - return withColor("1;41", "cannot stat cwd") -} diff --git a/pkg/rtree/get_test.go b/pkg/rtree/get_test.go deleted file mode 100644 index bdd0f2e..0000000 --- a/pkg/rtree/get_test.go +++ /dev/null @@ -1,140 +0,0 @@ -/******************************************************************************* -* -* Copyright 2017 Stefan Majewsky -* -* 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 . -* -*******************************************************************************/ - -package rtree - -import ( - "fmt" - "path/filepath" - "testing" -) - -var testIndexWithTwoRepos = Index{ - Repos: []*Repo{ - { - CheckoutPath: "github.com/foo/bar", - Remotes: []Remote{ - {Name: "origin", URL: "https://github.com/foo/bar"}, - }, - }, - { - CheckoutPath: "github.com/git/git", - Remotes: []Remote{ - {Name: "origin", URL: "gh:git/git"}, - }, - }, - }, -} - -func TestGetExistingRepo(t *testing.T) { - Test{ - Args: []string{"get", "gh:git/git"}, - Index: testIndexWithTwoRepos, - ExpectOutput: filepath.Join(RootPath, "/github.com/git/git") + "\n", - }.Run(t) -} - -func TestGetExistingRepoWithoutShortcut(t *testing.T) { - Test{ - Args: []string{"get", "https://github.com/git/git"}, - Index: testIndexWithTwoRepos, - ExpectOutput: filepath.Join(RootPath, "/github.com/git/git") + "\n", - }.Run(t) -} - -func TestGetNewRepo(t *testing.T) { - target := filepath.Join(RootPath, "/github.com/another/repo") - - Test{ - Args: []string{"get", "gh:another/repo"}, - Index: testIndexWithTwoRepos, - ExpectOutput: target + "\n", - ExpectExecution: Recorded("git clone gh:another/repo " + target), - ExpectIndex: &Index{ - Repos: []*Repo{ - { - CheckoutPath: "github.com/another/repo", - Remotes: []Remote{ - {Name: "origin", URL: "gh:another/repo"}, - }, - }, - testIndexWithTwoRepos.Repos[0], - testIndexWithTwoRepos.Repos[1], - }, - }, - }.Run(t) -} - -func TestGetNewForkAsRemote(t *testing.T) { - target := filepath.Join(RootPath, "/github.com/git/git") - Test{ - Args: []string{"get", "https://example.com/git"}, - Index: testIndexWithTwoRepos, - Input: fmt.Sprintf("add as remote to %s\nmyfork\n", target), - ExpectOutput: target + "\n", - ExpectError: fmt.Sprintf( - "Found possible fork candidates. What to do? -> add as remote to %s\n"+ - "Existing remotes:\n\t(origin) gh:git/git\n"+ - "Enter remote name for https://example.com/git: myfork\n", - target, - ), - ExpectExecution: Recorded( - "@"+target+" git remote add myfork https://example.com/git", - "@"+target+" git remote update myfork", - ), - ExpectIndex: &Index{ - Repos: []*Repo{ - testIndexWithTwoRepos.Repos[0], - { - CheckoutPath: "github.com/git/git", - Remotes: []Remote{ - {Name: "origin", URL: "gh:git/git"}, - {Name: "myfork", URL: "https://example.com/git"}, - }, - }, - }, - }, - }.Run(t) -} - -func TestGetNewForkAsSeparate(t *testing.T) { - target := filepath.Join(RootPath, "/example.com/git") - Test{ - Args: []string{"get", "https://example.com/git"}, - Index: testIndexWithTwoRepos, - Input: fmt.Sprintf("clone to %s\n", target), - ExpectOutput: target + "\n", - ExpectError: fmt.Sprintf( - "Found possible fork candidates. What to do? -> clone to %s\n", - target, - ), - ExpectExecution: Recorded("git clone https://example.com/git " + target), - ExpectIndex: &Index{ - Repos: []*Repo{ - { - CheckoutPath: "example.com/git", - Remotes: []Remote{ - {Name: "origin", URL: "https://example.com/git"}, - }, - }, - testIndexWithTwoRepos.Repos[0], - testIndexWithTwoRepos.Repos[1], - }, - }, - }.Run(t) -} diff --git a/pkg/rtree/index.go b/pkg/rtree/index.go deleted file mode 100644 index 4e10ffd..0000000 --- a/pkg/rtree/index.go +++ /dev/null @@ -1,431 +0,0 @@ -/******************************************************************************* -* -* Copyright 2017 Stefan Majewsky -* -* 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 . -* -*******************************************************************************/ - -package rtree - -import ( - "errors" - "fmt" - "io/ioutil" - "os" - "path" - "path/filepath" - "sort" - "strings" - - "github.com/majewsky/gofu/pkg/cli" - - yaml "gopkg.in/yaml.v2" -) - -//Index represents the contents of the index file. -type Index struct { - Repos []*Repo `yaml:"repos"` -} - -//ReadIndex reads the index file. -func ReadIndex() (*Index, []error) { - //read contents of index file - buf, err := ioutil.ReadFile(IndexPath) - if err != nil { - if os.IsNotExist(err) { - return &Index{Repos: nil}, nil - } - return nil, []error{err} - } - - //deserialize YAML - var index Index - err = yaml.Unmarshal(buf, &index) - if err != nil { - return nil, []error{err} - } - - //validate YAML - var errs []error - missing := func(key string, args ...interface{}) { - errs = append(errs, fmt.Errorf("read %s: missing \"%s\"", - IndexPath, fmt.Sprintf(key, args...), - )) - } - for idx, repo := range index.Repos { - if repo.CheckoutPath == "" { - missing("repos[%d].path", idx) - } - if len(repo.Remotes) == 0 { - missing("repos[%d].remotes", idx) - } - for idx2, remote := range repo.Remotes { - switch { - case remote.Name == "": - missing("repos[%d].remotes[%d].name", idx, idx2) - case remote.URL == "": - missing("repos[%d].remotes[%d].url", idx, idx2) - } - } - } - - sort.Sort(reposByAbsPath(index.Repos)) - return &index, errs -} - -type reposByAbsPath []*Repo - -func (r reposByAbsPath) Len() int { return len(r) } -func (r reposByAbsPath) Less(i, j int) bool { return r[i].AbsolutePath() < r[j].AbsolutePath() } -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 { - sort.Sort(reposByAbsPath(i.Repos)) - buf, err := yaml.Marshal(i) - if err != nil { - return err - } - - err = os.MkdirAll(filepath.Dir(IndexPath), 0755) - if err != nil { - return err - } - err = ioutil.WriteFile(IndexPath, buf, 0644) - if err != nil { - return err - } - - //perform sanity check (TODO: do this instead when rebuilding the index) - seen := make(map[string]bool) - warned := make(map[string]bool) - for _, repo := range i.Repos { - if seen[repo.CheckoutPath] && !warned[repo.CheckoutPath] { - cli.Interface.ShowWarning( - fmt.Sprintf("repo %s appears multiple times in the index file!", repo.AbsolutePath()), - ) - warned[repo.CheckoutPath] = true - } - seen[repo.CheckoutPath] = true - } - - return nil -} - -//Rebuild implements the `rtree index` subcommand. -func (i *Index) Rebuild() error { - //check if existing index entries are still checked out - var newRepos []*Repo - for _, repo := range i.Repos { - gitDirPath := filepath.Join(repo.AbsolutePath(), ".git") - fi, err := os.Stat(gitDirPath) - switch { - case err == nil: - if fi.IsDir() { - //everything okay with this repo - newRepos = append(newRepos, repo) - continue - } - return fmt.Errorf("expected repository at %s, but is not a directory", gitDirPath) - case !os.IsNotExist(err): - return err - } - - //repo has been deleted - ask what to do - var remoteURLs []string - for _, remote := range repo.Remotes { - if remote.Name == "origin" { - remoteURLs = []string{remote.URL} - break - } - remoteURLs = append(remoteURLs, remote.URL) - } - - var selection string - if len(remoteURLs) == 0 { - selection, err = cli.Interface.Query( - fmt.Sprintf("repository %s has been deleted; no remote to restore from", filepath.Join(RootPath, repo.CheckoutPath)), - cli.Choice{Return: "d", Shortcut: 'd', Text: "delete from index"}, - cli.Choice{Return: "s", Shortcut: 's', Text: "skip"}, - ) - } else { - selection, err = cli.Interface.Query( - fmt.Sprintf("repository %s has been deleted", filepath.Join(RootPath, repo.CheckoutPath)), - cli.Choice{Return: "r", Shortcut: 'r', Text: "restore 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 selection { - case "r": - err := repo.Checkout() - if err != nil { - return err - } - newRepos = append(newRepos, repo) - case "d": - continue - case "s": - newRepos = append(newRepos, repo) - } - } - - existingRepos := make(map[string]*Repo) - for _, repo := range newRepos { - existingRepos[repo.CheckoutPath] = repo - } - - //index new repos - err := ForeachPhysicalRepo(func(newRepo Repo) error { - repo, exists := existingRepos[newRepo.CheckoutPath] - if exists { - //update the existing index entry with the new remotes - repo.Remotes = newRepo.Remotes - } else { - newRepos = append(newRepos, &newRepo) - } - return nil - }) - if err != nil { - return err - } - - i.Repos = newRepos - return nil -} - -//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(remoteURL string, allowClone bool) (*Repo, error) { - //make sure that stdout is not used for prompts - cli.Interface.StdoutProtected = true - - expandedRemoteURL := ExpandRemoteURL(remoteURL) - basename := path.Base(expandedRemoteURL) - - //is this remote already checked out directly? also look for repos with the - //same basename that could be forks - var candidates []*Repo - for _, repo := range i.Repos { - isCandidate := false - for _, remote := range repo.Remotes { - otherExpandedRemoteURL := ExpandRemoteURL(remote.URL) - if expandedRemoteURL == otherExpandedRemoteURL { - return repo, nil - } - if basename == path.Base(otherExpandedRemoteURL) { - isCandidate = true - } - } - if isCandidate { - candidates = append(candidates, repo) - } - } - - //double-check if the repo is already checked out, but we didn't notice it yet - newRepo, err := NewRepoFromRemoteURL(remoteURL) - if err != nil { - return nil, err - } - _, err = os.Stat(newRepo.AbsolutePath()) - switch { - case err == nil: - return nil, fmt.Errorf( - "%s already exists (if there is a repo there, try `rtree index`)", - newRepo.AbsolutePath(), - ) - case !os.IsNotExist(err): - return nil, err - } - - if !allowClone { - return nil, errors.New("no such remote in index (you can validate the index with `rtree index`)") - } - - //if no fork candidates found, clone as new repo - if len(candidates) == 0 { - err := newRepo.Checkout() - if err != nil { - return nil, err - } - i.Repos = append(i.Repos, &newRepo) - i.Write() - return &newRepo, nil - } - - //if we found fork candidates, ask the user to match the repo with a fork - //candidate (or confirm that the repo shall be cloned fresh) - if len(candidates) > 10 { - candidates = candidates[:10] - } - choices := make([]cli.Choice, len(candidates)+1) - for idx, repo := range candidates { - 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(), - } - selection, err := cli.Interface.Query("Found possible fork candidates. What to do?", choices...) - if err != nil { - return nil, err - } - - if selection == "clone" { - err := newRepo.Checkout() - if err != nil { - return nil, err - } - i.Repos = append(i.Repos, &newRepo) - i.Write() - return &newRepo, nil - } - - //find the repo selected by the user - var target *Repo - for _, repo := range candidates { - if repo.CheckoutPath == selection { - target = repo - break - } - } - - //report the existing remotes, and ask for the name of the new remote - prompt := "Existing remotes:\n" - for _, remote := range target.Remotes { - prompt += fmt.Sprintf("\t(%s) %s\n", remote.Name, remote.URL) - } - prompt += fmt.Sprintf("Enter remote name for %s:", remoteURL) - remoteName, err := cli.Interface.ReadLine(prompt) - if err != nil { - return nil, err - } - - err = cli.Interface.Run(cli.Command{ - Program: []string{"git", "remote", "add", remoteName, remoteURL}, - WorkDir: target.AbsolutePath(), - }) - if err != nil { - return nil, err - } - - err = cli.Interface.Run(cli.Command{ - Program: []string{"git", "remote", "update", remoteName}, - WorkDir: target.AbsolutePath(), - }) - if err != nil { - return nil, err - } - - target.Remotes = append(target.Remotes, Remote{ - Name: remoteName, - URL: remoteURL, - }) - i.Write() - return target, nil -} - -//ImportRepo moves the given repo into the rtree and adds it to the index. -func (i *Index) ImportRepo(dirPath string) error { - //need to make dirPath absolute first - dirPath, err := filepath.Abs(dirPath) - if err != nil { - return err - } - repo, err := NewRepoFromAbsolutePath(dirPath) - if err != nil { - return err - } - - //repo must be outside $GOPATH/src - if !strings.HasPrefix(repo.CheckoutPath, "../") { - return fmt.Errorf("%s is already inside GOPATH", dirPath) - } - - //select the remote which determines the checkout path - choices := make([]cli.Choice, len(repo.Remotes)) - var checkoutPath string - for idx, remote := range repo.Remotes { - thisPath, err := CheckoutPathForRemoteURL(ExpandRemoteURL(remote.URL)) - if err != nil { - return err - } - if remote.Name == "origin" { - //prefer "origin" over everything else - checkoutPath = thisPath - break - } - choices[idx] = cli.Choice{Return: thisPath, Text: thisPath} - } - - //cannot decide myself -> let the user select - if checkoutPath == "" { - if len(choices) == 0 { - return errors.New("repo has no remotes") - } - - question := fmt.Sprintf("Repo has multiple remotes. Where to put below %s?", RootPath) - checkoutPath, err = cli.Interface.Query(question, choices...) - if err != nil { - return err - } - } - - //double-check that there is no such repo in the rtree yet - for _, other := range i.Repos { - if other.CheckoutPath == checkoutPath { - return errors.New("will not overwrite existing checkout at " + other.AbsolutePath()) - } - } - - //do the move - err = repo.Move(checkoutPath, true) - if err != nil { - return err - } - i.Repos = append(i.Repos, &repo) - return nil -} - -//DropRepo deletes the given repo from the rtree and removes it from the index. -func (i *Index) DropRepo(repo *Repo) error { - err := repo.Exec("git", "status") - if err != nil { - return err - } - ok, err := cli.Interface.Confirm(">> Drop this repo?") - if !ok || err != nil { - return err - } - - err = os.RemoveAll(repo.AbsolutePath()) - if err != nil { - return err - } - - reposNew := make([]*Repo, 0, len(i.Repos)-1) - for _, r := range i.Repos { - if r.CheckoutPath != repo.CheckoutPath { - reposNew = append(reposNew, r) - } - } - i.Repos = reposNew - return i.Write() -} diff --git a/pkg/rtree/init.go b/pkg/rtree/init.go deleted file mode 100644 index 1d2dc10..0000000 --- a/pkg/rtree/init.go +++ /dev/null @@ -1,99 +0,0 @@ -/******************************************************************************* -* -* Copyright 2017 Stefan Majewsky -* -* 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 . -* -*******************************************************************************/ - -package rtree - -import ( - "os" - "path/filepath" - "regexp" - "strings" - - "github.com/majewsky/gofu/pkg/cli" -) - -//RemoteAlias describes an alias that can be used in a Git remote URL (as -//defined by the "url..insteadOf" directive in man:git-config(1)). -type RemoteAlias struct { - Alias string - Replacement string -} - -//IndexPath is where the index file is stored. -var IndexPath string - -//RootPath is the directory below which all repositories are located. Its value -//is $GOPATH/src to match the repository layout created by `go get`. -var RootPath string - -//RemoteAliases is the list of remote aliases that is used by ExpandRemoteURL(). -var RemoteAliases []*RemoteAlias - -//Init initializes the global variables of this package to their standard -//values, unless they are already populated. Unit tests shall set IndexPath, -//RootPath etc. before calling Exec(), such that this function becomes a no-op -//when called by Exec(). -// -//Returns false if initialization failed. -func Init() bool { - ok := true //until shown otherwise - - if IndexPath == "" { - homeDir := os.Getenv("HOME") - if homeDir == "" { - cli.Interface.ShowError("$HOME is not set (rtree needs the HOME variable to locate its index file)") - ok = false //but keep going to report all errors at once - } else { - IndexPath = filepath.Join(homeDir, ".rtree/index.yaml") - } - } - - if RootPath == "" { - gopath := os.Getenv("GOPATH") - if gopath == "" { - cli.Interface.ShowError("$GOPATH is not set (rtree needs the GOPATH variable to know where to look for and place repos)") - ok = false //but keep going to report all errors at once - } else { - RootPath = filepath.Join(gopath, "src") - } - } - - if RemoteAliases == nil { - out, err := cli.Interface.CaptureStdout(cli.Command{ - Program: []string{"git", "config", "--global", "-l"}, - }) - if err != nil { - cli.Interface.ShowError(err.Error()) - ok = false //but keep going to report all errors at once - } - - rx := regexp.MustCompile(`^url\.([^=]+)\.insteadof=(.+)$`) - for _, line := range strings.Split(out, "\n") { - match := rx.FindStringSubmatch(line) - if match == nil { - continue - } - RemoteAliases = append(RemoteAliases, &RemoteAlias{ - Alias: match[2], - Replacement: match[1], - }) - } - } - - return ok -} diff --git a/pkg/rtree/main.go b/pkg/rtree/main.go deleted file mode 100644 index f172f7f..0000000 --- a/pkg/rtree/main.go +++ /dev/null @@ -1,171 +0,0 @@ -/******************************************************************************* -* -* Copyright 2017 Stefan Majewsky -* -* 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 . -* -*******************************************************************************/ - -package rtree - -import ( - "strings" - - "github.com/majewsky/gofu/pkg/cli" -) - -//Exec executes the rtree applet and returns an exit code (0 for success, >0 -//for error). The argument is os.Args minus the leading "rtree" or "gofu -//rtree". All side-effects (reading from stdin, writing to stdout/stderr, -//executing other programs) pass through cli.Interface and can be intercepted -//there for the purpose of testing. -func Exec(args []string) int { - if !Init() { - return 1 - } - - if len(args) == 0 { - return usage() - } - - index, errs := ReadIndex() - if len(errs) > 0 { - for _, err := range errs { - cli.Interface.ShowError(err.Error()) - } - return 1 - } - - var err error - switch args[0] { - case "get": - if len(args) != 2 { - return usage() - } - err = commandGet(index, args[1]) - case "drop": - if len(args) != 2 { - return usage() - } - err = commandDrop(index, args[1]) - case "index": - if len(args) != 1 { - return usage() - } - err = commandIndex(index) - case "repos": - if len(args) != 1 { - return usage() - } - commandRepos(index) - case "remotes": - if len(args) != 1 { - return usage() - } - commandRemotes(index) - case "import": - if len(args) != 2 { - return usage() - } - err = commandImport(index, args[1]) - case "each": - if len(args) < 2 { - return usage() - } - return commandEach(index, args[1:]) - default: - return usage() - } - - if err == nil { - return 0 - } - cli.Interface.ShowError(err.Error()) - return 1 -} - -var usageStr = strings.TrimSpace(` -Usage: - rtree [get|drop] - rtree [index|repos|remotes] - rtree import - rtree each -`) - -func usage() int { - cli.Interface.ShowUsage(usageStr) - return 1 -} - -func commandGet(index *Index, url string) error { - repo, err := index.FindRepo(url, true) - if err != nil { - return err - } - cli.Interface.ShowResult(repo.AbsolutePath()) - return nil -} - -func commandDrop(index *Index, url string) error { - repo, err := index.FindRepo(url, false) - if err != nil { - return err - } - return index.DropRepo(repo) -} - -func commandIndex(index *Index) error { - err := index.Rebuild() - if err != nil { - return err - } - return index.Write() -} - -func commandRepos(index *Index) { - var items []string - for _, repo := range index.Repos { - items = append(items, repo.CheckoutPath) - } - cli.Interface.ShowResultsSorted(items) -} - -func commandRemotes(index *Index) { - var items []string - for _, repo := range index.Repos { - for _, remote := range repo.Remotes { - items = append(items, remote.URL) - } - } - cli.Interface.ShowResultsSorted(items) -} - -func commandEach(index *Index, cmdline []string) (exitCode int) { - exitCode = 0 - for _, repo := range index.Repos { - err := repo.Exec(cmdline...) - if err != nil { - cli.Interface.ShowError(err.Error()) - exitCode = 1 - } - } - return -} - -func commandImport(index *Index, dirPath string) error { - err := index.ImportRepo(dirPath) - if err != nil { - return err - } - return index.Write() -} diff --git a/pkg/rtree/remote.go b/pkg/rtree/remote.go deleted file mode 100644 index 0c4e880..0000000 --- a/pkg/rtree/remote.go +++ /dev/null @@ -1,75 +0,0 @@ -/******************************************************************************* -* -* Copyright 2017 Stefan Majewsky -* -* 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 . -* -*******************************************************************************/ - -package rtree - -import ( - "net/url" - "path/filepath" - "regexp" - "strings" -) - -//ExpandRemoteURL derive the canonical URL for a given remote by substituting -//aliases defined in the system-wide and user-global Git config. For example, -//with -// -// $ cat /etc/gitconfig -// [url "git://github.com/"] -// insteadOf = gh: -// -//and the input "gh:foo/bar", this function returns "git://github.com/foo/bar". -func ExpandRemoteURL(remoteURL string) string { - var best *RemoteAlias - for _, current := range RemoteAliases { - if strings.HasPrefix(remoteURL, current.Alias) { - if best == nil || len(best.Alias) < len(current.Alias) { - best = current - } - } - } - if best == nil { - return remoteURL - } - return best.Replacement + strings.TrimPrefix(remoteURL, best.Alias) -} - -//This regex recognizes the scp-like syntax for git remotes -//(i.e. "[user@]example.org:path/to/repo") as specified by the "GIT URLS" -//section of man:git-clone(1). -var scpSyntaxRx = regexp.MustCompile(`^(?:[^/@:]+@)?([^/:]+\.[^/:]+):(.+)$`) - -//CheckoutPathForRemoteURL derives the checkout path for a remote URL that has -//already been expanded with ExpandRemoteURL() if necessary. -// -// "https://example.org/foo/bar" -> "example.org/foo/bar" -// "git@example.org:foo/bar" -> "example.org/foo/bar" -// -func CheckoutPathForRemoteURL(remoteURL string) (string, error) { - match := scpSyntaxRx.FindStringSubmatch(remoteURL) - if match != nil { - //match[1] is the hostname, match[2] is the path to the repo - return filepath.Join(match[1], match[2]), nil - } - - u, err := url.Parse(remoteURL) - if err != nil { - return "", err - } - return filepath.Join(u.Hostname(), u.Path), nil -} diff --git a/pkg/rtree/repo.go b/pkg/rtree/repo.go deleted file mode 100644 index 790db92..0000000 --- a/pkg/rtree/repo.go +++ /dev/null @@ -1,225 +0,0 @@ -/******************************************************************************* -* -* Copyright 2017 Stefan Majewsky -* -* 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 . -* -*******************************************************************************/ - -package rtree - -import ( - "fmt" - "os" - "path/filepath" - "regexp" - "strings" - - "github.com/majewsky/gofu/pkg/cli" -) - -//Repo describes the entry for a repository in the index file. -type Repo struct { - //CheckoutPath shall be relative to the RootPath. - CheckoutPath string `yaml:"path"` - //Remotes maps remote names (as noted in the .git/config of the repo) to - //remote URLs (as they appear in the .git/config of the repo, i.e. possibly - //abbreviated). - Remotes []Remote `yaml:"remotes"` -} - -//Remote describes a remote that is configured in a Repo. -type Remote struct { - Name string `yaml:"name"` - URL string `yaml:"url"` -} - -//AbsolutePath returns the absolute CheckoutPath of this repo. -func (r Repo) AbsolutePath() string { - return filepath.Join(RootPath, r.CheckoutPath) -} - -//NewRepoFromAbsolutePath initializes a Repo instance by scanning the existing -//checkout at the given path. -func NewRepoFromAbsolutePath(path string) (repo Repo, err error) { - repo.CheckoutPath, err = filepath.Rel(RootPath, path) - if err != nil { - return - } - - //list remotes - out, err := cli.Interface.CaptureStdout(cli.Command{ - Program: []string{"git", "config", "-l"}, - WorkDir: path, - }) - if err != nil { - return - } - - for _, line := range strings.Split(out, "\n") { - match := remoteConfigRx.FindStringSubmatch(line) - if match == nil { - continue - } - repo.Remotes = append(repo.Remotes, Remote{ - Name: match[1], - URL: match[2], - }) - } - return -} - -//NewRepoFromRemoteURL initializes a Repo instance for checking out a remote -//for the first time. The checkout does not happen until Checkout() is called. -func NewRepoFromRemoteURL(remoteURL string) (Repo, error) { - checkoutPath, err := CheckoutPathForRemoteURL(ExpandRemoteURL(remoteURL)) - return Repo{ - CheckoutPath: checkoutPath, - Remotes: []Remote{ - { - Name: "origin", - URL: remoteURL, - }, - }, - }, err -} - -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 { - return filepath.Walk(RootPath, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - //look for repos, i.e. directories containing a .git directory - if !info.IsDir() { - return nil - } - _, err = os.Stat(filepath.Join(path, ".git")) - if err != nil { - return nil - } - - //appears to be a repo - repo, err := NewRepoFromAbsolutePath(path) - if err == nil { - err = action(repo) - } - if err != nil { - return err - } - //do not traverse further down into submodules etc. - return filepath.SkipDir - }) -} - -//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 { - //check if we have an "origin" remote to clone from - var originURL string - for _, remote := range r.Remotes { - if remote.Name == "origin" { - originURL = remote.URL - break - } - } - - if originURL == "" { - err := cli.Interface.Run(cli.Command{ - Program: []string{"git", "init", r.AbsolutePath()}, - }) - if err != nil { - return err - } - cli.Interface.ShowWarning(`will not checkout anything since there is no remote named "origin"`) - } else { - err := cli.Interface.Run(cli.Command{ - Program: []string{"git", "clone", originURL, r.AbsolutePath()}, - }) - if err != nil { - return err - } - } - - remotesAdded := false - for _, remote := range r.Remotes { - if remote.Name != "origin" { - err := cli.Interface.Run(cli.Command{ - Program: []string{"git", "remote", "add", remote.Name, remote.URL}, - WorkDir: r.AbsolutePath(), - }) - if err != nil { - return err - } - remotesAdded = true - } - } - if remotesAdded { - return cli.Interface.Run(cli.Command{ - Program: []string{"git", "remote", "update"}, - WorkDir: r.AbsolutePath(), - }) - } - - return nil -} - -//Exec implements the meat of the `rtree exec` command. It returns -//true iff the command exited successfully. -func (r Repo) Exec(cmdline ...string) error { - cli.Interface.ShowProgress(r.AbsolutePath()) - return cli.Interface.Run(cli.Command{ - Program: cmdline, - WorkDir: r.AbsolutePath(), - }) -} - -//Move sets the CheckoutPath to the given value and moves the existing repo -//from the old to the new checkoutPath. If makeSymlink is given, a symlink will -//be created from the old to the new location. -func (r *Repo) Move(checkoutPath string, makeSymlink bool) error { - sourcePath := filepath.Join(RootPath, r.CheckoutPath) - targetPath := filepath.Join(RootPath, checkoutPath) - - //ensure that target does not exist - _, err := os.Lstat(targetPath) - if err == nil { - return fmt.Errorf("cannot move %s to %s: target exists in filesystem", sourcePath, targetPath) - } - if !os.IsNotExist(err) { - return err - } - - //prepare directory to move repo into - err = os.MkdirAll(filepath.Dir(targetPath), 0755) - if err != nil { - return err - } - - //move directory - err = os.Rename(sourcePath, targetPath) - if err != nil { - return err - } - r.CheckoutPath = checkoutPath - - //if requested, make compatibility symlink - if makeSymlink { - return os.Symlink(targetPath, sourcePath) - } - return nil -} diff --git a/pkg/rtree/shared_test.go b/pkg/rtree/shared_test.go deleted file mode 100644 index 3dadcec..0000000 --- a/pkg/rtree/shared_test.go +++ /dev/null @@ -1,204 +0,0 @@ -/******************************************************************************* -* -* Copyright 2017 Stefan Majewsky -* -* 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 . -* -*******************************************************************************/ - -package rtree - -import ( - "bytes" - "errors" - "fmt" - "io" - "io/ioutil" - "os" - "os/exec" - "path/filepath" - "strings" - "testing" - - "github.com/majewsky/gofu/pkg/cli" - yaml "gopkg.in/yaml.v2" -) - -//Path to a directory where tests can put their index files. -var indexTmpDir = filepath.Join(os.TempDir(), fmt.Sprintf("rtree-test-%d", os.Getpid())) - -func TestMain(m *testing.M) { - //make sure that test does not accidentally access user's actual rtree or index - os.Setenv("HOME", "") - os.Setenv("GOPATH", "") - //setup test configuration - RootPath = "/unittest/gopath/src" - RemoteAliases = []*RemoteAlias{ - {Alias: "gh:", Replacement: "https://github.com/"}, - {Alias: "my/", Replacement: "git@git.example.com:"}, - } - - exitCode := m.Run() - - //shared teardown - os.RemoveAll(indexTmpDir) - - os.Exit(exitCode) -} - -//Test describes a call to Main(), the environment that's given to it, and the -//assertions that are checked after the call returns. -type Test struct { - Args []string - Input string - Index Index - ExpectFailure bool - ExpectOutput string - ExpectError string - ExpectIndex *Index //if nil, .Index will be used instead - ExpectExecution []RecordedCommand -} - -func (test Test) Run(t *testing.T) { - //write index file, if any - IndexPath = filepath.Join(indexTmpDir, t.Name()+".yaml") - if test.Index.Repos != nil { - err := test.Index.Write() - if err != nil { - t.Fatalf("%s: cannot write index to %s: %s", t.Name(), IndexPath, err.Error()) - } - } - - //setup cli.Interface for test - var stdout bytes.Buffer - var stderr bytes.Buffer - cs := CommandSimulator{Cmd: test.ExpectExecution} - cli.SetupInterface(bytes.NewReader([]byte(test.Input)), &stdout, &stderr, cs.Next) - - //check exit code - exitCode := Exec(test.Args) - switch { - case exitCode == 0 && test.ExpectFailure: - t.Errorf("%s: expected failure, but returned success", t.Name()) - case exitCode != 0 && !test.ExpectFailure: - t.Errorf("%s: expected success, but returned failure", t.Name()) - } - - //check output - output := string(stdout.Bytes()) - if output != test.ExpectOutput { - t.Errorf("%s: expected stdout %#v, but got %#v", t.Name(), test.ExpectOutput, output) - } - output = string(stderr.Bytes()) - if output != test.ExpectError { - t.Errorf("%s: expected stderr %#v, but got %#v", t.Name(), test.ExpectError, output) - } - - //check index - idx := &test.Index - if test.ExpectIndex != nil { - idx = test.ExpectIndex - } - expectedIdxStr, err := yaml.Marshal(idx) - if err != nil { - t.Fatal(err.Error()) - } - actualIdxStr, err := ioutil.ReadFile(IndexPath) - if err != nil { - t.Fatalf("%s: could not read index from %s: %s", t.Name(), IndexPath, err.Error()) - } - if string(expectedIdxStr) != string(actualIdxStr) { - t.Errorf("%s: index does not match expectation after test; diff follows", t.Name()) - cmd := exec.Command("diff", "-u", "-", IndexPath) - cmd.Stdin = bytes.NewReader([]byte(expectedIdxStr)) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - err := cmd.Run() - if err != nil { - t.Fatal(err.Error()) - } - } -} - -type RecordedCommand struct { - Cmd cli.Command - Stdout string - Stderr string - Fails bool -} - -//Recorded is a shortcut function for initializing a []RecordedCommand. It -//splits each line on whitespace to obtain the command line of that command, -//and recognizes a leading "@/some/path" to set the workdir. -// -//This function can only be used for RecordedCommands without output that do not fail. -func Recorded(lines ...string) (cs []RecordedCommand) { - cs = make([]RecordedCommand, len(lines)) - for idx, line := range lines { - cmdline := strings.Fields(line) - if strings.HasPrefix(cmdline[0], "@") { - cs[idx].Cmd.WorkDir = strings.TrimPrefix(cmdline[0], "@") - cmdline = cmdline[1:] - } - cs[idx].Cmd.Program = cmdline - } - return -} - -//CommandSimulator implements the cli.CommandRunner interface (via its Next -//method). When a cli.Command is given to Next(), it is matched with the next -//command in the .Cmd list, and the result from that RecordedCommand is -//returned. If the given Command is different from the one expected (or if the -//.Cmd list has been exhausted), an error is returned. -type CommandSimulator struct { - Cmd []RecordedCommand - idx int -} - -func (s *CommandSimulator) Next(c cli.Command, stdin io.Reader, stdout, stderr io.Writer) error { - //take next RecordedCommand from list - if s.idx >= len(s.Cmd) { - return errors.New("got command to execute, but recorded commands have been exhausted") - } - sc := s.Cmd[s.idx] - s.idx++ - - //check if the given Command matches the expectation - if !areStringListsEqual(sc.Cmd.Program, c.Program) { - return fmt.Errorf("expected command %#v, but got %#v", - strings.Join(sc.Cmd.Program, " "), strings.Join(c.Program, " "), - ) - } - if sc.Cmd.WorkDir != c.WorkDir { - return fmt.Errorf("expected command workdir %s, but got %s", sc.Cmd.WorkDir, c.WorkDir) - } - - stdout.Write([]byte(sc.Stdout)) - stderr.Write([]byte(sc.Stderr)) - if sc.Fails { - return fmt.Errorf("command %#v has failed", strings.Join(c.Program, " ")) - } - return nil -} - -func areStringListsEqual(a []string, b []string) bool { - if len(a) != len(b) { - return false - } - for idx := range a { - if a[idx] != b[idx] { - return false - } - } - return true -} -- cgit v1.3.1