From 5392355edafb2fb3867947582e4ffff439cb9659 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81d=C3=A1m=20Kov=C3=A1cs?= Date: Fri, 5 Jul 2024 18:57:29 +0200 Subject: [PATCH] :construction: Link command --- src/app.go | 23 ++++ src/command/command.go | 8 ++ src/command/link.go | 196 ++++++++++++++++++++++++++++++ src/config/moduleConfiguration.go | 81 ++++++++++++ src/go.mod | 8 ++ src/go.sum | 4 + src/helpers/path.go | 28 +++++ src/helpers/repo.go | 44 +++++++ src/main.go | 14 +++ 9 files changed, 406 insertions(+) create mode 100644 src/app.go create mode 100644 src/command/command.go create mode 100644 src/command/link.go create mode 100644 src/config/moduleConfiguration.go create mode 100644 src/go.mod create mode 100644 src/go.sum create mode 100644 src/helpers/path.go create mode 100644 src/helpers/repo.go create mode 100644 src/main.go diff --git a/src/app.go b/src/app.go new file mode 100644 index 0000000..b3ff291 --- /dev/null +++ b/src/app.go @@ -0,0 +1,23 @@ +package main + +import ( + "alma/command" + + "github.com/samber/lo" +) + +func run(commands []command.Command, args []string) { + command, found := lo.Find(commands, func(c command.Command) bool { return c.GetName() == args[0] }) + + if !found { + println("Command not found") + return + } + + commandArgs := args[1:] + if lo.ContainsBy(commandArgs, func(item string) bool { return (item == "-h" || item == "--help") }) { + command.GetHelpText() + return + } + command.Run(commandArgs) +} diff --git a/src/command/command.go b/src/command/command.go new file mode 100644 index 0000000..3deb6a3 --- /dev/null +++ b/src/command/command.go @@ -0,0 +1,8 @@ +package command + +type Command interface { + GetHelpText() + GetName() string + Run(args []string) +} + diff --git a/src/command/link.go b/src/command/link.go new file mode 100644 index 0000000..a8e553c --- /dev/null +++ b/src/command/link.go @@ -0,0 +1,196 @@ +package command + +import ( + "alma/config" + "alma/helpers" + "os" + "path/filepath" + "strings" + + "github.com/samber/lo" +) + +type LinkCommand struct { +} + +type itemToLink struct { + source string + target string +} + +func (LinkCommand) GetName() string { + return "link" +} + +func (LinkCommand) GetHelpText() { + println( + `Usage: + alma link [module] + alma link [repository] [module] + +Options: + --help Show this message + -d, --dry-run Show what would be linked without actually linking`) +} + +func (LinkCommand) Run(args []string) { + repoName, moduleName := helpers.GetRepositoryAndModuleName(args) + + if moduleName == nil { + println("Module name is required") + return + } + + dryRun := lo.ContainsBy(args, func(item string) bool { return (item == "-d" || item == "--dry-run") }) + + sourceDirectory, targetDirectory := helpers.GetRepositorySourceAndTargetDirectory(repoName) + if sourceDirectory == nil { + println("Source directory not exists") + return + } + + sourceDirectoryFolderInfo, err := os.Stat(*sourceDirectory) + if err != nil || !sourceDirectoryFolderInfo.IsDir() { + println("Source directory not exists", *sourceDirectory) + return + } + + moduleNameAsPath := strings.ReplaceAll((*moduleName), "/", string(filepath.Separator)) + moduleDirectory := filepath.Join(*sourceDirectory, moduleNameAsPath) + + moduleDirectoryFolderInfo, err := os.Stat(moduleDirectory) + if err != nil || !moduleDirectoryFolderInfo.IsDir() { + println("Module directory not exists", moduleDirectory) + return + } + + almaConfigFilePath, err := os.Stat(filepath.Join(moduleDirectory, ".alma-config.json")) + moduleConfiguration := &config.ModuleConfiguration{} + if err == nil && !almaConfigFilePath.IsDir() { + moduleConfiguration = config.LoadModuleConfiguration(filepath.Join(moduleDirectory, ".alma-config.json")) + targetDirectory1 := helpers.ResolvePath(moduleConfiguration.Target) + targetDirectory = &targetDirectory1 + } + itemsToLink := TraverseTree( + &moduleDirectory, + targetDirectory, + &moduleDirectory, + targetDirectory, + moduleConfiguration, + ) + + filteredItemsToLink := lo.Filter(itemsToLink, func(item itemToLink, index int) bool { + for _, exclude := range moduleConfiguration.Exclude { + if strings.HasPrefix(item.source, exclude) { + return false + } + } + return true + }) + + if dryRun { + println("Dry run. No links will be created. The following links would be created:") + } + + linkItems(filteredItemsToLink, dryRun) + + // Not yet used things + + _ = targetDirectory + _ = moduleConfiguration + +} + +func TraverseTree( + currentDirectory *string, + currentTargetDirectory *string, + moduleDirectory *string, + targetDirectory *string, + moduleConfiguration *config.ModuleConfiguration) []itemToLink { + content, err := os.ReadDir(*currentDirectory) + if err != nil { + return nil + } + + itemsToLink := make([]itemToLink, 0, len(content)) + for _, item := range content { + if item.IsDir() { + continue + } + + if currentDirectory == moduleDirectory && item.Name() == ".alma-config.json" { + continue + } + + itemConfigTargetPath := moduleConfiguration.Links[item.Name()] + + var targetPath string + if itemConfigTargetPath != "" { + targetPath = helpers.ResolvePathWithDefault(moduleConfiguration.Links[item.Name()], *targetDirectory) + } else { + targetPath = filepath.Join(*currentTargetDirectory, item.Name()) + } + + itemsToLink = append(itemsToLink, itemToLink{ + source: filepath.Join(*currentDirectory, item.Name()), + target: targetPath, + }) + } + + for _, item := range content { + if !item.IsDir() { + continue + } + + relativePath := getRelativePath(filepath.Join(*currentDirectory, item.Name()), *moduleDirectory) + + itemConfigTargetPath := moduleConfiguration.Links[relativePath] + + if itemConfigTargetPath != "" { + itemsToLink = append(itemsToLink, itemToLink{ + source: filepath.Join(*currentDirectory, item.Name()), + target: helpers.ResolvePathWithDefault(itemConfigTargetPath, *targetDirectory), + }) + } else { + newCurrentDirectory := filepath.Join(*currentDirectory, item.Name()) + newTargetDirectory := filepath.Join(*currentTargetDirectory, item.Name()) + items := TraverseTree( + &newCurrentDirectory, + &newTargetDirectory, + moduleDirectory, + targetDirectory, + moduleConfiguration, + ) + + if items != nil { + itemsToLink = append(itemsToLink, items...) + } + } + } + + return itemsToLink +} + +func linkItems(itemsToLink []itemToLink, dryRun bool) { + for _, item := range itemsToLink { + _, err := os.Stat(item.target) + if err == nil { + println("Target already exists", item.target) + continue + } + + if dryRun { + println("Linking", item.source, item.target) + continue + } + + err = os.Symlink(item.source, item.target) + if err != nil { + println("Error while linking", item.source, item.target) + } + } +} + +func getRelativePath(full string, parent string) string { + return strings.TrimPrefix(full[len(parent):], string(filepath.Separator)) +} diff --git a/src/config/moduleConfiguration.go b/src/config/moduleConfiguration.go new file mode 100644 index 0000000..a425919 --- /dev/null +++ b/src/config/moduleConfiguration.go @@ -0,0 +1,81 @@ +package config + +import ( + "cmp" + "encoding/json" + "io" + "os" + "slices" +) + +type ModuleConfiguration struct { + Target string `json:"target"` + Links map[string]string `json:"links"` + Exclude []string `json:"exclude"` + ExcludeReadme bool `json:"excludeReadme"` + Install string `json:"install"` + Configure string `json:"configure"` +} + +func LoadModuleConfiguration(moduleConfigPath string) *ModuleConfiguration { + jsonFile, err := os.Open(moduleConfigPath) + + if err != nil { + return nil + } + + defer jsonFile.Close() + + byteValue, _ := io.ReadAll(jsonFile) + var moduleConfigurationRoot map[string]*ModuleConfiguration + err = json.Unmarshal(byteValue, &moduleConfigurationRoot) + if err != nil { + return nil + } + + platformKeys := make([]string, 0, len(moduleConfigurationRoot)) + + for key := range moduleConfigurationRoot { + if key != "default" { + platformKeys = append(platformKeys, key) + } + } + + slices.SortFunc(platformKeys, func(i, j string) int { + return cmp.Compare(i, j) + }) + + var moduleConfiguration *ModuleConfiguration + if moduleConfigurationRoot["default"] != nil { + moduleConfiguration = moduleConfigurationRoot["default"] + } + + for _, platformKey := range platformKeys { + moduleConfiguration.Merge(moduleConfigurationRoot[platformKey]) + } + + return moduleConfiguration +} + +func (moduleConfig *ModuleConfiguration) Merge(mergeConfig *ModuleConfiguration) { + mergedLinks := make(map[string]string, len(moduleConfig.Links)+len(mergeConfig.Links)) + for key, value := range moduleConfig.Links { + mergedLinks[key] = value + } + + for key, value := range mergeConfig.Links { + mergedLinks[key] = value + } + + moduleConfig.Links = mergedLinks + + if mergeConfig.Target != "" { + moduleConfig.Target = mergeConfig.Target + } + if mergeConfig.Install != "" { + moduleConfig.Install = mergeConfig.Install + } + if mergeConfig.Configure != "" { + moduleConfig.Configure = mergeConfig.Configure + } +} diff --git a/src/go.mod b/src/go.mod new file mode 100644 index 0000000..bc5b586 --- /dev/null +++ b/src/go.mod @@ -0,0 +1,8 @@ +module alma + +go 1.21.7 + +require ( + github.com/samber/lo v1.39.0 // indirect + golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect +) diff --git a/src/go.sum b/src/go.sum new file mode 100644 index 0000000..69409f9 --- /dev/null +++ b/src/go.sum @@ -0,0 +1,4 @@ +github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= +github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= diff --git a/src/helpers/path.go b/src/helpers/path.go new file mode 100644 index 0000000..257b9a0 --- /dev/null +++ b/src/helpers/path.go @@ -0,0 +1,28 @@ +package helpers + +import ( + "os" + "path/filepath" + "strings" +) + +func ResolvePath(path string) string { + return ResolvePathWithDefault(path, "") +} +func ResolvePathWithDefault(path string, currentDirectory string) string { + skipCombiningCurrentDirectory := false + if strings.Contains(path, "~") { + home, err := os.UserHomeDir() + if err == nil { + path = strings.ReplaceAll(path, "~", home) + skipCombiningCurrentDirectory = true + } + } + + if currentDirectory != "" && !skipCombiningCurrentDirectory { + path = filepath.Join(currentDirectory, path) + } + + path = filepath.FromSlash(path) + return path +} diff --git a/src/helpers/repo.go b/src/helpers/repo.go new file mode 100644 index 0000000..cc1e49a --- /dev/null +++ b/src/helpers/repo.go @@ -0,0 +1,44 @@ +package helpers + +import ( + "os" + "path" + "strings" + + "github.com/samber/lo" +) + +func GetRepositoryAndModuleName(args []string) (*string, *string) { + return getRepositoryAndModuleName(args, false) +} + +func getRepositoryAndModuleName(args []string, singleParamIsRepository bool) (*string, *string) { + var repositoryName, moduleName *string = nil, nil + + usefulArgs := lo.Filter(args, func(arg string, index int) bool { return !strings.HasPrefix(arg, "-") }) + if len(usefulArgs) == 1 { + if singleParamIsRepository { + repositoryName = &usefulArgs[0] + } else { + moduleName = &usefulArgs[0] + } + } else if len(usefulArgs) >= 1 { + repositoryName = &usefulArgs[0] + moduleName = &usefulArgs[1] + } + + return repositoryName, moduleName +} + +func GetRepositorySourceAndTargetDirectory(repoName *string) (*string, *string) { + repoSourceDirectory, _ := os.Getwd() + repoTargetDirectory, _ := os.Getwd() + repoTargetDirectory = path.Join(repoTargetDirectory, "..") + + return GetRepositorySourceAndTargetDirectoryWithFallback(repoName, &repoSourceDirectory, &repoTargetDirectory) +} + +func GetRepositorySourceAndTargetDirectoryWithFallback(repoName *string, sourceFallback *string, targetFallback *string) (*string, *string) { + //TODO: Use repository configuration + return sourceFallback, targetFallback +} diff --git a/src/main.go b/src/main.go new file mode 100644 index 0000000..f5ce99a --- /dev/null +++ b/src/main.go @@ -0,0 +1,14 @@ +package main + +import ( + "os" + "alma/command" +) + +func main() { + commands := []command.Command{ + command.LinkCommand{}, + } + + run(commands, os.Args[1:]) +}