diff options
Diffstat (limited to 'internal/rtree/index.go')
| -rw-r--r-- | internal/rtree/index.go | 431 |
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() +} |
