🚧 Link command

This commit is contained in:
2024-07-05 18:57:29 +02:00
parent 859c16b013
commit 5392355eda
9 changed files with 406 additions and 0 deletions

23
src/app.go Normal file
View File

@@ -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)
}

8
src/command/command.go Normal file
View File

@@ -0,0 +1,8 @@
package command
type Command interface {
GetHelpText()
GetName() string
Run(args []string)
}

196
src/command/link.go Normal file
View File

@@ -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))
}

View File

@@ -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
}
}

8
src/go.mod Normal file
View File

@@ -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
)

4
src/go.sum Normal file
View File

@@ -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=

28
src/helpers/path.go Normal file
View File

@@ -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
}

44
src/helpers/repo.go Normal file
View File

@@ -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
}

14
src/main.go Normal file
View File

@@ -0,0 +1,14 @@
package main
import (
"os"
"alma/command"
)
func main() {
commands := []command.Command{
command.LinkCommand{},
}
run(commands, os.Args[1:])
}