diff options
Diffstat (limited to 'pkg')
| -rw-r--r-- | pkg/rtree/index.go | 114 | ||||
| -rw-r--r-- | pkg/rtree/main.go | 13 | ||||
| -rw-r--r-- | pkg/rtree/remote.go | 34 | ||||
| -rw-r--r-- | pkg/rtree/repo.go | 30 | ||||
| -rw-r--r-- | pkg/util/util.go | 22 |
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) +} |
