aboutsummaryrefslogtreecommitdiff
path: root/pkg
diff options
context:
space:
mode:
Diffstat (limited to 'pkg')
-rw-r--r--pkg/cli/command.go70
-rw-r--r--pkg/cli/interface.go168
-rw-r--r--pkg/cli/query.go295
-rw-r--r--pkg/i3status/battery.go75
-rw-r--r--pkg/i3status/main.go169
-rw-r--r--pkg/i3status/net.go61
-rw-r--r--pkg/prompt/cloud.go83
-rw-r--r--pkg/prompt/git.go138
-rw-r--r--pkg/prompt/login.go66
-rw-r--r--pkg/prompt/main.go123
-rw-r--r--pkg/prompt/misc.go43
-rw-r--r--pkg/prompt/pwd.go157
-rw-r--r--pkg/rtree/get_test.go140
-rw-r--r--pkg/rtree/index.go431
-rw-r--r--pkg/rtree/init.go99
-rw-r--r--pkg/rtree/main.go171
-rw-r--r--pkg/rtree/remote.go75
-rw-r--r--pkg/rtree/repo.go225
-rw-r--r--pkg/rtree/shared_test.go204
19 files changed, 0 insertions, 2793 deletions
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 <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 (
- "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 <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"
- "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 <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 (
- "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 <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 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 <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 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 <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 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 <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 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 <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 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 <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 prompt
-
-import "os"
-
-//#include <sys/types.h>
-//#include <pwd.h>
-//#include <unistd.h>
-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 = "<unknown>"
- }
- 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 "<unknown>"
-}
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 <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 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 <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 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 <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 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 <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 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 <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 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 <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 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.<base>.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 <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 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] <url>
- rtree [index|repos|remotes]
- rtree import <path>
- rtree each <command>
-`)
-
-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 <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 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 <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 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 <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 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
-}