summaryrefslogtreecommitdiff
path: root/internal/rtree/repo.go
diff options
context:
space:
mode:
authorStefan Majewsky <majewsky@gmx.net>2020-03-28 13:53:10 +0100
committerStefan Majewsky <majewsky@gmx.net>2020-03-28 13:53:10 +0100
commited2b9374599cc00467b1c27121318748cc0d3f49 (patch)
tree65e3200a0b69817b24ae772223d1d852a52458af /internal/rtree/repo.go
parent1ff86053ddef8de3247909434e0bd7000c060f2a (diff)
downloadgofu-ed2b9374599cc00467b1c27121318748cc0d3f49.tar.gz
rename pkg/ to internal/
Diffstat (limited to 'internal/rtree/repo.go')
-rw-r--r--internal/rtree/repo.go225
1 files changed, 225 insertions, 0 deletions
diff --git a/internal/rtree/repo.go b/internal/rtree/repo.go
new file mode 100644
index 0000000..4af51c1
--- /dev/null
+++ b/internal/rtree/repo.go
@@ -0,0 +1,225 @@
+/*******************************************************************************
+*
+* 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/internal/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
+}