From fe680c31dbcfa0becd7a24311209d490f69fbf96 Mon Sep 17 00:00:00 2001 From: Knut Ahlers Date: Mon, 19 Mar 2018 18:15:49 +0100 Subject: [PATCH] Add update command to update from git tags Signed-off-by: Knut Ahlers --- cmd/helper.go | 34 +++++++++----- cmd/update.go | 50 +++++++++++++++++++++ tags/tags.go | 112 ++++++++++++++++++++++++++++++++++++++++++++++ tags/tags_test.go | 12 +++++ 4 files changed, 196 insertions(+), 12 deletions(-) create mode 100644 cmd/update.go create mode 100644 tags/tags.go create mode 100644 tags/tags_test.go diff --git a/cmd/helper.go b/cmd/helper.go index 6a23ab2..957f008 100644 --- a/cmd/helper.go +++ b/cmd/helper.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "io/ioutil" + "os" yaml "gopkg.in/yaml.v2" ) @@ -14,17 +15,26 @@ type ansibleRoleDefinition struct { Version string `yaml:"version"` } -func patchRoleFile(rolesFile string, updates map[string]string) error { - var ( - inFileContent []byte - err error - ) - if inFileContent, err = ioutil.ReadFile(rolesFile); err != nil { - return fmt.Errorf("Roles file not found: %s", err) +func getRoleDefinitions(rolesFile string) ([]ansibleRoleDefinition, error) { + rf, err := os.Open(rolesFile) + if err != nil { + return nil, err } + defer rf.Close() + + def := []ansibleRoleDefinition{} + return def, yaml.NewDecoder(rf).Decode(&def) +} + +func patchRoleFile(rolesFile string, updates map[string]string) error { + inFile, err := os.Open(rolesFile) + if err != nil { + return fmt.Errorf("Unable to open roles files: %s", err) + } + defer inFile.Close() in := []ansibleRoleDefinition{} - if err = yaml.Unmarshal(inFileContent, &in); err != nil { + if err = yaml.NewDecoder(inFile).Decode(&in); err != nil { return fmt.Errorf("Unable to parse roles file: %s", err) } @@ -36,13 +46,13 @@ func patchRoleFile(rolesFile string, updates map[string]string) error { } } - if inFileContent, err = yaml.Marshal(in); err != nil { + buf := new(bytes.Buffer) + buf.Write([]byte("---\n\n")) + + if err := yaml.NewEncoder(buf).Encode(in); err != nil { return fmt.Errorf("Unable to marshal roles file: %s", err) } - buf := new(bytes.Buffer) - buf.Write([]byte("---\n\n")) - buf.Write(inFileContent) buf.Write([]byte("\n...\n")) if err = ioutil.WriteFile(rolesFile, buf.Bytes(), 0644); err != nil { diff --git a/cmd/update.go b/cmd/update.go new file mode 100644 index 0000000..c98b495 --- /dev/null +++ b/cmd/update.go @@ -0,0 +1,50 @@ +package cmd + +import ( + "time" + + "github.com/Luzifer/ansible-role-version/tags" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +// updateCmd represents the update command +var updateCmd = &cobra.Command{ + Use: "update", + Short: "Seek for updates in git repositories and update the roles file", + RunE: func(cmd *cobra.Command, args []string) error { + roles, err := getRoleDefinitions(cfg.RolesFile) + if err != nil { + return err + } + + updates := map[string]string{} + + for _, role := range roles { + logger := log.WithFields(log.Fields{ + "role": role.Name, + }) + + tag, err := tags.GetLatestTag(role.Src, true) + if err != nil { + logger.WithError(err).Error("Failed to fetch latest tag") + continue + } + + if tag.Name != role.Version { + updates[role.Name] = tag.Name + logger.WithFields(log.Fields{ + "from": role.Version, + "to": tag.Name, + "released": tag.When.Format(time.RFC1123), + }).Info("Update queued") + } + } + + return patchRoleFile(cfg.RolesFile, updates) + }, +} + +func init() { + RootCmd.AddCommand(updateCmd) +} diff --git a/tags/tags.go b/tags/tags.go new file mode 100644 index 0000000..139a264 --- /dev/null +++ b/tags/tags.go @@ -0,0 +1,112 @@ +package tags + +import ( + "errors" + "io" + "sort" + "time" + + "gopkg.in/src-d/go-billy.v4/memfs" + git "gopkg.in/src-d/go-git.v4" + "gopkg.in/src-d/go-git.v4/plumbing" + "gopkg.in/src-d/go-git.v4/storage/memory" +) + +type Tag struct { + Name string + When time.Time +} + +var ( + ErrNoTagsFound = errors.New("No tags found") +) + +// GetLatestTag clones a Git repository into memory and resolves latest +// leightweight or annotated tag from it +func GetLatestTag(repoURL string, includeLightweight bool) (*Tag, error) { + fs := memfs.New() + // Git objects storer based on memory + storer := memory.NewStorage() + + // Clones the repository into the worktree (fs) and storer all the .git + // content into the storer + r, _ := git.Clone(storer, fs, &git.CloneOptions{ + URL: repoURL, + }) + + tags := []Tag{} + + // Get reference iterator for all tags + it, err := r.Tags() + if err != nil { + return nil, err + } + defer it.Close() + + for { + ref, err := it.Next() + if err != nil { + if err == io.EOF { + break + } + return nil, err + } + + if !ref.Name().IsTag() { + continue + } + + var when time.Time + + // Check whether the hash is resolvable to a tag + if t := tryTag(r, ref.Hash()); t != nil { + when = *t + } + + // If it wasn't and we may include leightweight tags check for commit + if when.IsZero() && includeLightweight { + if t := tryCommit(r, ref.Hash()); t != nil { + when = *t + } + } + + if when.IsZero() { + // We've resolved no tag + continue + } + + tags = append(tags, Tag{ + Name: ref.Name().Short(), + When: when, + }) + } + + if len(tags) == 0 { + return nil, ErrNoTagsFound + } + + // Tags may be unsorted by design so we need to sort them + sort.SliceStable(tags, func(i, j int) bool { + return tags[i].When.Before(tags[j].When) + }) + + return &tags[len(tags)-1], nil +} + +func tryTag(r *git.Repository, hash plumbing.Hash) *time.Time { + tag, err := r.TagObject(hash) + if err != nil { + return nil + } + + return &tag.Tagger.When +} + +func tryCommit(r *git.Repository, hash plumbing.Hash) *time.Time { + commit, err := r.CommitObject(hash) + if err != nil { + return nil + } + + return &commit.Committer.When +} diff --git a/tags/tags_test.go b/tags/tags_test.go new file mode 100644 index 0000000..059593c --- /dev/null +++ b/tags/tags_test.go @@ -0,0 +1,12 @@ +package tags + +import "log" + +func ExampleGetLatestTag() { + tag, err := GetLatestTag("https://github.com/luzifer-ansible/deploy-git.git", true) + if err != nil { + log.Fatalf("Could not resolve latest tag: %s", err) + } + + log.Printf("Latest tag: %q, created at %v", tag.Name, tag.When) +}