From 567faeb07e1511af0784276d10856598fbf77511 Mon Sep 17 00:00:00 2001 From: boombuler Date: Fri, 23 Sep 2016 10:03:42 +0200 Subject: [PATCH 01/18] initial commit of pluginmanager --- cmd/micro/pluginmanager.go | 345 ++++++++++++++++++++++++++++++++ cmd/micro/pluginmanager_test.go | 54 +++++ 2 files changed, 399 insertions(+) create mode 100644 cmd/micro/pluginmanager.go create mode 100644 cmd/micro/pluginmanager_test.go diff --git a/cmd/micro/pluginmanager.go b/cmd/micro/pluginmanager.go new file mode 100644 index 00000000..25e0890d --- /dev/null +++ b/cmd/micro/pluginmanager.go @@ -0,0 +1,345 @@ +package main + +import ( + "archive/zip" + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "regexp" + "sort" + "strings" + "sync" + + "github.com/blang/semver" + "github.com/yuin/gopher-lua" +) + +var Repositories []PluginRepository = []PluginRepository{} + +type PluginRepository string + +type PluginPackage struct { + Name string + Description string + Author string + Tags []string + Versions PluginVersions +} + +type PluginPackages []*PluginPackage + +type PluginVersion struct { + pack *PluginPackage + Version semver.Version + Url string + Require PluginDependencies +} +type PluginVersions []*PluginVersion + +type PluginDependency struct { + Name string + Range semver.Range +} +type PluginDependencies []*PluginDependency + +func (pv *PluginVersion) UnmarshalJSON(data []byte) error { + var values struct { + Version semver.Version + Url string + Require map[string]string + } + + if err := json.Unmarshal(data, &values); err != nil { + return err + } + pv.Version = values.Version + pv.Url = values.Url + pv.Require = make(PluginDependencies, 0) + + for k, v := range values.Require { + if vRange, err := semver.ParseRange(v); err == nil { + pv.Require = append(pv.Require, &PluginDependency{k, vRange}) + } + } + return nil +} + +func (pv *PluginVersion) String() string { + return fmt.Sprintf("%s (%s)", pv.pack.Name, pv.Version) +} + +func (pd *PluginDependency) String() string { + return pd.Name +} + +func (pp *PluginPackage) UnmarshalJSON(data []byte) error { + var values struct { + Name string + Description string + Author string + Tags []string + Versions PluginVersions + } + if err := json.Unmarshal(data, &values); err != nil { + return err + } + pp.Name = values.Name + pp.Description = values.Description + pp.Author = values.Author + pp.Tags = values.Tags + pp.Versions = values.Versions + for _, v := range pp.Versions { + v.pack = pp + } + return nil +} + +func (pv PluginVersions) Find(name string) *PluginVersion { + for _, v := range pv { + if v.pack.Name == name { + return v + } + } + return nil +} +func (pv PluginVersions) Len() int { + return len(pv) +} + +func (pv PluginVersions) Swap(i, j int) { + pv[i], pv[j] = pv[j], pv[i] +} + +func (s PluginVersions) Less(i, j int) bool { + // sort descending + return s[i].Version.GT(s[j].Version) +} + +func (pr PluginRepository) Query() <-chan *PluginPackage { + resChan := make(chan *PluginPackage) + go func() { + defer close(resChan) + + resp, err := http.Get(string(pr)) + if err != nil { + TermMessage("Failed to query plugin repository:\n", err) + return + } + defer resp.Body.Close() + decoder := json.NewDecoder(resp.Body) + + var plugins PluginPackages + if err := decoder.Decode(&plugins); err != nil { + TermMessage("Failed to decode repository data:\n", err) + return + } + for _, p := range plugins { + resChan <- p + } + }() + return resChan +} + +func (pp *PluginPackage) GetInstallableVersion() *PluginVersion { + matching := make(PluginVersions, 0) + +versionLoop: + for _, pv := range pp.Versions { + for _, req := range pv.Require { + curVersion := GetInstalledVersion(req.Name) + if curVersion == nil || !req.Range(*curVersion) { + continue versionLoop + } + } + matching = append(matching, pv) + } + if len(matching) > 0 { + sort.Sort(matching) + return matching[0] + } + return nil +} + +func (pp PluginPackage) Match(text string) bool { + // ToDo: improve matching. + text = "(?i)" + text + if r, err := regexp.Compile(text); err == nil { + return r.MatchString(pp.Name) + } + return false +} + +func SearchPlugin(text string) (plugins []*PluginPackage) { + wgQuery := new(sync.WaitGroup) + wgQuery.Add(len(Repositories)) + results := make(chan *PluginPackage) + + wgDone := new(sync.WaitGroup) + wgDone.Add(1) + for _, repo := range Repositories { + go func(repo PluginRepository) { + res := repo.Query() + for r := range res { + results <- r + } + wgQuery.Done() + }(repo) + } + go func() { + for res := range results { + if res.GetInstallableVersion() != nil && res.Match(text) { + plugins = append(plugins, res) + } + } + wgDone.Done() + }() + wgQuery.Wait() + close(results) + wgDone.Wait() + return +} + +func GetInstalledVersion(name string) *semver.Version { + versionStr := "" + if name == "micro" { + versionStr = Version + + } else { + plugin := L.GetGlobal(name) + if plugin == lua.LNil { + return nil + } + version := L.GetField(plugin, "VERSION") + if str, ok := version.(lua.LString); ok { + versionStr = string(str) + } + } + + if v, err := semver.Parse(versionStr); err != nil { + return nil + } else { + return &v + } +} + +func (pv *PluginVersion) Install() { + resp, err := http.Get(pv.Url) + if err == nil { + defer resp.Body.Close() + data, _ := ioutil.ReadAll(resp.Body) + zipbuf := bytes.NewReader(data) + z, err := zip.NewReader(zipbuf, zipbuf.Size()) + if err == nil { + targetDir := filepath.Join(configDir, "plugins", pv.pack.Name) + dirPerm := os.FileMode(0755) + if err = os.MkdirAll(targetDir, dirPerm); err == nil { + for _, f := range z.File { + targetName := filepath.Join(targetDir, filepath.Join(strings.Split(f.Name, "/")...)) + if f.FileInfo().IsDir() { + err = os.MkdirAll(targetName, dirPerm) + } else { + content, err := f.Open() + if err == nil { + defer content.Close() + if target, err := os.Create(targetName); err == nil { + defer target.Close() + _, err = io.Copy(target, content) + } + } + } + if err != nil { + break + } + } + } + } + } + if err != nil { + TermMessage("Failed to install plugin:", err) + } +} + +func UninstallPlugin(name string) { + os.RemoveAll(filepath.Join(configDir, name)) +} + +// Updates... + +func (pl PluginPackages) Get(name string) *PluginPackage { + for _, p := range pl { + if p.Name == name { + return p + } + } + return nil +} + +func (pl PluginPackages) GetAllVersions(name string) PluginVersions { + result := make(PluginVersions, 0) + p := pl.Get(name) + if p != nil { + for _, v := range p.Versions { + result = append(result, v) + } + } + return result +} + +func (req PluginDependencies) Join(other PluginDependencies) PluginDependencies { + m := make(map[string]*PluginDependency) + for _, r := range req { + m[r.Name] = r + } + for _, o := range other { + cur, ok := m[o.Name] + if ok { + m[o.Name] = &PluginDependency{ + o.Name, + o.Range.AND(cur.Range), + } + } else { + m[o.Name] = o + } + } + result := make(PluginDependencies, 0, len(m)) + for _, v := range m { + result = append(result, v) + } + return result +} + +func (all PluginPackages) ResolveStep(selectedVersions PluginVersions, open PluginDependencies) (PluginVersions, error) { + if len(open) == 0 { + return selectedVersions, nil + } + currentRequirement, stillOpen := open[0], open[1:] + if currentRequirement != nil { + if selVersion := selectedVersions.Find(currentRequirement.Name); selVersion != nil { + if currentRequirement.Range(selVersion.Version) { + return all.ResolveStep(selectedVersions, stillOpen) + } + return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name) + } else { + availableVersions := all.GetAllVersions(currentRequirement.Name) + sort.Sort(availableVersions) + + for _, version := range availableVersions { + if currentRequirement.Range(version.Version) { + resolved, err := all.ResolveStep(append(selectedVersions, version), stillOpen.Join(version.Require)) + + if err == nil { + return resolved, nil + } + } + } + return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name) + } + } else { + return selectedVersions, nil + } +} diff --git a/cmd/micro/pluginmanager_test.go b/cmd/micro/pluginmanager_test.go new file mode 100644 index 00000000..47960391 --- /dev/null +++ b/cmd/micro/pluginmanager_test.go @@ -0,0 +1,54 @@ +package main + +import ( + "encoding/json" + "github.com/blang/semver" + "testing" +) + +func TestDependencyResolving(t *testing.T) { + js := ` +[{ + "Name": "Foo", + "Versions": [{ "Version": "1.0.0" }, { "Version": "1.5.0" },{ "Version": "2.0.0" }] +}, { + "Name": "Bar", + "Versions": [{ "Version": "1.0.0", "Require": {"Foo": ">1.0.0 <2.0.0"} }] +}, { + "Name": "Unresolvable", + "Versions": [{ "Version": "1.0.0", "Require": {"Foo": "<=1.0.0", "Bar": ">0.0.0"} }] + }] +` + var all PluginPackages + err := json.Unmarshal([]byte(js), &all) + if err != nil { + t.Error(err) + } + selected, err := all.ResolveStep(PluginVersions{}, PluginDependencies{ + &PluginDependency{"Bar", semver.MustParseRange(">=1.0.0")}, + }) + + check := func(name, version string) { + v := selected.Find(name) + expected := semver.MustParse(version) + if v == nil { + t.Errorf("Failed to resolve %s", name) + } else if expected.NE(v.Version) { + t.Errorf("%s resolved in wrong version got %s", name, v) + } + } + + if err != nil { + t.Error(err) + } else { + check("Foo", "1.5.0") + check("Bar", "1.0.0") + } + + selected, err = all.ResolveStep(PluginVersions{}, PluginDependencies{ + &PluginDependency{"Unresolvable", semver.MustParseRange(">0.0.0")}, + }) + if err == nil { + t.Error("Unresolvable package resolved:", selected) + } +} From f351c251e46253c334ad641ce6d083dc8565ea5f Mon Sep 17 00:00:00 2001 From: Florian Sundermann Date: Mon, 26 Sep 2016 16:53:39 +0200 Subject: [PATCH 02/18] first few pm commands --- cmd/micro/command.go | 35 ++++ cmd/micro/micro.go | 2 +- cmd/micro/pluginmanager.go | 419 ++++++++++++++++++++++++------------- 3 files changed, 314 insertions(+), 142 deletions(-) diff --git a/cmd/micro/command.go b/cmd/micro/command.go index f0cfe9b3..60b64558 100644 --- a/cmd/micro/command.go +++ b/cmd/micro/command.go @@ -39,6 +39,7 @@ var commandActions = map[string]func([]string){ "Tab": NewTab, "Help": Help, "Eval": Eval, + "Plugin": PluginCmd, } // InitCommands initializes the default commands @@ -84,6 +85,40 @@ func DefaultCommands() map[string]StrCommand { "tab": {"Tab", []Completion{FileCompletion, NoCompletion}}, "help": {"Help", []Completion{HelpCompletion, NoCompletion}}, "eval": {"Eval", []Completion{NoCompletion}}, + "plugin": {"Plugin", []Completion{NoCompletion}}, + } +} + +// InstallPlugin installs the given plugin by exact name match +func PluginCmd(args []string) { + if len(args) >= 1 { + switch args[0] { + case "install": + for _, plugin := range args[1:] { + pp := GetAllPluginPackages().Get(plugin) + if pp == nil { + messenger.Error("Unknown plugin \"" + plugin + "\"") + } else if !pp.IsInstallable() { + messenger.Error("Plugin \"" + plugin + "\" can not be installed.") + } else { + pp.Install() + } + } + case "remove": + for _, plugin := range args[1:] { + // check if the plugin exists. + for _, lp := range loadedPlugins { + if lp == plugin { + UninstallPlugin(plugin) + continue + } + } + } + case "update": + UpdatePlugins() + } + } else { + messenger.Error("Not enough arguments") } } diff --git a/cmd/micro/micro.go b/cmd/micro/micro.go index 2d2829c8..a3a75b23 100644 --- a/cmd/micro/micro.go +++ b/cmd/micro/micro.go @@ -44,7 +44,7 @@ var ( // Version is the version number or commit hash // These variables should be set by the linker when compiling - Version = "Unknown" + Version = "2.0.0" CommitHash = "Unknown" CompileDate = "Unknown" diff --git a/cmd/micro/pluginmanager.go b/cmd/micro/pluginmanager.go index 25e0890d..43296473 100644 --- a/cmd/micro/pluginmanager.go +++ b/cmd/micro/pluginmanager.go @@ -19,10 +19,24 @@ import ( "github.com/yuin/gopher-lua" ) -var Repositories []PluginRepository = []PluginRepository{} +var ( + pluginChannels PluginChannels = PluginChannels{ + PluginChannel("https://www.boombuler.de/channel.json"), + } + allPluginPackages PluginPackages = nil +) + +// PluginChannel contains an url to a json list of PluginRepository +type PluginChannel string + +// PluginChannels is a slice of PluginChannel +type PluginChannels []PluginChannel + +// PluginRepository contains an url to json file containing PluginPackages type PluginRepository string +// PluginPackage contains the meta-data of a plugin and all available versions type PluginPackage struct { Name string Description string @@ -31,22 +45,103 @@ type PluginPackage struct { Versions PluginVersions } +// PluginPackages is a list of PluginPackage instances. type PluginPackages []*PluginPackage +// PluginVersion descripes a version of a PluginPackage. Containing a version, download url and also dependencies. type PluginVersion struct { pack *PluginPackage Version semver.Version Url string Require PluginDependencies } + +// PluginVersions is a slice of PluginVersion type PluginVersions []*PluginVersion +// PluginDenendency descripes a dependency to another plugin or micro itself. type PluginDependency struct { Name string Range semver.Range } + +// PluginDependencies is a slice of PluginDependency type PluginDependencies []*PluginDependency +func fetchAllSources(count int, fetcher func(i int) PluginPackages) PluginPackages { + wgQuery := new(sync.WaitGroup) + wgQuery.Add(count) + + results := make(chan PluginPackages) + + wgDone := new(sync.WaitGroup) + wgDone.Add(1) + var packages PluginPackages + for i := 0; i < count; i++ { + go func(i int) { + results <- fetcher(i) + wgQuery.Done() + }(i) + } + go func() { + packages = make(PluginPackages, 0) + for res := range results { + packages = append(packages, res...) + } + wgDone.Done() + }() + wgQuery.Wait() + close(results) + wgDone.Wait() + return packages +} + +// Fetch retrieves all available PluginPackages from the given channels +func (pc PluginChannels) Fetch() PluginPackages { + return fetchAllSources(len(pc), func(i int) PluginPackages { + return pc[i].Fetch() + }) +} + +// Fetch retrieves all available PluginPackages from the given channel +func (pc PluginChannel) Fetch() PluginPackages { + resp, err := http.Get(string(pc)) + if err != nil { + TermMessage("Failed to query plugin channel:\n", err) + return PluginPackages{} + } + defer resp.Body.Close() + decoder := json.NewDecoder(resp.Body) + + var repositories []PluginRepository + if err := decoder.Decode(&repositories); err != nil { + TermMessage("Failed to decode channel data:\n", err) + return PluginPackages{} + } + return fetchAllSources(len(repositories), func(i int) PluginPackages { + return repositories[i].Fetch() + }) +} + +// Fetch retrieves all available PluginPackages from the given repository +func (pr PluginRepository) Fetch() PluginPackages { + resp, err := http.Get(string(pr)) + if err != nil { + TermMessage("Failed to query plugin repository:\n", err) + return PluginPackages{} + } + defer resp.Body.Close() + decoder := json.NewDecoder(resp.Body) + + var plugins PluginPackages + if err := decoder.Decode(&plugins); err != nil { + TermMessage("Failed to decode repository data:\n", err) + return PluginPackages{} + } + return plugins +} + +// UnmarshalJSON unmarshals raw json to a PluginVersion func (pv *PluginVersion) UnmarshalJSON(data []byte) error { var values struct { Version semver.Version @@ -69,14 +164,7 @@ func (pv *PluginVersion) UnmarshalJSON(data []byte) error { return nil } -func (pv *PluginVersion) String() string { - return fmt.Sprintf("%s (%s)", pv.pack.Name, pv.Version) -} - -func (pd *PluginDependency) String() string { - return pd.Name -} - +// UnmarshalJSON unmarshals raw json to a PluginPackage func (pp *PluginPackage) UnmarshalJSON(data []byte) error { var values struct { Name string @@ -99,72 +187,39 @@ func (pp *PluginPackage) UnmarshalJSON(data []byte) error { return nil } -func (pv PluginVersions) Find(name string) *PluginVersion { +// GetAllPluginPackages gets all PluginPackages which may be available. +func GetAllPluginPackages() PluginPackages { + if allPluginPackages == nil { + allPluginPackages = pluginChannels.Fetch() + } + return allPluginPackages +} + +func (pv PluginVersions) find(ppName string) *PluginVersion { for _, v := range pv { - if v.pack.Name == name { + if v.pack.Name == ppName { return v } } return nil } + +// Len returns the number of pluginversions in this slice func (pv PluginVersions) Len() int { return len(pv) } +// Swap two entries of the slice func (pv PluginVersions) Swap(i, j int) { pv[i], pv[j] = pv[j], pv[i] } +// Less returns true if the version at position i is greater then the version at position j (used for sorting) func (s PluginVersions) Less(i, j int) bool { - // sort descending return s[i].Version.GT(s[j].Version) } -func (pr PluginRepository) Query() <-chan *PluginPackage { - resChan := make(chan *PluginPackage) - go func() { - defer close(resChan) - - resp, err := http.Get(string(pr)) - if err != nil { - TermMessage("Failed to query plugin repository:\n", err) - return - } - defer resp.Body.Close() - decoder := json.NewDecoder(resp.Body) - - var plugins PluginPackages - if err := decoder.Decode(&plugins); err != nil { - TermMessage("Failed to decode repository data:\n", err) - return - } - for _, p := range plugins { - resChan <- p - } - }() - return resChan -} - -func (pp *PluginPackage) GetInstallableVersion() *PluginVersion { - matching := make(PluginVersions, 0) - -versionLoop: - for _, pv := range pp.Versions { - for _, req := range pv.Require { - curVersion := GetInstalledVersion(req.Name) - if curVersion == nil || !req.Range(*curVersion) { - continue versionLoop - } - } - matching = append(matching, pv) - } - if len(matching) > 0 { - sort.Sort(matching) - return matching[0] - } - return nil -} - +// Match returns true if the package matches a given search text func (pp PluginPackage) Match(text string) bool { // ToDo: improve matching. text = "(?i)" + text @@ -174,102 +229,119 @@ func (pp PluginPackage) Match(text string) bool { return false } -func SearchPlugin(text string) (plugins []*PluginPackage) { - wgQuery := new(sync.WaitGroup) - wgQuery.Add(len(Repositories)) - results := make(chan *PluginPackage) +// IsInstallable returns true if the package can be installed. +func (pp PluginPackage) IsInstallable() bool { + _, err := GetAllPluginPackages().Resolve(GetInstalledVersions(), PluginDependencies{ + &PluginDependency{ + Name: pp.Name, + Range: semver.Range(func(v semver.Version) bool { return true }), + }}) + return err == nil +} - wgDone := new(sync.WaitGroup) - wgDone.Add(1) - for _, repo := range Repositories { - go func(repo PluginRepository) { - res := repo.Query() - for r := range res { - results <- r - } - wgQuery.Done() - }(repo) - } - go func() { - for res := range results { - if res.GetInstallableVersion() != nil && res.Match(text) { - plugins = append(plugins, res) - } +// SearchPlugin retrieves a list of all PluginPackages which match the given search text and +// could be or are already installed +func SearchPlugin(text string) (plugins PluginPackages) { + plugins = make(PluginPackages, 0) + for _, pp := range GetAllPluginPackages() { + if pp.Match(text) && pp.IsInstallable() { + plugins = append(plugins, pp) } - wgDone.Done() - }() - wgQuery.Wait() - close(results) - wgDone.Wait() + } return } -func GetInstalledVersion(name string) *semver.Version { - versionStr := "" - if name == "micro" { - versionStr = Version - - } else { - plugin := L.GetGlobal(name) - if plugin == lua.LNil { - return nil - } - version := L.GetField(plugin, "VERSION") - if str, ok := version.(lua.LString); ok { - versionStr = string(str) - } - } - - if v, err := semver.Parse(versionStr); err != nil { +func newStaticPluginVersion(name, version string) *PluginVersion { + vers, err := semver.Parse(version) + if err != nil { return nil - } else { - return &v } + pl := &PluginPackage{ + Name: name, + } + pv := &PluginVersion{ + pack: pl, + Version: vers, + } + pl.Versions = PluginVersions{pv} + return pv } -func (pv *PluginVersion) Install() { +// GetInstalledVersions returns a list of all currently installed plugins including an entry for +// micro itself. This can be used to resolve dependencies. +func GetInstalledVersions() PluginVersions { + result := PluginVersions{ + newStaticPluginVersion("micro", Version), + } + + for _, name := range loadedPlugins { + version := GetInstalledPluginVersion(name) + if pv := newStaticPluginVersion(name, version); pv != nil { + result = append(result, pv) + } + } + + return result +} + +// GetInstalledPluginVersion returns the string of the exported VERSION variable of a loaded plugin +func GetInstalledPluginVersion(name string) string { + plugin := L.GetGlobal(name) + if plugin != lua.LNil { + version := L.GetField(plugin, "VERSION") + if str, ok := version.(lua.LString); ok { + return string(str) + + } + } + return "" +} + +func (pv *PluginVersion) DownloadAndInstall() error { resp, err := http.Get(pv.Url) - if err == nil { - defer resp.Body.Close() - data, _ := ioutil.ReadAll(resp.Body) - zipbuf := bytes.NewReader(data) - z, err := zip.NewReader(zipbuf, zipbuf.Size()) - if err == nil { - targetDir := filepath.Join(configDir, "plugins", pv.pack.Name) - dirPerm := os.FileMode(0755) - if err = os.MkdirAll(targetDir, dirPerm); err == nil { - for _, f := range z.File { - targetName := filepath.Join(targetDir, filepath.Join(strings.Split(f.Name, "/")...)) - if f.FileInfo().IsDir() { - err = os.MkdirAll(targetName, dirPerm) - } else { - content, err := f.Open() - if err == nil { - defer content.Close() - if target, err := os.Create(targetName); err == nil { - defer target.Close() - _, err = io.Copy(target, content) - } - } - } - if err != nil { - break - } + if err != nil { + return err + } + defer resp.Body.Close() + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + zipbuf := bytes.NewReader(data) + z, err := zip.NewReader(zipbuf, zipbuf.Size()) + if err != nil { + return err + } + targetDir := filepath.Join(configDir, "plugins", pv.pack.Name) + dirPerm := os.FileMode(0755) + if err = os.MkdirAll(targetDir, dirPerm); err != nil { + return err + } + for _, f := range z.File { + targetName := filepath.Join(targetDir, filepath.Join(strings.Split(f.Name, "/")...)) + if f.FileInfo().IsDir() { + if err := os.MkdirAll(targetName, dirPerm); err != nil { + return err + } + } else { + content, err := f.Open() + if err != nil { + return err + } + defer content.Close() + if target, err := os.Create(targetName); err != nil { + return err + } else { + defer target.Close() + if _, err = io.Copy(target, content); err != nil { + return err } } } } - if err != nil { - TermMessage("Failed to install plugin:", err) - } + return nil } -func UninstallPlugin(name string) { - os.RemoveAll(filepath.Join(configDir, name)) -} - -// Updates... - func (pl PluginPackages) Get(name string) *PluginPackage { for _, p := range pl { if p.Name == name { @@ -313,15 +385,15 @@ func (req PluginDependencies) Join(other PluginDependencies) PluginDependencies return result } -func (all PluginPackages) ResolveStep(selectedVersions PluginVersions, open PluginDependencies) (PluginVersions, error) { +func (all PluginPackages) Resolve(selectedVersions PluginVersions, open PluginDependencies) (PluginVersions, error) { if len(open) == 0 { return selectedVersions, nil } currentRequirement, stillOpen := open[0], open[1:] if currentRequirement != nil { - if selVersion := selectedVersions.Find(currentRequirement.Name); selVersion != nil { + if selVersion := selectedVersions.find(currentRequirement.Name); selVersion != nil { if currentRequirement.Range(selVersion.Version) { - return all.ResolveStep(selectedVersions, stillOpen) + return all.Resolve(selectedVersions, stillOpen) } return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name) } else { @@ -330,7 +402,7 @@ func (all PluginPackages) ResolveStep(selectedVersions PluginVersions, open Plug for _, version := range availableVersions { if currentRequirement.Range(version.Version) { - resolved, err := all.ResolveStep(append(selectedVersions, version), stillOpen.Join(version.Require)) + resolved, err := all.Resolve(append(selectedVersions, version), stillOpen.Join(version.Require)) if err == nil { return resolved, nil @@ -343,3 +415,68 @@ func (all PluginPackages) ResolveStep(selectedVersions PluginVersions, open Plug return selectedVersions, nil } } + +func (versions PluginVersions) install() { + anyInstalled := false + for _, sel := range versions { + if sel.pack.Name != "micro" { + installed := GetInstalledPluginVersion(sel.pack.Name) + if v, err := semver.Parse(installed); err != nil || v.NE(sel.Version) { + UninstallPlugin(sel.pack.Name) + } + if err := sel.DownloadAndInstall(); err != nil { + messenger.Error(err) + return + } + anyInstalled = true + } + } + if anyInstalled { + messenger.Message("One or more plugins installed. Please restart micro.") + } +} + +// UninstallPlugin deletes the plugin folder of the given plugin +func UninstallPlugin(name string) { + if err := os.RemoveAll(filepath.Join(configDir, "plugins", name)); err != nil { + messenger.Error(err) + } +} + +func (pl PluginPackage) Install() { + selected, err := GetAllPluginPackages().Resolve(GetInstalledVersions(), PluginDependencies{ + &PluginDependency{ + Name: pl.Name, + Range: semver.Range(func(v semver.Version) bool { return true }), + }}) + if err != nil { + TermMessage(err) + return + } + selected.install() +} + +func UpdatePlugins() { + microVersion := PluginVersions{ + newStaticPluginVersion("micro", Version), + } + + var updates = make(PluginDependencies, 0) + for _, name := range loadedPlugins { + pv := GetInstalledPluginVersion(name) + r, err := semver.ParseRange(">=" + pv) // Try to get newer versions. + if err == nil { + updates = append(updates, &PluginDependency{ + Name: name, + Range: r, + }) + } + } + + selected, err := GetAllPluginPackages().Resolve(microVersion, updates) + if err != nil { + TermMessage(err) + return + } + selected.install() +} From 56b3b79c50870805be69a7a003f3de59e47a0e54 Mon Sep 17 00:00:00 2001 From: boombuler Date: Mon, 26 Sep 2016 17:37:53 +0200 Subject: [PATCH 03/18] removed testing code --- cmd/micro/micro.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/micro/micro.go b/cmd/micro/micro.go index a3a75b23..2d2829c8 100644 --- a/cmd/micro/micro.go +++ b/cmd/micro/micro.go @@ -44,7 +44,7 @@ var ( // Version is the version number or commit hash // These variables should be set by the linker when compiling - Version = "2.0.0" + Version = "Unknown" CommitHash = "Unknown" CompileDate = "Unknown" From f6891436709b505ef2fc0a6b65bab8df115a2051 Mon Sep 17 00:00:00 2001 From: boombuler Date: Mon, 26 Sep 2016 17:51:50 +0200 Subject: [PATCH 04/18] fixed tests --- cmd/micro/pluginmanager_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/micro/pluginmanager_test.go b/cmd/micro/pluginmanager_test.go index 47960391..48a9201e 100644 --- a/cmd/micro/pluginmanager_test.go +++ b/cmd/micro/pluginmanager_test.go @@ -24,12 +24,12 @@ func TestDependencyResolving(t *testing.T) { if err != nil { t.Error(err) } - selected, err := all.ResolveStep(PluginVersions{}, PluginDependencies{ + selected, err := all.Resolve(PluginVersions{}, PluginDependencies{ &PluginDependency{"Bar", semver.MustParseRange(">=1.0.0")}, }) check := func(name, version string) { - v := selected.Find(name) + v := selected.find(name) expected := semver.MustParse(version) if v == nil { t.Errorf("Failed to resolve %s", name) @@ -45,7 +45,7 @@ func TestDependencyResolving(t *testing.T) { check("Bar", "1.0.0") } - selected, err = all.ResolveStep(PluginVersions{}, PluginDependencies{ + selected, err = all.Resolve(PluginVersions{}, PluginDependencies{ &PluginDependency{"Unresolvable", semver.MustParseRange(">0.0.0")}, }) if err == nil { From 4bcb13efc0db43de978b8c1e3b5a2349ccb32e0b Mon Sep 17 00:00:00 2001 From: Florian Sundermann Date: Tue, 27 Sep 2016 13:25:17 +0200 Subject: [PATCH 05/18] try to set a more matching version number --- Makefile | 2 +- tools/build-version.go | 65 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 tools/build-version.go diff --git a/Makefile b/Makefile index 2480d529..38956d27 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: runtime -VERSION = $(shell git describe --tags --abbrev=0) +VERSION = $(shell go run tools/build-version.go) HASH = $(shell git rev-parse --short HEAD) DATE = $(shell go run tools/build-date.go) diff --git a/tools/build-version.go b/tools/build-version.go new file mode 100644 index 00000000..a8e68d01 --- /dev/null +++ b/tools/build-version.go @@ -0,0 +1,65 @@ +package main + +import ( + "fmt" + "os/exec" + "strings" + + "github.com/blang/semver" +) + +func getTag(match ...string) (string, *semver.PRVersion) { + args := append([]string{ + "describe", "--tags", + }, match...) + if tag, err := exec.Command("git", args...).Output(); err != nil { + return "", nil + } else { + tagParts := strings.Split(string(tag), "-") + if len(tagParts) == 3 { + if ahead, err := semver.NewPRVersion(tagParts[1]); err == nil { + return tagParts[0], &ahead + } + } + + return tagParts[0], nil + } +} + +func main() { + // Find the last vX.X.X Tag and get how many builds we are ahead of it. + versionStr, ahead := getTag("--match", "v*") + version, err := semver.ParseTolerant(versionStr) + if err != nil { + // no version tag found so just return what ever we can find. + fmt.Println(getTag()) + return + } + // Get the tag of the current revision. + tag, _ := getTag("--exact-match") + if tag == versionStr { + // Seems that we are going to build a release. + // So the version number should already be correct. + fmt.Println(version.String()) + return + } + + // If we don't have any tag assume "dev" + if tag == "" { + tag = "dev" + } + // Get the most likely next version: + version.Patch = version.Patch + 1 + + if pr, err := semver.NewPRVersion(tag); err == nil { + // append the tag as pre-release name + version.Pre = append(version.Pre, pr) + } + + if ahead != nil { + // if we know how many commits we are ahead of the last release, append that too. + version.Pre = append(version.Pre, *ahead) + } + + fmt.Println(version.String()) +} From 55c790f069ce39ad9cbcf29c519526b086ec3336 Mon Sep 17 00:00:00 2001 From: Florian Sundermann Date: Tue, 27 Sep 2016 13:26:11 +0200 Subject: [PATCH 06/18] more tolerant version parsing --- cmd/micro/pluginmanager.go | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/cmd/micro/pluginmanager.go b/cmd/micro/pluginmanager.go index 43296473..6e97a0ed 100644 --- a/cmd/micro/pluginmanager.go +++ b/cmd/micro/pluginmanager.go @@ -27,6 +27,9 @@ var ( allPluginPackages PluginPackages = nil ) +// CorePluginName is a plugin dependency name for the micro core. +const CorePluginName = "micro" + // PluginChannel contains an url to a json list of PluginRepository type PluginChannel string @@ -252,9 +255,12 @@ func SearchPlugin(text string) (plugins PluginPackages) { } func newStaticPluginVersion(name, version string) *PluginVersion { - vers, err := semver.Parse(version) + vers, err := semver.ParseTolerant(version) + if err != nil { - return nil + if vers, err = semver.ParseTolerant("0.0.0-" + version); err != nil { + vers = semver.MustParse("0.0.0-unknown") + } } pl := &PluginPackage{ Name: name, @@ -271,7 +277,7 @@ func newStaticPluginVersion(name, version string) *PluginVersion { // micro itself. This can be used to resolve dependencies. func GetInstalledVersions() PluginVersions { result := PluginVersions{ - newStaticPluginVersion("micro", Version), + newStaticPluginVersion(CorePluginName, Version), } for _, name := range loadedPlugins { @@ -419,9 +425,9 @@ func (all PluginPackages) Resolve(selectedVersions PluginVersions, open PluginDe func (versions PluginVersions) install() { anyInstalled := false for _, sel := range versions { - if sel.pack.Name != "micro" { + if sel.pack.Name != CorePluginName { installed := GetInstalledPluginVersion(sel.pack.Name) - if v, err := semver.Parse(installed); err != nil || v.NE(sel.Version) { + if v, err := semver.ParseTolerant(installed); err != nil || v.NE(sel.Version) { UninstallPlugin(sel.pack.Name) } if err := sel.DownloadAndInstall(); err != nil { @@ -458,7 +464,7 @@ func (pl PluginPackage) Install() { func UpdatePlugins() { microVersion := PluginVersions{ - newStaticPluginVersion("micro", Version), + newStaticPluginVersion(CorePluginName, Version), } var updates = make(PluginDependencies, 0) From 23ef69b9352d4880a2a184cb6c6d06dc0c0f3b4e Mon Sep 17 00:00:00 2001 From: Florian Sundermann Date: Tue, 27 Sep 2016 13:28:32 +0200 Subject: [PATCH 07/18] change pluginmanager json to json5 --- cmd/micro/pluginmanager.go | 10 +++++----- cmd/micro/pluginmanager_test.go | 5 +++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/cmd/micro/pluginmanager.go b/cmd/micro/pluginmanager.go index 6e97a0ed..a2fc98eb 100644 --- a/cmd/micro/pluginmanager.go +++ b/cmd/micro/pluginmanager.go @@ -3,7 +3,6 @@ package main import ( "archive/zip" "bytes" - "encoding/json" "fmt" "io" "io/ioutil" @@ -16,6 +15,7 @@ import ( "sync" "github.com/blang/semver" + "github.com/yosuke-furukawa/json5/encoding/json5" "github.com/yuin/gopher-lua" ) @@ -114,7 +114,7 @@ func (pc PluginChannel) Fetch() PluginPackages { return PluginPackages{} } defer resp.Body.Close() - decoder := json.NewDecoder(resp.Body) + decoder := json5.NewDecoder(resp.Body) var repositories []PluginRepository if err := decoder.Decode(&repositories); err != nil { @@ -134,7 +134,7 @@ func (pr PluginRepository) Fetch() PluginPackages { return PluginPackages{} } defer resp.Body.Close() - decoder := json.NewDecoder(resp.Body) + decoder := json5.NewDecoder(resp.Body) var plugins PluginPackages if err := decoder.Decode(&plugins); err != nil { @@ -152,7 +152,7 @@ func (pv *PluginVersion) UnmarshalJSON(data []byte) error { Require map[string]string } - if err := json.Unmarshal(data, &values); err != nil { + if err := json5.Unmarshal(data, &values); err != nil { return err } pv.Version = values.Version @@ -176,7 +176,7 @@ func (pp *PluginPackage) UnmarshalJSON(data []byte) error { Tags []string Versions PluginVersions } - if err := json.Unmarshal(data, &values); err != nil { + if err := json5.Unmarshal(data, &values); err != nil { return err } pp.Name = values.Name diff --git a/cmd/micro/pluginmanager_test.go b/cmd/micro/pluginmanager_test.go index 48a9201e..0cfb76f5 100644 --- a/cmd/micro/pluginmanager_test.go +++ b/cmd/micro/pluginmanager_test.go @@ -1,9 +1,10 @@ package main import ( - "encoding/json" "github.com/blang/semver" "testing" + + "github.com/yosuke-furukawa/json5/encoding/json5" ) func TestDependencyResolving(t *testing.T) { @@ -20,7 +21,7 @@ func TestDependencyResolving(t *testing.T) { }] ` var all PluginPackages - err := json.Unmarshal([]byte(js), &all) + err := json5.Unmarshal([]byte(js), &all) if err != nil { t.Error(err) } From f3f4790103572fce975e6889d0124eacc7f8148f Mon Sep 17 00:00:00 2001 From: boombuler Date: Tue, 27 Sep 2016 21:25:57 +0200 Subject: [PATCH 08/18] simple plugin search --- cmd/micro/command.go | 13 +++++++++++++ cmd/micro/pluginmanager.go | 16 ++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/cmd/micro/command.go b/cmd/micro/command.go index e43b3813..588586bf 100644 --- a/cmd/micro/command.go +++ b/cmd/micro/command.go @@ -118,6 +118,19 @@ func PluginCmd(args []string) { } case "update": UpdatePlugins() + case "search": + searchText := strings.Join(args[1:], " ") + plugins := SearchPlugin(searchText) + messenger.Message(len(plugins), " plugins found") + for _, p := range plugins { + messenger.AddLog("\n") + messenger.AddLog(p.String()) + } + if len(plugins) > 0 { + if CurView().Type != vtLog { + ToggleLog([]string{}) + } + } } } else { messenger.Error("Not enough arguments") diff --git a/cmd/micro/pluginmanager.go b/cmd/micro/pluginmanager.go index a2fc98eb..1e00f6db 100644 --- a/cmd/micro/pluginmanager.go +++ b/cmd/micro/pluginmanager.go @@ -71,6 +71,22 @@ type PluginDependency struct { // PluginDependencies is a slice of PluginDependency type PluginDependencies []*PluginDependency +func (pp *PluginPackage) String() string { + buf := new(bytes.Buffer) + buf.WriteString("Plugin: ") + buf.WriteString(pp.Name) + buf.WriteRune('\n') + if pp.Author != "" { + buf.WriteString("Author: ") + buf.WriteString(pp.Author) + buf.WriteRune('\n') + } + if pp.Description != "" { + buf.WriteString(pp.Description) + } + return buf.String() +} + func fetchAllSources(count int, fetcher func(i int) PluginPackages) PluginPackages { wgQuery := new(sync.WaitGroup) wgQuery.Add(count) From cce36624dc5cb6d4640fb08d4ebbce8a11188c31 Mon Sep 17 00:00:00 2001 From: Florian Sundermann Date: Wed, 28 Sep 2016 16:34:28 +0200 Subject: [PATCH 09/18] PM should not install already installed plugins. --- cmd/micro/pluginmanager.go | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/cmd/micro/pluginmanager.go b/cmd/micro/pluginmanager.go index 1e00f6db..969c29c7 100644 --- a/cmd/micro/pluginmanager.go +++ b/cmd/micro/pluginmanager.go @@ -320,6 +320,7 @@ func GetInstalledPluginVersion(name string) string { } func (pv *PluginVersion) DownloadAndInstall() error { + messenger.AddLog(fmt.Sprintf("Downloading %q (%s) from %q", pv.pack.Name, pv.Version, pv.Url)) resp, err := http.Get(pv.Url) if err != nil { return err @@ -440,17 +441,27 @@ func (all PluginPackages) Resolve(selectedVersions PluginVersions, open PluginDe func (versions PluginVersions) install() { anyInstalled := false + currentlyInstalled := GetInstalledVersions() + for _, sel := range versions { if sel.pack.Name != CorePluginName { - installed := GetInstalledPluginVersion(sel.pack.Name) - if v, err := semver.ParseTolerant(installed); err != nil || v.NE(sel.Version) { - UninstallPlugin(sel.pack.Name) + shouldInstall := true + if pv := currentlyInstalled.find(sel.pack.Name); pv != nil { + if pv.Version.NE(sel.Version) { + messenger.AddLog(fmt.Sprint("Uninstalling %q", sel.pack.Name)) + UninstallPlugin(sel.pack.Name) + } else { + shouldInstall = false + } } - if err := sel.DownloadAndInstall(); err != nil { - messenger.Error(err) - return + + if shouldInstall { + if err := sel.DownloadAndInstall(); err != nil { + messenger.Error(err) + return + } + anyInstalled = true } - anyInstalled = true } } if anyInstalled { From 2a7a55eca4b5abc90a9f3edb37b5548fdbeb229c Mon Sep 17 00:00:00 2001 From: boombuler Date: Wed, 28 Sep 2016 17:55:44 +0200 Subject: [PATCH 10/18] better plugin search --- cmd/micro/pluginmanager.go | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/cmd/micro/pluginmanager.go b/cmd/micro/pluginmanager.go index 969c29c7..27cd802e 100644 --- a/cmd/micro/pluginmanager.go +++ b/cmd/micro/pluginmanager.go @@ -9,7 +9,6 @@ import ( "net/http" "os" "path/filepath" - "regexp" "sort" "strings" "sync" @@ -240,11 +239,20 @@ func (s PluginVersions) Less(i, j int) bool { // Match returns true if the package matches a given search text func (pp PluginPackage) Match(text string) bool { - // ToDo: improve matching. - text = "(?i)" + text - if r, err := regexp.Compile(text); err == nil { - return r.MatchString(pp.Name) + text = strings.ToLower(text) + for _, t := range pp.Tags { + if strings.ToLower(t) == text { + return true + } } + if strings.Contains(strings.ToLower(pp.Name), text) { + return true + } + + if strings.Contains(strings.ToLower(pp.Description), text) { + return true + } + return false } From 9ea947c808a1844389527cb1e776da141e66a088 Mon Sep 17 00:00:00 2001 From: boombuler Date: Wed, 28 Sep 2016 18:00:12 +0200 Subject: [PATCH 11/18] improved logging --- cmd/micro/pluginmanager.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cmd/micro/pluginmanager.go b/cmd/micro/pluginmanager.go index 27cd802e..0c4fbef2 100644 --- a/cmd/micro/pluginmanager.go +++ b/cmd/micro/pluginmanager.go @@ -123,6 +123,7 @@ func (pc PluginChannels) Fetch() PluginPackages { // Fetch retrieves all available PluginPackages from the given channel func (pc PluginChannel) Fetch() PluginPackages { + messenger.AddLog(fmt.Sprintf("Fetching channel: %q", string(pc))) resp, err := http.Get(string(pc)) if err != nil { TermMessage("Failed to query plugin channel:\n", err) @@ -143,6 +144,7 @@ func (pc PluginChannel) Fetch() PluginPackages { // Fetch retrieves all available PluginPackages from the given repository func (pr PluginRepository) Fetch() PluginPackages { + messenger.AddLog(fmt.Sprintf("Fetching repository: %q", string(pr))) resp, err := http.Get(string(pr)) if err != nil { TermMessage("Failed to query plugin repository:\n", err) @@ -474,6 +476,8 @@ func (versions PluginVersions) install() { } if anyInstalled { messenger.Message("One or more plugins installed. Please restart micro.") + } else { + messenger.AddLog("Nothing to install / update") } } @@ -498,6 +502,7 @@ func (pl PluginPackage) Install() { } func UpdatePlugins() { + messenger.AddLog("Checking for plugin updates") microVersion := PluginVersions{ newStaticPluginVersion(CorePluginName, Version), } From 8aa017bfdaaa0e25c16be2a37c75e4baf6289540 Mon Sep 17 00:00:00 2001 From: boombuler Date: Wed, 28 Sep 2016 18:15:39 +0200 Subject: [PATCH 12/18] autocomplete plugin commands --- cmd/micro/autocomplete.go | 26 ++++++++++++++++++++++++++ cmd/micro/command.go | 4 ++-- cmd/micro/messenger.go | 6 ++++++ cmd/micro/pluginmanager.go | 9 +++++++-- 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/cmd/micro/autocomplete.go b/cmd/micro/autocomplete.go index d0ee3e17..52055dc2 100644 --- a/cmd/micro/autocomplete.go +++ b/cmd/micro/autocomplete.go @@ -149,3 +149,29 @@ func PluginComplete(complete Completion, input string) (chosen string, suggestio } return } + +func PluginCmdComplete(input string) (chosen string, suggestions []string) { + for _, cmd := range []string{"install", "remove", "search", "update"} { + if strings.HasPrefix(cmd, input) { + suggestions = append(suggestions, cmd) + } + } + + if len(suggestions) == 1 { + chosen = suggestions[0] + } + return chosen, suggestions +} + +func PluginNameComplete(input string) (chosen string, suggestions []string) { + for _, pp := range GetAllPluginPackages() { + if strings.HasPrefix(pp.Name, input) { + suggestions = append(suggestions, pp.Name) + } + } + + if len(suggestions) == 1 { + chosen = suggestions[0] + } + return chosen, suggestions +} diff --git a/cmd/micro/command.go b/cmd/micro/command.go index 588586bf..b978e132 100644 --- a/cmd/micro/command.go +++ b/cmd/micro/command.go @@ -87,7 +87,7 @@ func DefaultCommands() map[string]StrCommand { "help": {"Help", []Completion{HelpCompletion, NoCompletion}}, "eval": {"Eval", []Completion{NoCompletion}}, "log": {"ToggleLog", []Completion{NoCompletion}}, - "plugin": {"Plugin", []Completion{NoCompletion}}, + "plugin": {"Plugin", []Completion{PluginCmdCompletion, PluginNameCompletion}}, } } @@ -117,7 +117,7 @@ func PluginCmd(args []string) { } } case "update": - UpdatePlugins() + UpdatePlugins(args[1:]) case "search": searchText := strings.Join(args[1:], " ") plugins := SearchPlugin(searchText) diff --git a/cmd/micro/messenger.go b/cmd/micro/messenger.go index a9535b35..36e46889 100644 --- a/cmd/micro/messenger.go +++ b/cmd/micro/messenger.go @@ -192,6 +192,8 @@ const ( CommandCompletion HelpCompletion OptionCompletion + PluginCmdCompletion + PluginNameCompletion ) // Prompt sends the user a message and waits for a response to be typed in @@ -255,6 +257,10 @@ func (m *Messenger) Prompt(prompt, historyType string, completionTypes ...Comple chosen, suggestions = HelpComplete(currentArg) } else if completionType == OptionCompletion { chosen, suggestions = OptionComplete(currentArg) + } else if completionType == PluginCmdCompletion { + chosen, suggestions = PluginCmdComplete(currentArg) + } else if completionType == PluginNameCompletion { + chosen, suggestions = PluginNameComplete(currentArg) } else if completionType < NoCompletion { chosen, suggestions = PluginComplete(completionType, currentArg) } diff --git a/cmd/micro/pluginmanager.go b/cmd/micro/pluginmanager.go index 0c4fbef2..32e8aba9 100644 --- a/cmd/micro/pluginmanager.go +++ b/cmd/micro/pluginmanager.go @@ -501,14 +501,19 @@ func (pl PluginPackage) Install() { selected.install() } -func UpdatePlugins() { +func UpdatePlugins(plugins []string) { + // if no plugins are specified, update all installed plugins. + if len(plugins) == 0 { + plugins = loadedPlugins + } + messenger.AddLog("Checking for plugin updates") microVersion := PluginVersions{ newStaticPluginVersion(CorePluginName, Version), } var updates = make(PluginDependencies, 0) - for _, name := range loadedPlugins { + for _, name := range plugins { pv := GetInstalledPluginVersion(name) r, err := semver.ParseRange(">=" + pv) // Try to get newer versions. if err == nil { From 1fe1c3eabb08d6bf93009cd6291911bb385a490f Mon Sep 17 00:00:00 2001 From: boombuler Date: Wed, 28 Sep 2016 18:31:05 +0200 Subject: [PATCH 13/18] improved plugin search --- cmd/micro/command.go | 6 +++--- cmd/micro/pluginmanager.go | 13 +++++++++++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/cmd/micro/command.go b/cmd/micro/command.go index b978e132..22ebafc9 100644 --- a/cmd/micro/command.go +++ b/cmd/micro/command.go @@ -119,13 +119,13 @@ func PluginCmd(args []string) { case "update": UpdatePlugins(args[1:]) case "search": - searchText := strings.Join(args[1:], " ") - plugins := SearchPlugin(searchText) + plugins := SearchPlugin(args[1:]) messenger.Message(len(plugins), " plugins found") for _, p := range plugins { - messenger.AddLog("\n") + messenger.AddLog("----------------") messenger.AddLog(p.String()) } + messenger.AddLog("----------------") if len(plugins) > 0 { if CurView().Type != vtLog { ToggleLog([]string{}) diff --git a/cmd/micro/pluginmanager.go b/cmd/micro/pluginmanager.go index 32e8aba9..bb330682 100644 --- a/cmd/micro/pluginmanager.go +++ b/cmd/micro/pluginmanager.go @@ -81,6 +81,7 @@ func (pp *PluginPackage) String() string { buf.WriteRune('\n') } if pp.Description != "" { + buf.WriteRune('\n') buf.WriteString(pp.Description) } return buf.String() @@ -270,10 +271,18 @@ func (pp PluginPackage) IsInstallable() bool { // SearchPlugin retrieves a list of all PluginPackages which match the given search text and // could be or are already installed -func SearchPlugin(text string) (plugins PluginPackages) { +func SearchPlugin(texts []string) (plugins PluginPackages) { plugins = make(PluginPackages, 0) + +pluginLoop: for _, pp := range GetAllPluginPackages() { - if pp.Match(text) && pp.IsInstallable() { + for _, text := range texts { + if !pp.Match(text) { + continue pluginLoop + } + } + + if pp.IsInstallable() { plugins = append(plugins, pp) } } From b54853140a980c421104135cc89ed8f31eb09e03 Mon Sep 17 00:00:00 2001 From: boombuler Date: Sat, 1 Oct 2016 07:37:20 +0200 Subject: [PATCH 14/18] new command `plugin list` this command shows all currently installed plugins and their verion --- cmd/micro/autocomplete.go | 2 +- cmd/micro/command.go | 14 ++++++++++++++ cmd/micro/pluginmanager.go | 13 +++++++------ 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/cmd/micro/autocomplete.go b/cmd/micro/autocomplete.go index 52055dc2..47aeca73 100644 --- a/cmd/micro/autocomplete.go +++ b/cmd/micro/autocomplete.go @@ -151,7 +151,7 @@ func PluginComplete(complete Completion, input string) (chosen string, suggestio } func PluginCmdComplete(input string) (chosen string, suggestions []string) { - for _, cmd := range []string{"install", "remove", "search", "update"} { + for _, cmd := range []string{"install", "remove", "search", "update", "list"} { if strings.HasPrefix(cmd, input) { suggestions = append(suggestions, cmd) } diff --git a/cmd/micro/command.go b/cmd/micro/command.go index 22ebafc9..4adb6948 100644 --- a/cmd/micro/command.go +++ b/cmd/micro/command.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "fmt" "io" "io/ioutil" "os" @@ -118,6 +119,19 @@ func PluginCmd(args []string) { } case "update": UpdatePlugins(args[1:]) + case "list": + plugins := GetInstalledVersions(false) + messenger.AddLog("----------------") + messenger.AddLog("The following plugins are currently installed:\n") + for _, p := range plugins { + messenger.AddLog(fmt.Sprintf("%s (%s)", p.pack.Name, p.Version)) + } + messenger.AddLog("----------------") + if len(plugins) > 0 { + if CurView().Type != vtLog { + ToggleLog([]string{}) + } + } case "search": plugins := SearchPlugin(args[1:]) messenger.Message(len(plugins), " plugins found") diff --git a/cmd/micro/pluginmanager.go b/cmd/micro/pluginmanager.go index bb330682..837ef659 100644 --- a/cmd/micro/pluginmanager.go +++ b/cmd/micro/pluginmanager.go @@ -261,7 +261,7 @@ func (pp PluginPackage) Match(text string) bool { // IsInstallable returns true if the package can be installed. func (pp PluginPackage) IsInstallable() bool { - _, err := GetAllPluginPackages().Resolve(GetInstalledVersions(), PluginDependencies{ + _, err := GetAllPluginPackages().Resolve(GetInstalledVersions(true), PluginDependencies{ &PluginDependency{ Name: pp.Name, Range: semver.Range(func(v semver.Version) bool { return true }), @@ -310,9 +310,10 @@ func newStaticPluginVersion(name, version string) *PluginVersion { // GetInstalledVersions returns a list of all currently installed plugins including an entry for // micro itself. This can be used to resolve dependencies. -func GetInstalledVersions() PluginVersions { - result := PluginVersions{ - newStaticPluginVersion(CorePluginName, Version), +func GetInstalledVersions(withCore bool) PluginVersions { + result := PluginVersions{} + if withCore { + result = append(result, newStaticPluginVersion(CorePluginName, Version)) } for _, name := range loadedPlugins { @@ -460,7 +461,7 @@ func (all PluginPackages) Resolve(selectedVersions PluginVersions, open PluginDe func (versions PluginVersions) install() { anyInstalled := false - currentlyInstalled := GetInstalledVersions() + currentlyInstalled := GetInstalledVersions(true) for _, sel := range versions { if sel.pack.Name != CorePluginName { @@ -498,7 +499,7 @@ func UninstallPlugin(name string) { } func (pl PluginPackage) Install() { - selected, err := GetAllPluginPackages().Resolve(GetInstalledVersions(), PluginDependencies{ + selected, err := GetAllPluginPackages().Resolve(GetInstalledVersions(true), PluginDependencies{ &PluginDependency{ Name: pl.Name, Range: semver.Range(func(v semver.Version) bool { return true }), From d7da72a72058c63ae1de0a46056b2dc07ceb0549 Mon Sep 17 00:00:00 2001 From: boombuler Date: Sat, 1 Oct 2016 08:05:05 +0200 Subject: [PATCH 15/18] fix plugin zips which contain a root directory --- cmd/micro/pluginmanager.go | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/cmd/micro/pluginmanager.go b/cmd/micro/pluginmanager.go index 837ef659..c9d0b7ac 100644 --- a/cmd/micro/pluginmanager.go +++ b/cmd/micro/pluginmanager.go @@ -360,8 +360,32 @@ func (pv *PluginVersion) DownloadAndInstall() error { if err = os.MkdirAll(targetDir, dirPerm); err != nil { return err } + + // Check if all files in zip are in the same directory. + // this might be the case if the plugin zip contains the whole plugin dir + // instead of its content. + var prefix string + allPrefixed := false + for i, f := range z.File { + parts := strings.Split(f.Name, "/") + if i == 0 { + prefix = parts[0] + } else if parts[0] != prefix { + allPrefixed = false + break + } else { + // switch to true since we have at least a second file + allPrefixed = true + } + } + for _, f := range z.File { - targetName := filepath.Join(targetDir, filepath.Join(strings.Split(f.Name, "/")...)) + parts := strings.Split(f.Name, "/") + if allPrefixed { + parts = parts[1:] + } + + targetName := filepath.Join(targetDir, filepath.Join(parts...)) if f.FileInfo().IsDir() { if err := os.MkdirAll(targetName, dirPerm); err != nil { return err From a940ce3036db74d3ba320c9f99be79883af6120d Mon Sep 17 00:00:00 2001 From: boombuler Date: Sat, 1 Oct 2016 08:37:04 +0200 Subject: [PATCH 16/18] allow user to set plugin channels / repos in settings.json --- cmd/micro/pluginmanager.go | 39 +++++++++++++++++++++++++++++++++----- cmd/micro/settings.go | 4 ++++ 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/cmd/micro/pluginmanager.go b/cmd/micro/pluginmanager.go index c9d0b7ac..37a3a17d 100644 --- a/cmd/micro/pluginmanager.go +++ b/cmd/micro/pluginmanager.go @@ -19,10 +19,6 @@ import ( ) var ( - pluginChannels PluginChannels = PluginChannels{ - PluginChannel("https://www.boombuler.de/channel.json"), - } - allPluginPackages PluginPackages = nil ) @@ -211,7 +207,40 @@ func (pp *PluginPackage) UnmarshalJSON(data []byte) error { // GetAllPluginPackages gets all PluginPackages which may be available. func GetAllPluginPackages() PluginPackages { if allPluginPackages == nil { - allPluginPackages = pluginChannels.Fetch() + getOption := func(name string) []string { + data := GetOption(name) + if strs, ok := data.([]string); ok { + return strs + } + if ifs, ok := data.([]interface{}); ok { + result := make([]string, len(ifs)) + for i, urlIf := range ifs { + if url, ok := urlIf.(string); ok { + result[i] = url + } else { + return nil + } + } + return result + } + return nil + } + + channels := PluginChannels{} + for _, url := range getOption("pluginchannels") { + channels = append(channels, PluginChannel(url)) + } + repos := []PluginRepository{} + for _, url := range getOption("pluginrepos") { + repos = append(repos, PluginRepository(url)) + } + allPluginPackages = fetchAllSources(len(repos)+1, func(i int) PluginPackages { + if i == 0 { + return channels.Fetch() + } else { + return repos[i-1].Fetch() + } + }) } return allPluginPackages } diff --git a/cmd/micro/settings.go b/cmd/micro/settings.go index 41d26449..de31c951 100644 --- a/cmd/micro/settings.go +++ b/cmd/micro/settings.go @@ -192,6 +192,10 @@ func DefaultGlobalSettings() map[string]interface{} { "syntax": true, "tabsize": float64(4), "tabstospaces": false, + "pluginchannels": []string{ + "https://www.boombuler.de/channel.json", + }, + "pluginrepos": []string{}, } } From 8f2f1f8c1d5e408c7ac17348cd40d780d180e4aa Mon Sep 17 00:00:00 2001 From: boombuler Date: Sat, 1 Oct 2016 09:28:48 +0200 Subject: [PATCH 17/18] skip core dependencies if micro was build with an unknown version. --- cmd/micro/pluginmanager.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/cmd/micro/pluginmanager.go b/cmd/micro/pluginmanager.go index 37a3a17d..bf51552d 100644 --- a/cmd/micro/pluginmanager.go +++ b/cmd/micro/pluginmanager.go @@ -174,8 +174,13 @@ func (pv *PluginVersion) UnmarshalJSON(data []byte) error { pv.Require = make(PluginDependencies, 0) for k, v := range values.Require { - if vRange, err := semver.ParseRange(v); err == nil { - pv.Require = append(pv.Require, &PluginDependency{k, vRange}) + // don't add the dependency if it's the core and + // we have a unknown version number. + // in that case just accept that dependency (which equals to not adding it.) + if k != CorePluginName || !isUnknownCoreVersion() { + if vRange, err := semver.ParseRange(v); err == nil { + pv.Require = append(pv.Require, &PluginDependency{k, vRange}) + } } } return nil @@ -318,6 +323,11 @@ pluginLoop: return } +func isUnknownCoreVersion() bool { + _, err := semver.ParseTolerant(Version) + return err != nil +} + func newStaticPluginVersion(name, version string) *PluginVersion { vers, err := semver.ParseTolerant(version) From 3e8a587aa3bf395e97a754e7e58e5d8a39bcbf39 Mon Sep 17 00:00:00 2001 From: boombuler Date: Sun, 2 Oct 2016 07:57:39 +0200 Subject: [PATCH 18/18] changed json5 repo --- cmd/micro/pluginmanager.go | 2 +- cmd/micro/pluginmanager_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/micro/pluginmanager.go b/cmd/micro/pluginmanager.go index bf51552d..3f0b5a6f 100644 --- a/cmd/micro/pluginmanager.go +++ b/cmd/micro/pluginmanager.go @@ -14,8 +14,8 @@ import ( "sync" "github.com/blang/semver" - "github.com/yosuke-furukawa/json5/encoding/json5" "github.com/yuin/gopher-lua" + "github.com/zyedidia/json5/encoding/json5" ) var ( diff --git a/cmd/micro/pluginmanager_test.go b/cmd/micro/pluginmanager_test.go index 0cfb76f5..25e2203f 100644 --- a/cmd/micro/pluginmanager_test.go +++ b/cmd/micro/pluginmanager_test.go @@ -4,7 +4,7 @@ import ( "github.com/blang/semver" "testing" - "github.com/yosuke-furukawa/json5/encoding/json5" + "github.com/zyedidia/json5/encoding/json5" ) func TestDependencyResolving(t *testing.T) {