diff --git a/README.md b/README.md index a8c65b1..862241c 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Currently it implements the watchers for the following services: - Amazon S3 - Google Drive - Dropbox +- Git - local filesystem (fsnotify/polling) It is possible specify the directory to monitor and the polling time (how much often the watcher should check that directory), @@ -124,4 +125,26 @@ It is not mandatory to call the `SetConfig()` function, and the polling time arg Setting `disable_fsnotify` parameter on config to "true" the watcher doesn't use fsnotify and use the listing approach instead. -> :warning: not set `disable_fsnotify` to "true" if you plan to use it on a big directory!!! It could increase the I/O on disk \ No newline at end of file +> :warning: not set `disable_fsnotify` to "true" if you plan to use it on a big directory!!! It could increase the I/O on disk + +## Git + +Git watcher has the following configurations: + +| Name | Description +| --- | --- | +| `debug` | if "true" the debug mode is enabled (default "false") | +| `monitor_type` | it can be "file" or "repo" (default is "repo") | +| `auth_type` | authentication type to use: "none", "ssh", "http_token", "http_user_pass" (default "none") | +| `ssh_pkey` | path of the ssh private key (required if auth_type = "ssh") | +| `ssh_pkey_password` | password of the private key if set | +| `http_token` | token to use if auth_type = "http_token" | +| `http_username` | username of github account (auth_type = "http_user_pass") | +| `http_password` | password of github account (auth_type = "http_user_pass") | +| `repo_url` | url of the repository | +| `repo_branch` | branch to watch (if `monitor_type` is "repo" you can leave it empty to watch all the branches) | +| `assemble_events` | if "true" the events could contain one or more commit events (only if `monitor_type` = "repo") | +| `temp_dir` | temporary directory to use for clone the repo: if empty the tmp dir will be used | + +If `monitor_type` is set to "repo", the event channel will receive an event with the `Object` field filled with commits or tags. +If `assemble_events` is "true" the `Object` field could contains one or more commits. \ No newline at end of file diff --git a/examples/git/git_file.go b/examples/git/git_file.go new file mode 100644 index 0000000..51a7ac4 --- /dev/null +++ b/examples/git/git_file.go @@ -0,0 +1,40 @@ +package main + +import ( + "fmt" + "github.com/Matrix86/cloudwatcher" + "time" +) + +func main() { + s, err := cloudwatcher.New("git", "", 2*time.Second) + if err != nil { + fmt.Printf("ERROR: %s", err) + return + } + + config := map[string]string{ + "debug": "true", + "monitor_type": "file", + "repo_url": "git@github.com:Matrix86/cloudwatcher.git", + "repo_branch": "main", + } + + err = s.SetConfig(config) + if err != nil { + fmt.Printf("ERROR: %s", err) + return + } + + err = s.Start() + defer s.Close() + for { + select { + case v := <-s.GetEvents(): + fmt.Printf("EVENT: %s %s\n", v.Key, v.TypeString()) + + case e := <-s.GetErrors(): + fmt.Printf("ERROR: %s\n", e) + } + } +} diff --git a/examples/git/git_repo.go b/examples/git/git_repo.go new file mode 100644 index 0000000..8fd5f3f --- /dev/null +++ b/examples/git/git_repo.go @@ -0,0 +1,50 @@ +package main + +import ( + "fmt" + "github.com/Matrix86/cloudwatcher" + "time" +) + +func main() { + s, err := cloudwatcher.New("git", "", 2*time.Second) + if err != nil { + fmt.Printf("ERROR: %s", err) + return + } + + config := map[string]string{ + "debug": "true", + "monitor_type": "repo", + "repo_url": "git@github.com:Matrix86/cloudwatcher.git", + "assemble_events": "true", + } + + err = s.SetConfig(config) + if err != nil { + fmt.Printf("ERROR: %s", err) + return + } + + err = s.Start() + defer s.Close() + for { + select { + case v := <-s.GetEvents(): + if v.Key == "commit" { + fmt.Println("New commits:") + for _, c := range v.Object.(*cloudwatcher.GitObject).Commits { + fmt.Printf("- hash=%s branch=%s : %s\n", c.Hash, c.Branch, c.Message) + } + } else if v.Key == "tag" { + fmt.Println("New tags:") + for _, c := range v.Object.(*cloudwatcher.GitObject).Commits { + fmt.Printf("- %s\n", c.Message) + } + } + + case e := <-s.GetErrors(): + fmt.Printf("ERROR: %s\n", e) + } + } +} diff --git a/git.go b/git.go new file mode 100644 index 0000000..cfc3409 --- /dev/null +++ b/git.go @@ -0,0 +1,519 @@ +package cloudwatcher + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "os" + "strings" + "sync/atomic" + "time" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/go-git/go-git/v5/plumbing/transport/http" + "github.com/go-git/go-git/v5/plumbing/transport/ssh" +) + +var errExitFromLoop = errors.New("exit") + +// GitWatcher is the specialized watcher for Git service +type GitWatcher struct { + WatcherBase + + syncing uint32 + + repository *git.Repository + + ticker *time.Ticker + stop chan bool + config *gitConfiguration + fileCache map[string]*GitObject + branchCache map[string]string // Branch name -> last commit hash + tagCache map[string]string +} + +// GitCommit is the object that contains the info about the commit +type GitCommit struct { + Hash string + Message string + Branch string +} + +// GitObject is the object that contains the info of the file +type GitObject struct { + Key string + Size int64 + FileMode os.FileMode + Hash string + Commits []*GitCommit +} + +type gitConfiguration struct { + Debug Bool `json:"debug"` + MonitorType string `json:"monitor_type"` // file, repo + AuthType string `json:"auth_type"` // ssh, http_token, http_user_pass + SSHPrivateKey string `json:"ssh_pkey"` + SSHPKeyPassword string `json:"ssh_pkey_password"` + HTTPToken string `json:"http_token"` + HTTPUsername string `json:"http_username"` + HTTPPassword string `json:"http_password"` + RepoURL string `json:"repo_url"` + RepoBranch string `json:"repo_branch"` + AssembleEvents Bool `json:"assemble_events"` + TempDir string `json:"temp_dir"` +} + +func newGitWatcher(dir string, interval time.Duration) (Watcher, error) { + return &GitWatcher{ + tagCache: make(map[string]string), + fileCache: make(map[string]*GitObject), + branchCache: make(map[string]string), + stop: make(chan bool, 1), + WatcherBase: WatcherBase{ + Events: make(chan Event, 100), + Errors: make(chan error, 100), + watchDir: dir, + pollingTime: interval, + }, + }, nil +} + +// SetConfig is used to configure the GitWatcher +func (w *GitWatcher) SetConfig(m map[string]string) error { + j, err := json.Marshal(m) + if err != nil { + return err + } + + config := gitConfiguration{} + if err := json.Unmarshal(j, &config); err != nil { + return err + } + + if config.MonitorType == "" { + config.MonitorType = "repo" // setting default behaviour + } else if !inArray(config.MonitorType, []string{"repo", "file"}) { + return fmt.Errorf("unknown monitor_type '%s'", config.MonitorType) + } + + if config.AuthType == "ssh" { + _, err := os.Stat(config.SSHPrivateKey) + if err != nil { + return fmt.Errorf("cannot read file '%s': %s", config.SSHPrivateKey, err) + } + } + + if !inArray(config.AuthType, []string{"", "none", "ssh", "http_token", "http_user_pass"}) { + return fmt.Errorf("unknown auth_type '%s'", config.AuthType) + } + + if config.RepoURL == "" { + return fmt.Errorf("url repository required") + } + + if config.AuthType == "file" && config.RepoBranch == "" { + return fmt.Errorf("branch repository required") + } + + if config.TempDir == "" { + dir, err := ioutil.TempDir("", "tmp_git") + if err != nil { + return fmt.Errorf("creating temp dir: %s", err) + } + config.TempDir = dir + } + + w.config = &config + return nil +} + +// Start launches the polling process +func (w *GitWatcher) Start() error { + if w.config == nil { + return fmt.Errorf("configuration for Git needed") + } + + w.ticker = time.NewTicker(w.pollingTime) + go func() { + // launch synchronization also the first time + w.sync() + for { + select { + case <-w.ticker.C: + w.sync() + + case <-w.stop: + close(w.Events) + close(w.Errors) + return + } + } + }() + return nil +} + +// Close stop the polling process +func (w *GitWatcher) Close() { + if w.stop != nil { + w.stop <- true + } +} + +func (w *GitWatcher) getCachedObject(o *GitObject) *GitObject { + if cachedObject, ok := w.fileCache[o.Key]; ok { + return cachedObject + } + return nil +} + +func (w *GitWatcher) sync() { + // allow only one sync at same time + if !atomic.CompareAndSwapUint32(&w.syncing, 0, 1) { + return + } + defer atomic.StoreUint32(&w.syncing, 0) + + err := w.updateRepo() + if err != nil { + w.Errors <- err + return + } + + // default behaviour is file + if w.config.MonitorType == "repo" { + // if we don't have commits in the cache this is the first time it is being executed + firstSync := len(w.branchCache) == 0 + w.checkCommits(firstSync) + w.checkTags(firstSync) + } else { + fileList := make(map[string]*GitObject, 0) + err := w.enumerateFiles(w.watchDir, func(obj *GitObject) bool { + // Store the files to check the deleted one + fileList[obj.Key] = obj + // Check if the object is cached by Key + cached := w.getCachedObject(obj) + // Object has been cached previously by Key + if cached != nil { + // Check if the Hash or the FileMode have been changed + if cached.Hash != obj.Hash { + event := Event{ + Key: obj.Key, + Type: FileChanged, + Object: obj, + } + w.Events <- event + } else if cached.FileMode != obj.FileMode { + event := Event{ + Key: obj.Key, + Type: TagsChanged, + Object: obj, + } + w.Events <- event + } + } else { + event := Event{ + Key: obj.Key, + Type: FileCreated, + Object: obj, + } + w.Events <- event + } + w.fileCache[obj.Key] = obj + return true + }) + if err != nil { + w.Errors <- err + return + } + + for k, o := range w.fileCache { + if _, found := fileList[k]; !found { + // file not found in the list...deleting it + delete(w.fileCache, k) + event := Event{ + Key: o.Key, + Type: FileDeleted, + Object: o, + } + w.Events <- event + } + } + } +} + +func (w *GitWatcher) checkCommits(disableNotification bool) { + branches := make([]string, 0) + // if RepoBranch is empty we are collecting all the branches + if w.config.RepoBranch == "" { + rIter, err := w.repository.Branches() + if err != nil { + w.Errors <- fmt.Errorf("retrieving branches: %s", err) + return + } + err = rIter.ForEach(func(ref *plumbing.Reference) error { + branches = append(branches, ref.Name().Short()) + return nil + }) + } else { + branches = append(branches, w.config.RepoBranch) + } + + for _, branch := range branches { + err := w.moveToBranch(branch) + if err != nil { + w.Errors <- err + continue + } + + // retrieving commits for the current branch + cIter, err := w.repository.Log(&git.LogOptions{}) + if err != nil { + w.Errors <- err + return + } + + commits := make([]*GitCommit, 0) + err = cIter.ForEach(func(c *object.Commit) error { + if v, ok := w.branchCache[branch]; ok { + // Exit from the loop, we reached the last commit we saw previously + if c.Hash.String() == v { + return errExitFromLoop + } + + commit := &GitCommit{ + Hash: c.Hash.String(), + Message: c.Message, + Branch: branch, + } + commits = append(commits, commit) + } else { + // this is the first time we see this branch. + // Storing only the last commit's hash because + // we can't know from what other branch it comes + commit := &GitCommit{ + Hash: c.Hash.String(), + Message: c.Message, + Branch: branch, + } + commits = append(commits, commit) + return errExitFromLoop + } + return nil + }) + if err != nil && err != errExitFromLoop { + w.Errors <- err + return + } + + if len(commits) != 0 { + // Caching last commit + w.branchCache[branch] = commits[0].Hash + + if disableNotification == false { + if w.config.AssembleEvents { + event := Event{ + Key: "commit", + Type: 0, + Object: &GitObject{ + Commits: commits, + }, + } + w.Events <- event + } else { + for _, commit := range commits { + event := Event{ + Key: "commit", + Type: 0, + Object: &GitObject{ + Commits: []*GitCommit{commit}, + }, + } + w.Events <- event + } + } + } + } + } +} + +func (w *GitWatcher) checkTags(disableNotification bool) { + // event on Tags + tagrefs, err := w.repository.Tags() + if err != nil { + w.Errors <- err + return + } + tags := make([]*GitCommit, 0) + err = tagrefs.ForEach(func(t *plumbing.Reference) error { + if _, ok := w.tagCache[t.Name().Short()]; !ok { + tags = append(tags, &GitCommit{ + Hash: t.Hash().String(), + Message: t.Name().Short(), + }) + w.tagCache[t.Name().Short()] = t.Hash().String() + } + return nil + }) + if err != nil { + w.Errors <- err + return + } + + if disableNotification == false && len(tags) != 0 { + if w.config.AssembleEvents { + event := Event{ + Key: "tag", + Type: 0, + Object: &GitObject{ + Commits: tags, + }, + } + w.Events <- event + } else { + for _, tag := range tags { + event := Event{ + Key: "tag", + Type: 0, + Object: &GitObject{ + Commits: []*GitCommit{tag}, + }, + } + w.Events <- event + } + } + } +} + +func (w *GitWatcher) moveToBranch(branch string) error { + wt, err := w.repository.Worktree() + if err != nil { + return fmt.Errorf("getting worktree: %s", err) + } + + // Move to a precise branch + err = wt.Checkout(&git.CheckoutOptions{ + Branch: plumbing.NewBranchReferenceName(branch), + }) + if err != nil { + return fmt.Errorf("checkout of repo '%s': %s", branch, err) + } + return nil +} + +func (w *GitWatcher) updateRepo() error { + // tmp dir has been deleted?!?! + if _, err := os.Stat(w.config.TempDir); os.IsNotExist(err) { + w.repository = nil + } + + if w.repository == nil { + opts := &git.CloneOptions{ + URL: w.config.RepoURL, + } + + switch w.config.AuthType { + case "ssh": + _, err := os.Stat(w.config.SSHPrivateKey) + if err != nil { + return fmt.Errorf("cannot read file '%s': %s", w.config.SSHPrivateKey, err) + } + + publicKeys, err := ssh.NewPublicKeysFromFile("git", w.config.SSHPrivateKey, w.config.SSHPKeyPassword) + if err != nil { + return fmt.Errorf("loading pkeys from '%s': %s", w.config.SSHPrivateKey, err) + } + opts.Auth = publicKeys + + case "http_token": + opts.Auth = &http.BasicAuth{ + Username: "token", + Password: w.config.HTTPToken, + } + + case "http_user_pass": + opts.Auth = &http.BasicAuth{ + Username: w.config.HTTPPassword, + Password: w.config.HTTPToken, + } + + default: + + } + + r, err := git.PlainClone(w.config.TempDir, false, opts) + if err != nil && err == git.ErrRepositoryAlreadyExists { + r, err = git.PlainOpen(w.config.TempDir) + } + if err != nil { + return fmt.Errorf("cloning repo: %s", err) + } + w.repository = r + } + + wt, err := w.repository.Worktree() + if err != nil { + return fmt.Errorf("getting worktree: %s", err) + } + + // Update the repository + err = wt.Pull(&git.PullOptions{}) + if err != nil && err != git.NoErrAlreadyUpToDate { + return fmt.Errorf("checkout of repo '%s': %s", w.config.RepoBranch, err) + } + return nil +} + +func (w *GitWatcher) enumerateFiles(prefix string, callback func(object *GitObject) bool) error { + err := w.moveToBranch(w.config.RepoBranch) + if err != nil { + return fmt.Errorf("switching branch to '%s': %s", w.config.RepoBranch, err) + } + + // getting head reference + ref, err := w.repository.Head() + if err != nil { + return fmt.Errorf("getting head reference: %s", err) + } + + // retrieve the commit pointed from head + commit, err := w.repository.CommitObject(ref.Hash()) + if err != nil { + return fmt.Errorf("retrieving commit '%s': %s", ref.Hash().String(), err) + } + + // retrieve the tree from the commit + tree, err := commit.Tree() + if err != nil { + return fmt.Errorf("retrieving tree of '%s': %s", ref.Hash().String(), err) + } + + // iterate files in the commit + err = tree.Files().ForEach(func(f *object.File) error { + if strings.HasPrefix(f.Name, prefix) || prefix != "" { + o := &GitObject{ + Key: f.Name, + Size: f.Size, + Hash: f.Hash.String(), + FileMode: os.FileMode(f.Mode), + Commits: nil, + } + if callback(o) == false { + return errExitFromLoop + } + } + + return nil + }) + if err != nil && err != errExitFromLoop { + return fmt.Errorf("looping files : %s", err) + } + + return nil +} + +func init() { + supportedServices["git"] = newGitWatcher +} diff --git a/go.mod b/go.mod index 6a698ec..cdd062c 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( cloud.google.com/go v0.76.0 // indirect github.com/dropbox/dropbox-sdk-go-unofficial v5.6.0+incompatible github.com/fsnotify/fsnotify v1.4.9 + github.com/go-git/go-git/v5 v5.2.0 // indirect github.com/golang/mock v1.4.4 github.com/google/uuid v1.2.0 // indirect github.com/minio/md5-simd v1.1.1 // indirect diff --git a/go.sum b/go.sum index ee4ba6f..a5a264c 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,9 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cheggaaa/pb v1.0.29/go.mod h1:W40334L7FMC5JKWldsTWbdGjLo0RxUKK73K+TuPxX30= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -47,12 +50,15 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dropbox/dropbox-sdk-go-unofficial v5.6.0+incompatible h1:DtumzkLk2zZ2SeElEr+VNz+zV7l+BTe509cV4sKPXbM= github.com/dropbox/dropbox-sdk-go-unofficial v5.6.0+incompatible/go.mod h1:lr+LhMM3F6Y3lW1T9j2U5l7QeuWm87N9+PPXo3yH4qY= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= +github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -60,8 +66,19 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= +github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= +github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM= +github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= +github.com/go-git/go-git v1.0.0 h1:YcN9iDGDoXuIw0vHls6rINwV416HYa0EB2X+RBsyYp4= +github.com/go-git/go-git v4.7.0+incompatible h1:+W9rgGY4DOKKdX2x6HxSR7HNeTxqiKrOvKnuittYVdA= +github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw= +github.com/go-git/go-git/v5 v5.2.0 h1:YPBLG/3UK1we1ohRkncLjaXWLW+HKp5QNM/jTli2JgI= +github.com/go-git/go-git/v5 v5.2.0/go.mod h1:kh02eMX+wdqqxgNMEyq8YgwlIOsDOa9homkUq1PoTMs= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -135,12 +152,19 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg= +github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= +github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.3.1 h1:5JNjFYYQrZeKRJ0734q51WCEEn2huer72Dc7K+R/b6s= @@ -150,6 +174,7 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= @@ -171,12 +196,16 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= @@ -190,6 +219,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70= +github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -203,11 +234,13 @@ go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.22.6 h1:BdkrbWrzDlV9dnbzoP7sfN+dHheJ4J9JOaYxcUDL+ok= go.opencensus.io v0.22.6/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -303,6 +336,7 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -505,11 +539,16 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..78f520a --- /dev/null +++ b/utils.go @@ -0,0 +1,12 @@ +package cloudwatcher + +func inArray(needle interface{}, generic interface{}) bool { + if haystack, ok := generic.([]string); ok { + for _, v := range haystack { + if v == needle { + return true + } + } + } + return false +}