From 91b85c06ef716a0f1a7e32b722e5f0bb028d514f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Gerdau?= Date: Fri, 9 Oct 2020 12:31:20 +0200 Subject: [PATCH 1/5] feat: implement re-writing config --- internal/config.go | 26 +++++++++++++++++++++----- internal/config_test.go | 10 +++++----- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/internal/config.go b/internal/config.go index 5d7fed9..61334cf 100644 --- a/internal/config.go +++ b/internal/config.go @@ -2,6 +2,7 @@ package internal import ( "encoding/json" + "errors" "os" "path/filepath" "sort" @@ -25,8 +26,9 @@ type encodable interface { // Config contains all runtime configuration for ew, such as // available tags. type Config struct { - Source ReadSource `json:"-" yaml:"-"` - Tags Tags `json:"tags" yaml:"tags"` + Source ReadSource `json:"-" yaml:"-"` + LoadedFrom string `json:"-" yaml:"-"` + Tags Tags `json:"tags" yaml:"tags"` } // Tags is a convenience wrapper around map[string][]string @@ -153,7 +155,7 @@ func ParseConfigFromFolder(path string) Config { } // If no config is found, use default yaml - return Config{Source: YamlSrc} + return Config{Source: YamlSrc, LoadedFrom: path} } func parseConfigFromYaml(path string) (Config, error) { @@ -166,7 +168,8 @@ func parseConfigFromYaml(path string) (Config, error) { decoder := yaml.NewDecoder(f) config := Config{ - Source: YamlSrc, + Source: YamlSrc, + LoadedFrom: path, } if err := decoder.Decode(&config); err != nil { return Config{}, err @@ -185,7 +188,8 @@ func parseConfigFromJson(path string) (Config, error) { decoder := json.NewDecoder(f) config := Config{ - Source: JsonSrc, + Source: JsonSrc, + LoadedFrom: path, } if err := decoder.Decode(&config); err != nil { return Config{}, err @@ -194,6 +198,16 @@ func parseConfigFromJson(path string) (Config, error) { return config, nil } +// ReWriteConfig re-writes the config from the path it was +// loaded from. +func (c *Config) ReWriteConfig() (string, error) { + if c.LoadedFrom == "" { + return "", errors.New("loaded path not set, cannot re-write") + } + + return c.WriteConfig(c.LoadedFrom) +} + // WriteConfig writes the config to the given folder. // Naming of the file is derived from the read source of // the config. @@ -230,5 +244,7 @@ func (c *Config) WriteConfig(path string) (string, error) { return "", err } + c.LoadedFrom = path + return f.Name(), nil } diff --git a/internal/config_test.go b/internal/config_test.go index d05a6b0..9c0ef55 100644 --- a/internal/config_test.go +++ b/internal/config_test.go @@ -232,7 +232,7 @@ func writeTempFile(t *testing.T, filename string, fileString string) string { func TestParseConfigFromFolder(t *testing.T) { t.Run("no config found", func(t *testing.T) { - want := Config{Source: YamlSrc, Tags: nil} + want := Config{Source: YamlSrc, LoadedFrom: "does-not-exist", Tags: nil} if got := ParseConfigFromFolder("does-not-exist"); !reflect.DeepEqual(got, want) { t.Errorf("ParseConfigFromFolder() = %v, want %v", got, want) } @@ -246,7 +246,7 @@ func TestParseConfigFromFolder(t *testing.T) { clearFolder(t, folder) }() - want := Config{Source: JsonSrc, Tags: Tags{"some-tag": []string{"path1", "path2"}}} + want := Config{Source: JsonSrc, LoadedFrom: folder, Tags: Tags{"some-tag": []string{"path1", "path2"}}} if got := ParseConfigFromFolder(folder); !reflect.DeepEqual(got, want) { t.Errorf("ParseConfigFromFolder() = %v, want %v", got, want) } @@ -258,7 +258,7 @@ func TestParseConfigFromFolder(t *testing.T) { clearFolder(t, folder) }() - want := Config{Source: YamlSrc, Tags: nil} + want := Config{Source: YamlSrc, LoadedFrom: folder, Tags: nil} if got := ParseConfigFromFolder(folder); !reflect.DeepEqual(got, want) { t.Errorf("ParseConfigFromFolder() = %v, want %v", got, want) } @@ -277,7 +277,7 @@ tags: clearFolder(t, folder) }() - want := Config{Source: YamlSrc, Tags: Tags{"some-tag": []string{"path1", "path2"}}} + want := Config{Source: YamlSrc, LoadedFrom: folder, Tags: Tags{"some-tag": []string{"path1", "path2"}}} if got := ParseConfigFromFolder(folder); !reflect.DeepEqual(got, want) { t.Errorf("ParseConfigFromFolder() = %v, want %v", got, want) } @@ -289,7 +289,7 @@ tags: clearFolder(t, folder) }() - want := Config{Source: YamlSrc, Tags: nil} + want := Config{Source: YamlSrc, LoadedFrom: folder, Tags: nil} if got := ParseConfigFromFolder(folder); !reflect.DeepEqual(got, want) { t.Errorf("ParseConfigFromFolder() = %v, want %v", got, want) } From b59fd7613df4065cae38feacbc34c41a32f308cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Gerdau?= Date: Fri, 9 Oct 2020 12:32:11 +0200 Subject: [PATCH 2/5] feat: add possibility to add paths to tag via command --- README.md | 4 +- internal/cmd/cmd_add_paths.go | 89 +++++++++++++++++++++++++++++++++++ internal/cmd/cmd_help.go | 4 +- internal/cmd/parser.go | 3 +- internal/config.go | 27 +++++++++++ 5 files changed, 121 insertions(+), 6 deletions(-) create mode 100644 internal/cmd/cmd_add_paths.go diff --git a/README.md b/README.md index f9a2407..75e2c07 100644 --- a/README.md +++ b/README.md @@ -49,8 +49,8 @@ ew paths | list all paths (alias for ew paths list) ew paths list | list all paths ew tags | list all tags (alias for ew tags list) ew tags list | list all tags -ew tags add @some-tag | add current directory to tag "some-tag" (NOT YET IMPLEMENTED) -ew tags add \some\path @some-tag | add \some\path to tag "some-tag" (NOT YET IMPLEMENTED) +ew tags add @some-tag | add current directory to tag "some-tag" +ew tags add \some\path @some-tag | add \some\path to tag "some-tag" ew tags rm @some-tag | add current directory to tag "some-tag" (NOT YET IMPLEMENTED) ew tags rm \some\path @some-tag | add \some\path to tag "some-tag" (NOT YET IMPLEMENTED) ew status | show quick git status for all paths diff --git a/internal/cmd/cmd_add_paths.go b/internal/cmd/cmd_add_paths.go new file mode 100644 index 0000000..6db3efb --- /dev/null +++ b/internal/cmd/cmd_add_paths.go @@ -0,0 +1,89 @@ +package cmd + +import ( + "github.com/kernle32dll/ew/internal" + + "errors" + "fmt" + "io" + "os" + "strings" +) + +// AddPathsCommand adds one or multiple paths to a tag. +type AddPathsCommand struct { + output io.Writer + config internal.Config + args []string +} + +// NewAddPathsCommand creates a new AddPathsCommand. +func NewAddPathsCommand( + output io.Writer, + config internal.Config, + args []string, +) *AddPathsCommand { + return &AddPathsCommand{ + output: output, + config: config, + args: args, + } +} + +func (c AddPathsCommand) Execute() error { + if len(c.args) == 0 { + return errors.New("missing @tag to add path to") + } + + // Current path to tag + if len(c.args) == 1 { + return c.addCurrentPath() + } + + // Multiple paths + return c.addPaths() +} + +func (c AddPathsCommand) addPaths() error { + tag := c.args[len(c.args)-1] + if !strings.HasPrefix(tag, "@") { + return fmt.Errorf("expected @tag for last argument, got %q", tag) + } + tag = strings.TrimPrefix(tag, "@") + + paths := c.args[:1] + + c.config.AddPathsToTag(tag, paths...) + + if _, err := c.config.ReWriteConfig(); err != nil { + return err + } + + fmt.Fprintf(c.output, "Added paths to tag @%s:\n", tag) + for _, path := range paths { + fmt.Fprintf(c.output, " %s\n", path) + } + return nil +} + +func (c AddPathsCommand) addCurrentPath() error { + tag := c.args[0] + if !strings.HasPrefix(tag, "@") { + return fmt.Errorf("expected @tag, got %q", tag) + } + tag = strings.TrimPrefix(tag, "@") + + path, err := os.Getwd() + if err != nil { + return err + } + + c.config.AddPathsToTag(tag, path) + + if _, err := c.config.ReWriteConfig(); err != nil { + return err + } + + fmt.Fprintf(c.output, "Added path %s to tag @%s\n", path, tag) + return nil +} diff --git a/internal/cmd/cmd_help.go b/internal/cmd/cmd_help.go index 3615faf..647d8d8 100644 --- a/internal/cmd/cmd_help.go +++ b/internal/cmd/cmd_help.go @@ -45,8 +45,8 @@ func (c HelpCommand) Execute() error { {"ew paths list", "list all paths"}, {"ew tags", "list all tags (alias for ew tags list)"}, {"ew tags list", "list all tags"}, - {"ew tags add @some-tag", "add current directory to tag \"some-tag\"" + nyi}, - {"ew tags add \\some\\path @some-tag", "add \\some\\path to tag \"some-tag\"" + nyi}, + {"ew tags add @some-tag", "add current directory to tag \"some-tag\""}, + {"ew tags add \\some\\path @some-tag", "add \\some\\path to tag \"some-tag\""}, {"ew tags rm @some-tag", "add current directory to tag \"some-tag\"" + nyi}, {"ew tags rm \\some\\path @some-tag", "add \\some\\path to tag \"some-tag\"" + nyi}, {"ew status", "show quick git status for all paths"}, diff --git a/internal/cmd/parser.go b/internal/cmd/parser.go index e390db5..890e498 100644 --- a/internal/cmd/parser.go +++ b/internal/cmd/parser.go @@ -40,8 +40,7 @@ func ParseCommand(output io.Writer, conf internal.Config, args []string) (Comman } if args[1] == "add" { - //&ListPathsCommand{config: conf}.Execute() - return nil, errors.New("tag adding not implemented yet") + return NewAddPathsCommand(output, conf, args[2:]), nil } if args[1] == "rm" { //&ListPathsCommand{config: conf}.Execute() diff --git a/internal/config.go b/internal/config.go index 61334cf..119c14f 100644 --- a/internal/config.go +++ b/internal/config.go @@ -34,6 +34,33 @@ type Config struct { // Tags is a convenience wrapper around map[string][]string type Tags map[string][]string +// AddPathsToTag adds a list of paths to a tag. +func (c *Config) AddPathsToTag(tag string, paths ...string) { + if len(paths) == 0 { + return + } + + c.Tags[tag] = deDuplicateAndSort(append(c.Tags[tag], paths...)) +} + +func deDuplicateAndSort(keys []string) []string { + table := make(map[string]struct{}, len(keys)) + for _, key := range keys { + table[key] = struct{}{} + } + + deduped := make([]string, len(table)) + i := 0 + for key := range table { + deduped[i] = key + i++ + } + + sort.Strings(deduped) + + return deduped +} + // GetTagsSorted returns a sorted list of configured tags. func (c Config) GetTagsSorted() []string { if len(c.Tags) == 0 { From d234dfdd4f99b468e7aa39395ef46de3a1207897 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Gerdau?= Date: Fri, 9 Oct 2020 15:12:00 +0200 Subject: [PATCH 3/5] feat: add possibility to remove paths from tag via command --- README.md | 4 +- internal/cmd/cmd_help.go | 7 +-- internal/cmd/cmd_rm_paths.go | 89 ++++++++++++++++++++++++++++++++++++ internal/cmd/parser.go | 4 +- internal/config.go | 22 +++++++++ 5 files changed, 116 insertions(+), 10 deletions(-) create mode 100644 internal/cmd/cmd_rm_paths.go diff --git a/README.md b/README.md index 75e2c07..146b349 100644 --- a/README.md +++ b/README.md @@ -51,8 +51,8 @@ ew tags | list all tags (alias for ew tags list) ew tags list | list all tags ew tags add @some-tag | add current directory to tag "some-tag" ew tags add \some\path @some-tag | add \some\path to tag "some-tag" -ew tags rm @some-tag | add current directory to tag "some-tag" (NOT YET IMPLEMENTED) -ew tags rm \some\path @some-tag | add \some\path to tag "some-tag" (NOT YET IMPLEMENTED) +ew tags rm @some-tag | add current directory to tag "some-tag" +ew tags rm \some\path @some-tag | add \some\path to tag "some-tag" ew status | show quick git status for all paths ew @tag1 status | show quick git status for all paths of tag1 (supports multiple tags) ew @tag1 some-cmd | executes some-cmd in all paths of tag1 (supports multiple tags) diff --git a/internal/cmd/cmd_help.go b/internal/cmd/cmd_help.go index 647d8d8..eff61b4 100644 --- a/internal/cmd/cmd_help.go +++ b/internal/cmd/cmd_help.go @@ -1,8 +1,6 @@ package cmd import ( - "github.com/fatih/color" - "fmt" "io" "strings" @@ -32,7 +30,6 @@ func (c HelpCommand) Execute() error { fmt.Fprintln(c.output) fmt.Fprintln(c.output, "Available commands:") - nyi := color.RedString(" (NOT YET IMPLEMENTED)") cmds := [][]string{ {"ew", "list all paths, grouped by their tags"}, {"ew help", "displays this help"}, @@ -47,8 +44,8 @@ func (c HelpCommand) Execute() error { {"ew tags list", "list all tags"}, {"ew tags add @some-tag", "add current directory to tag \"some-tag\""}, {"ew tags add \\some\\path @some-tag", "add \\some\\path to tag \"some-tag\""}, - {"ew tags rm @some-tag", "add current directory to tag \"some-tag\"" + nyi}, - {"ew tags rm \\some\\path @some-tag", "add \\some\\path to tag \"some-tag\"" + nyi}, + {"ew tags rm @some-tag", "add current directory to tag \"some-tag\""}, + {"ew tags rm \\some\\path @some-tag", "add \\some\\path to tag \"some-tag\""}, {"ew status", "show quick git status for all paths"}, {"ew @tag1 status", "show quick git status for all paths of tag1 (supports multiple tags)"}, {"ew @tag1 some-cmd", "executes some-cmd in all paths of tag1 (supports multiple tags)"}, diff --git a/internal/cmd/cmd_rm_paths.go b/internal/cmd/cmd_rm_paths.go new file mode 100644 index 0000000..79f37d2 --- /dev/null +++ b/internal/cmd/cmd_rm_paths.go @@ -0,0 +1,89 @@ +package cmd + +import ( + "github.com/kernle32dll/ew/internal" + + "errors" + "fmt" + "io" + "os" + "strings" +) + +// RmPathsCommand removes one or multiple paths from a tag. +type RmPathsCommand struct { + output io.Writer + config internal.Config + args []string +} + +// NewRmPathsCommand creates a new RmPathsCommand. +func NewRmPathsCommand( + output io.Writer, + config internal.Config, + args []string, +) *RmPathsCommand { + return &RmPathsCommand{ + output: output, + config: config, + args: args, + } +} + +func (c RmPathsCommand) Execute() error { + if len(c.args) == 0 { + return errors.New("missing @tag to remove path from") + } + + // Current path from tag + if len(c.args) == 1 { + return c.rmCurrentPath() + } + + // Multiple paths + return c.rmPaths() +} + +func (c RmPathsCommand) rmPaths() error { + tag := c.args[len(c.args)-1] + if !strings.HasPrefix(tag, "@") { + return fmt.Errorf("expected @tag for last argument, got %q", tag) + } + tag = strings.TrimPrefix(tag, "@") + + paths := c.args[:1] + + c.config.RemovePathsFromTag(tag, paths...) + + if _, err := c.config.ReWriteConfig(); err != nil { + return err + } + + fmt.Fprintf(c.output, "Removed paths from tag @%s:\n", tag) + for _, path := range paths { + fmt.Fprintf(c.output, " %s\n", path) + } + return nil +} + +func (c RmPathsCommand) rmCurrentPath() error { + tag := c.args[0] + if !strings.HasPrefix(tag, "@") { + return fmt.Errorf("expected @tag, got %q", tag) + } + tag = strings.TrimPrefix(tag, "@") + + path, err := os.Getwd() + if err != nil { + return err + } + + c.config.RemovePathsFromTag(tag, path) + + if _, err := c.config.ReWriteConfig(); err != nil { + return err + } + + fmt.Fprintf(c.output, "Removing path %s from tag @%s\n", path, tag) + return nil +} diff --git a/internal/cmd/parser.go b/internal/cmd/parser.go index 890e498..8e02cff 100644 --- a/internal/cmd/parser.go +++ b/internal/cmd/parser.go @@ -3,7 +3,6 @@ package cmd import ( "github.com/kernle32dll/ew/internal" - "errors" "io" "strings" ) @@ -43,8 +42,7 @@ func ParseCommand(output io.Writer, conf internal.Config, args []string) (Comman return NewAddPathsCommand(output, conf, args[2:]), nil } if args[1] == "rm" { - //&ListPathsCommand{config: conf}.Execute() - return nil, errors.New("tag adding not implemented yet") + return NewRmPathsCommand(output, conf, args[2:]), nil } } diff --git a/internal/config.go b/internal/config.go index 119c14f..d486f0c 100644 --- a/internal/config.go +++ b/internal/config.go @@ -43,6 +43,28 @@ func (c *Config) AddPathsToTag(tag string, paths ...string) { c.Tags[tag] = deDuplicateAndSort(append(c.Tags[tag], paths...)) } +// RemovePathsFromTag removes a list of paths from a tag. +func (c *Config) RemovePathsFromTag(tag string, paths ...string) { + if len(paths) == 0 { + return + } + + // Prepare full array size, and splice later + newTags := make([]string, len(c.Tags[tag])) + i, rmCount := 0, 0 + for _, path := range c.Tags[tag] { + if contains(paths, path) { + rmCount++ + continue + } + + newTags[i] = path + i++ + } + + c.Tags[tag] = newTags[:len(newTags)-rmCount] +} + func deDuplicateAndSort(keys []string) []string { table := make(map[string]struct{}, len(keys)) for _, key := range keys { From 66335d33664d901e670b047b82c662a9be16756d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Gerdau?= Date: Fri, 9 Oct 2020 15:12:40 +0200 Subject: [PATCH 4/5] fix: remove remark about missing functionality - its implemented now --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 146b349..23b4bc9 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,6 @@ ew - short for `(run things) e(very)w(here)` is a tool for grouping folders by tags, and executing tasks in all folders via these tags. -**Note**: Currently it is not possible to add or remove folders to tags via the cli. -See paragraph "Config" below, to see how to do this manually. - ## Quickstart You need a working installation of the Go tooling for installation: From 8620e908dffb96c9628d7ad0f4f0ee1cfc4dc37a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Gerdau?= Date: Tue, 13 Oct 2020 20:36:07 +0200 Subject: [PATCH 5/5] refactor: make migrate command use ReWriteConfig func --- internal/cmd/cmd_migrate.go | 2 +- internal/gr/grconfig.go | 6 ++++-- internal/gr/grconfig_test.go | 4 +++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/internal/cmd/cmd_migrate.go b/internal/cmd/cmd_migrate.go index ad39199..cd60984 100644 --- a/internal/cmd/cmd_migrate.go +++ b/internal/cmd/cmd_migrate.go @@ -49,7 +49,7 @@ func (c MigrateCommand) Execute() error { conf.Source = internal.YamlSrc } - migratedPath, err := conf.WriteConfig(home) + migratedPath, err := conf.ReWriteConfig() if err != nil { return err } diff --git a/internal/gr/grconfig.go b/internal/gr/grconfig.go index c94ae0b..6f8b86e 100644 --- a/internal/gr/grconfig.go +++ b/internal/gr/grconfig.go @@ -5,6 +5,7 @@ import ( "encoding/json" "os" + "path/filepath" ) type config struct { @@ -30,7 +31,8 @@ func ParseConfigFromGr(filename string) (internal.Config, error) { } return internal.Config{ - Source: internal.JsonSrc, - Tags: map[string][]string(config.Tags), + Source: internal.JsonSrc, + LoadedFrom: filepath.Dir(filename), + Tags: map[string][]string(config.Tags), }, nil } diff --git a/internal/gr/grconfig_test.go b/internal/gr/grconfig_test.go index 5c8a434..5e50d3b 100644 --- a/internal/gr/grconfig_test.go +++ b/internal/gr/grconfig_test.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "os" + "path/filepath" "reflect" "testing" ) @@ -26,7 +27,8 @@ func TestParseConfigFromGr(t *testing.T) { {name: "not existing", filename: "does-not-exist", want: internal.Config{}, wantErr: true}, {name: "broken file", filename: brokenFile.Name(), want: internal.Config{}, wantErr: true}, {name: "working file", filename: workingFile.Name(), want: internal.Config{ - Source: internal.JsonSrc, + Source: internal.JsonSrc, + LoadedFrom: filepath.Clean(os.TempDir()), Tags: map[string][]string{ "tag1": {"path1a", "path1b"}, "tag2": {"path2a"},