From d710717489dfef811acedafabaf8192880902322 Mon Sep 17 00:00:00 2001 From: sloum Date: Mon, 14 Nov 2022 13:48:49 -0800 Subject: [PATCH] Adds solo repository support --- README.md | 6 +- main.go | 10 ++- operators/generate.go | 28 ++++++-- operators/install.go | 62 ++++++++++------- operators/solo.go | 157 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 230 insertions(+), 33 deletions(-) create mode 100644 operators/solo.go diff --git a/README.md b/README.md index 03e4256..26ce5d9 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ An easy way to install, remove, and update [slope](https://git.rawtext.club/slop slp deps [[-g]] [file] # install the dependencies of 'file' slp docs [[-g]] [module] # open a module's readme in $PAGER slp fetch # fetches the updated registry -slp gen # creates new module dir/skeleton +slp gen [[-s|--solo]] # creates new module dir/skeleton slp help # print usage information slp install [[-g]] [module...] # installs module(s) slp installed [[-g]] # lists all installed packages @@ -23,6 +23,10 @@ slp update-all [[-g]] [module...] # updates all installed modules The above options are more or less self-explanatory with the exception of `local`. `local` will install a module that you have on your system, but not on the slope module path. This is useful if, for example, a person has made their module available but it is not in the slp registry. In which case you can clone their repo and run `slp local ~/path/to/their-module`. Once installed in this manner the slp `remove`, `installed`, and `docs` commands will be able to operate on the module based on its folder name. `update`, however, requires the registry to know what git tag is the current/newest tag. +## VCS Choices + +`slp gen` supports git repositories (the default) or [solo](https://slope.colorfield.space/solo/repos/solo) repositories. To create the new module folder as a solo repository instead of a git repository simply pass the `-s` or `--solo`. For example: `slp gen -s` or `slp gen --solo`. If you opt to use solo repositories be sure that the repository link in your `module.json` file is a link to the `[reponame].tar.gz` file that soloweb creates when pushing a repository. Linking to the index page of the soloweb view of the repository will not do the job. If you are not sure where the correct file is, but have pushed your repository to a remote using `solo push [credentials]`, you can visit the web page and right click on the `download` link in the navigation menu and choose `copy link`. That will add the correct link to your clipboard, and it can then be added to your `module.json` file as needed. + ## Global installs Operations that accept a -g flag will attempt to install a module systemwide (this may require root access). A --global flag may be passed in lieu of a -g flag if desired for clarity. diff --git a/main.go b/main.go index 05c3d33..27dc085 100644 --- a/main.go +++ b/main.go @@ -17,7 +17,7 @@ const ( helptext string = ` slp deps [[-g]] [file] # install the dependencies of 'file' slp fetch # fetches the updated registry -slp gen # creates new module dir/skeleton +slp gen [[-s|--solo]] # creates new module dir/skeleton slp help # print usage information slp install [[-g]] [module...] # installs module(s) slp installed [[-g]] # lists all installed packages @@ -32,6 +32,8 @@ slp version # print the current slp version Operations that accept a -g flag will attempt to install a module systemwide (this may require root access). A --global flag may be passed in lieu of a -g flag if desired for clarity. +slp gen will automatically init a git repository in the created folder. The -s/--solo flag will change that behavior to create a solo repository instead. + The install location for global modules is: /usr/local/lib/slope/modules Globals modules must be dealt with separately from local modules and cannot be combined in a single command.` @@ -108,7 +110,11 @@ MainSwitch: os.Exit(1) } case "gen": - err := operators.Generate() + solo := false + if len(arg) > 2 && arg[2] == "-s" || arg[2] == "--solo" { + solo = true + } + err := operators.Generate(solo) if err != nil { fmt.Fprintf(os.Stderr, "%s\n", err.Error()) os.Exit(1) diff --git a/operators/generate.go b/operators/generate.go index 89b5969..2421c06 100644 --- a/operators/generate.go +++ b/operators/generate.go @@ -52,7 +52,7 @@ func (g genOpts) String() string { // Generate generates a package // by cloning the package template repo -func Generate() error { +func Generate(solo bool) error { opts, err := promptOptions() if err != nil { return err @@ -70,9 +70,27 @@ func Generate() error { dir := filepath.Join(pwd, opts.Title) - _, err = git.PlainInit(dir, false) - if err != nil { - return err + if solo { + err = os.MkdirAll(filepath.Join(dir, ".solo", "tree", "master"), 0755) + if err != nil { + return err + } + err = os.MkdirAll(filepath.Join(dir, ".solo", "tags"), 0755) + if err != nil { + return err + } + f, err := os.Create(filepath.Join(dir, ".solo", "solo.conf")) + if err != nil { + return err + } + defer f.Close() + host, _ := os.Hostname() + f.WriteString(fmt.Sprintf(`(("heads" ()) ("branch" "master") ("tags" ()) ("description" "%s") ("author" "%s@%s") ("version" (0 3 0)) ("remote" #f) ("floating?" #f))`, opts.Description, opts.Author, host)) + } else { + _, err = git.PlainInit(dir, false) + if err != nil { + return err + } } jf, err := os.Create(filepath.Join(dir, "module.json")) @@ -92,7 +110,7 @@ func Generate() error { sf.WriteString(fmt.Sprintf(";;; Author: %s\n", opts.Author)) sf.WriteString(fmt.Sprintf(";;; Version: %s\n", opts.Version)) sf.WriteString(";;;\n\n; display \"Hello, World!\"\n") - sf.WriteString(fmt.Sprintf("(define %s-hello-world (lambda ()\n", opts.Title)) + sf.WriteString("(define hello-world (lambda ()\n") sf.WriteString(fmt.Sprintf(" (display \"'Hello, world!' from %s\\n\")))\n\n; vim: ts=2 sw=2 expandtab ft=slope\n", opts.Title)) rm, err := os.Create(filepath.Join(dir, "README.md")) diff --git a/operators/install.go b/operators/install.go index f7f7955..651a9c0 100644 --- a/operators/install.go +++ b/operators/install.go @@ -51,33 +51,39 @@ func Install(pkg string, global bool) error { for k, _ := range depList { fmt.Printf(" \033[2m├\033[0m staging %q\n", k) p := packages[k] - repository, err := git.PlainClone(filepath.Join(stagingDir, p.Title), false, &git.CloneOptions{ - URL: p.Repository, - }) - - if err != nil { - failedInstall = append(failedInstall, err.Error()) - continue - } - - if p.Tag != "" { - wt, err := repository.Worktree() - if err != nil { + if strings.HasSuffix(p.Repository, "tar.gz") { + if err := InstallFromSolo(p.Title, p.Repository, p.Tag, stagingDir); err != nil { failedInstall = append(failedInstall, err.Error()) - continue } - err = wt.Checkout(&git.CheckoutOptions{ - Branch: plumbing.NewTagReferenceName(p.Tag), + } else { + repository, err := git.PlainClone(filepath.Join(stagingDir, p.Title), false, &git.CloneOptions{ + URL: p.Repository, }) + if err != nil { failedInstall = append(failedInstall, err.Error()) continue } - } - _, err = os.Stat(filepath.Join(stagingDir, pkg, "main.slo")) - if err != nil && os.IsNotExist(err) { - failedInstall = append(failedInstall, fmt.Sprintf(" \033[2m└\033[0m \033[91mError:\033[0m package %q does not contain a valid 'main.slo' file", pkg)) + if p.Tag != "" { + wt, err := repository.Worktree() + if err != nil { + failedInstall = append(failedInstall, err.Error()) + continue + } + err = wt.Checkout(&git.CheckoutOptions{ + Branch: plumbing.NewTagReferenceName(p.Tag), + }) + if err != nil { + failedInstall = append(failedInstall, err.Error()) + continue + } + } + + _, err = os.Stat(filepath.Join(stagingDir, pkg, "main.slo")) + if err != nil && os.IsNotExist(err) { + failedInstall = append(failedInstall, fmt.Sprintf(" \033[2m└\033[0m \033[91mError:\033[0m package %q does not contain a valid 'main.slo' file", pkg)) + } } } if len(failedInstall) > 0 { @@ -88,13 +94,19 @@ func Install(pkg string, global bool) error { return fmt.Errorf(strings.Join(failedInstall, "\n")) } fmt.Println(" \033[2m├\033[0m moving staged items to modules folder") - cmd := exec.Command("sh", "-c", fmt.Sprintf("mv %s %s", filepath.Join(stagingDir, "*"), modDir)) - err = cmd.Run() - if err != nil { - if global { - return fmt.Errorf("Unable to write modules to final location. Do you have access?") + globPattern := filepath.Join(stagingDir, "*") + glob, _ := filepath.Glob(globPattern) + if len(glob) > 0 { + cmd := exec.Command("sh", "-c", fmt.Sprintf("mv %s %s", globPattern, modDir)) + cmd.Stdout = nil + cmd.Stderr = nil + err = cmd.Run() + if err != nil { + if global { + return fmt.Errorf("Unable to write modules to final location. Do you have access?") + } + return err } - return err } return nil } diff --git a/operators/solo.go b/operators/solo.go new file mode 100644 index 0000000..1304c1c --- /dev/null +++ b/operators/solo.go @@ -0,0 +1,157 @@ +package operators + +import ( + "archive/tar" + "compress/gzip" + "fmt" + "io" + "net/http" + "os" + "path" + "path/filepath" + "regexp" + "strings" +) + +func inStringSlice(slice []string, value string) bool { + for i := range slice { + if slice[i] == value { + return true + } + } + return false +} + +func InstallFromSolo(repoName, repo, tag, targetDir string) error { + // Get the repo + tarGzName, err := getSoloTarGz(repo) + if err != nil { + return err + } + defer os.Remove(tarGzName) + + // Un-gzip the repo + tarName, err := unGzip(tarGzName, filepath.Dir(tarGzName)) + if err != nil { + return err + } + defer os.Remove(tarName) + + repoFolder, err := unTar(tarName, filepath.Dir(tarName)) + if err != nil { + // return err + return fmt.Errorf("3") + } + defer os.RemoveAll(repoFolder) + tagFile, err := os.ReadFile(filepath.Join(repoFolder, ".solo", "tags", tag)) + if err != nil { + return fmt.Errorf("The tag %q does not exist", tag) + } + re := regexp.MustCompile(`\("([^"\n]+)"\s+"([^"\n"]+)"[^\)]*\)`) + matches := re.FindAllStringSubmatch(string(tagFile), 1) + if len(matches) == 0 || len(matches[0]) < 3 { + return fmt.Errorf("The tag %q is corrupted and cannot be used", tag) + } + branch := string(matches[0][1]) + snap := string(matches[0][2]) + if branch == "" || snap == "" { + return fmt.Errorf("The tag %q contains invalid references and cannot be used", tag) + } + folderRoot := filepath.Join(repoFolder, ".solo", "tree", branch, snap, "data") + files, _ := filepath.Glob(filepath.Join(folderRoot, "*")) + if len(files) == 0 { + return fmt.Errorf("Invalid repository") + } + if !inStringSlice(files, filepath.Join(folderRoot, "main.slo")) || !inStringSlice(files, filepath.Join(folderRoot, "module.json")) { + return fmt.Errorf("Invalid repository structure") + } + return os.Rename(folderRoot, filepath.Join(targetDir, repoName)) +} + +func getSoloTarGz(u string) (string, error) { + if !strings.HasSuffix(u, ".tar.gz") { + return "", fmt.Errorf("Invalid repository URL") + } + resp, err := http.Get(u) + if err != nil { + return "", err + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + f, err := os.CreateTemp("", path.Base(u)) + if err != nil { + return "", err + } + defer f.Close() + f.Write(body) + return f.Name(), nil +} + +func unTar(tarPath, destPath string) (string, error) { + reader, err := os.Open(tarPath) + if err != nil { + return "", err + } + defer reader.Close() + tarReader := tar.NewReader(reader) + root := "" + + for { + header, err := tarReader.Next() + if err == io.EOF { + break + } else if err != nil { + return "", err + } + + path := filepath.Join(destPath, header.Name) + info := header.FileInfo() + if info.IsDir() { + if err = os.MkdirAll(path, info.Mode()); err != nil { + return "", err + } + if root == "" { + root = path + } + continue + } + + file, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, info.Mode()) + if err != nil { + return "", err + } + defer file.Close() + _, err = io.Copy(file, tarReader) + if err != nil { + return "", err + } + } + return root, nil +} + +func unGzip(source, target string) (string, error) { + reader, err := os.Open(source) + if err != nil { + return "", err + } + defer reader.Close() + + archive, err := gzip.NewReader(reader) + if err != nil { + return "", err + } + defer archive.Close() + splitInd := strings.Index(source, ".gz") + target = filepath.Join(target, filepath.Base(source)[:splitInd]) + writer, err := os.Create(target) + if err != nil { + return "", err + } + defer writer.Close() + + _, err = io.Copy(writer, archive) + return target, err +}