summaryrefslogtreecommitdiff
path: root/internal/rtree/index.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/rtree/index.go')
-rw-r--r--internal/rtree/index.go431
1 files changed, 431 insertions, 0 deletions
diff --git a/internal/rtree/index.go b/internal/rtree/index.go
new file mode 100644
index 0000000..4ca7a41
--- /dev/null
+++ b/internal/rtree/index.go
@@ -0,0 +1,431 @@
+/*******************************************************************************
+*
+* 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/internal/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()
+}