aboutsummaryrefslogtreecommitdiff
path: root/pkg
diff options
context:
space:
mode:
Diffstat (limited to 'pkg')
-rw-r--r--pkg/rtree/index.go114
-rw-r--r--pkg/rtree/main.go13
-rw-r--r--pkg/rtree/remote.go34
-rw-r--r--pkg/rtree/repo.go30
-rw-r--r--pkg/util/util.go22
5 files changed, 199 insertions, 14 deletions
diff --git a/pkg/rtree/index.go b/pkg/rtree/index.go
index a4a0b74..0809fee 100644
--- a/pkg/rtree/index.go
+++ b/pkg/rtree/index.go
@@ -23,6 +23,8 @@ import (
"fmt"
"io/ioutil"
"os"
+ "os/exec"
+ "path"
"path/filepath"
"sort"
"strings"
@@ -142,12 +144,12 @@ func (i *Index) InteractiveRebuild() error {
var choice string
if len(remoteURLs) == 0 {
- choice = util.Prompt(
+ choice = util.Prompt(os.Stdout,
"no remote to restore from; (d)elete from index or (s)kip?",
[]string{"d", "s"},
)
} else {
- choice = util.Prompt(
+ choice = util.Prompt(os.Stdout,
fmt.Sprintf("(r)estore from %s, (d)elete from index, or (s)kip?", strings.Join(remoteURLs, " and ")),
[]string{"r", "d", "s"},
)
@@ -185,3 +187,111 @@ func (i *Index) InteractiveRebuild() error {
i.Repos = newRepos
return nil
}
+
+var tenLetters = []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}
+
+//InteractiveFindRepo locates the repo with the given remote if it exists on
+//disk or (if allowClone is set) clones it and adds it to the index. This
+//is the meat of `rtree get`, and is also used by `rtree drop`.
+func (i *Index) InteractiveFindRepo(remoteURL string, allowClone bool) *Repo {
+ //NOTE: This function prints *everything* on stderr, because stdout is
+ //reserved for the result path during `rtree get`.
+
+ 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
+ }
+ 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 := NewRepoFromRemoteURL(remoteURL)
+ if newRepo.ExistsOnDisk() {
+ util.FatalIfError(fmt.Errorf(
+ "%s already exists (if there is a repo there, try `rtree index`)",
+ newRepo.AbsolutePath(),
+ ))
+ }
+
+ if !allowClone {
+ return nil
+ }
+
+ //if no fork candidates found, clone as new repo
+ if len(candidates) == 0 {
+ util.FatalIfError(newRepo.Checkout())
+ i.Repos = append(i.Repos, &newRepo)
+ i.Write()
+ return &newRepo
+ }
+
+ //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]
+ }
+ prompt := "Found possible fork candidates.\n"
+ for idx, repo := range candidates {
+ prompt += fmt.Sprintf("\t(%s) add as remote to %s\n", tenLetters[idx], repo.AbsolutePath())
+ }
+ prompt += fmt.Sprintf("\t(x) clone to %s\nSelect action:", newRepo.AbsolutePath())
+ choices := append([]string{"x"}, tenLetters[:len(candidates)]...)
+ choice := util.Prompt(os.Stderr, prompt, choices)
+
+ if choice == "x" {
+ util.FatalIfError(newRepo.Checkout())
+ i.Repos = append(i.Repos, &newRepo)
+ i.Write()
+ return &newRepo
+ }
+
+ //find the repo selected by the user
+ var target *Repo
+ for idx, str := range choices {
+ if choice == str {
+ target = candidates[idx-1]
+ }
+ }
+
+ //report the existing remotes, and ask for the name of the new remote
+ fmt.Fprintln(os.Stderr, "Existing remotes:")
+ for _, remote := range target.Remotes {
+ fmt.Fprintf(os.Stderr, "\t(%s) %s\n", remote.Name, remote.URL)
+ }
+ fmt.Fprintf(os.Stderr, "Enter remote name for %s: ", remoteURL)
+ remoteName := util.ReadLine()
+
+ cmd := exec.Command("git", "remote", "add", remoteName, remoteURL)
+ cmd.Stdout = os.Stderr
+ cmd.Stderr = os.Stderr
+ cmd.Dir = target.AbsolutePath()
+ util.FatalIfError(cmd.Run())
+
+ cmd = exec.Command("git", "remote", "update", remoteName)
+ cmd.Stdout = os.Stderr
+ cmd.Stderr = os.Stderr
+ cmd.Dir = target.AbsolutePath()
+ util.FatalIfError(cmd.Run())
+
+ target.Remotes = append(target.Remotes, Remote{
+ Name: remoteName,
+ URL: remoteURL,
+ })
+ i.Write()
+ return target
+}
diff --git a/pkg/rtree/main.go b/pkg/rtree/main.go
index 9513d06..2a85ab9 100644
--- a/pkg/rtree/main.go
+++ b/pkg/rtree/main.go
@@ -33,7 +33,10 @@ func Exec(args []string) {
}
switch args[0] {
case "get":
- panic("unimplemented")
+ if len(args) != 2 {
+ usageAndExit()
+ }
+ commandGet(args[1])
case "drop":
panic("unimplemented")
case "index":
@@ -74,6 +77,14 @@ func usageAndExit() {
os.Exit(1)
}
+func commandGet(url string) {
+ index := ReadIndex()
+ repo := index.InteractiveFindRepo(url, true)
+ if repo != nil {
+ fmt.Println(repo.AbsolutePath())
+ }
+}
+
func commandIndex() {
index := ReadIndex()
util.FatalIfError(index.InteractiveRebuild())
diff --git a/pkg/rtree/remote.go b/pkg/rtree/remote.go
index 4170717..a2a2ab3 100644
--- a/pkg/rtree/remote.go
+++ b/pkg/rtree/remote.go
@@ -20,7 +20,9 @@ package rtree
import (
"bytes"
+ "net/url"
"os/exec"
+ "path/filepath"
"regexp"
"strings"
@@ -64,17 +66,41 @@ func init() {
// insteadOf = gh:
//
//and the input "gh:foo/bar", this function returns "git://github.com/foo/bar".
-func ExpandRemoteURL(url string) string {
+func ExpandRemoteURL(remoteURL string) string {
var best *remoteAlias
for _, current := range remoteAliases {
- if strings.HasPrefix(url, current.Alias) {
+ if strings.HasPrefix(remoteURL, current.Alias) {
if best == nil || len(best.Alias) < len(current.Alias) {
best = current
}
}
}
if best == nil {
- return url
+ return remoteURL
}
- return best.Replacement + strings.TrimPrefix(url, best.Alias)
+ 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 {
+ 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])
+ }
+
+ u, err := url.Parse(remoteURL)
+ util.FatalIfError(err)
+
+ return filepath.Join(u.Hostname(), u.Path)
}
diff --git a/pkg/rtree/repo.go b/pkg/rtree/repo.go
index f231d0a..93ab9d2 100644
--- a/pkg/rtree/repo.go
+++ b/pkg/rtree/repo.go
@@ -95,6 +95,20 @@ func NewRepoFromAbsolutePath(path string) (repo Repo, err error) {
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 {
+ return Repo{
+ CheckoutPath: CheckoutPathForRemoteURL(ExpandRemoteURL(remoteURL)),
+ Remotes: []Remote{
+ {
+ Name: "origin",
+ URL: remoteURL,
+ },
+ },
+ }
+}
+
var remoteConfigRx = regexp.MustCompile(`remote\.([^=]+)\.url=(.+)`)
//ForeachPhysicalRepo walks over the repository tree, executing the action
@@ -127,6 +141,22 @@ func ForeachPhysicalRepo(action func(repo Repo) error) error {
})
}
+//ExistsOnDisk returns true if the top directory of this repo exists.
+func (r Repo) ExistsOnDisk() bool {
+ path := r.AbsolutePath()
+ fi, err := os.Stat(path)
+ if err == nil {
+ if !fi.IsDir() {
+ util.FatalIfError(fmt.Errorf("expected %s to be a directory, but it is not", path))
+ }
+ return true
+ }
+ if !os.IsNotExist(err) {
+ util.FatalIfError(err)
+ }
+ return false
+}
+
//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 {
diff --git a/pkg/util/util.go b/pkg/util/util.go
index 3c5e724..8d2b0bf 100644
--- a/pkg/util/util.go
+++ b/pkg/util/util.go
@@ -21,6 +21,7 @@ package util
import (
"bufio"
"fmt"
+ "io"
"os"
"sort"
"strings"
@@ -57,12 +58,12 @@ var stdin = bufio.NewReader(os.Stdin)
//
// choice := Prompt("(y)es or (n)o", []string{"y","n"})
// //choice is either "y" or "n"
-func Prompt(question string, answers []string) string {
+func Prompt(out io.Writer, question string, answers []string) string {
for idx, answer := range answers {
answers[idx] = strings.ToLower(answer)
}
- os.Stdout.Write([]byte(">> " + strings.TrimSpace(question) + " "))
+ out.Write([]byte(">> " + strings.TrimSpace(question) + " "))
for {
input, err := stdin.ReadString('\n')
FatalIfError(err)
@@ -74,17 +75,24 @@ func Prompt(question string, answers []string) string {
}
//user typed gibberish - ask again
- os.Stdout.Write([]byte("Please type "))
+ out.Write([]byte("Please type "))
for idx, answer := range answers {
if idx > 0 {
if idx == len(answers)-1 {
- os.Stdout.Write([]byte(" or "))
+ out.Write([]byte(" or "))
} else {
- os.Stdout.Write([]byte(", "))
+ out.Write([]byte(", "))
}
}
- os.Stdout.Write([]byte("'" + answer + "'"))
+ out.Write([]byte("'" + answer + "'"))
}
- os.Stdout.Write([]byte(": "))
+ out.Write([]byte(": "))
}
}
+
+//ReadLine reads a line from stdin, with whitespace already trimmed.
+func ReadLine() string {
+ input, err := stdin.ReadString('\n')
+ FatalIfError(err)
+ return strings.TrimSpace(input)
+}