1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: Apache-2.0
15 "git.arvados.org/arvados-dev.git/lib/redmine"
16 survey "github.com/AlecAivazis/survey/v2"
17 "github.com/go-git/go-git/v5"
18 "github.com/go-git/go-git/v5/plumbing"
19 "github.com/go-git/go-git/v5/plumbing/object"
20 "github.com/go-git/go-git/v5/plumbing/storer"
21 "github.com/go-git/go-git/v5/storage/memory"
22 "github.com/spf13/cobra"
26 rootCmd.AddCommand(redmineCmd)
27 redmineCmd.AddCommand(issuesCmd)
29 associateIssueCmd.Flags().IntP("release", "r", 0, "Redmine release ID")
30 err := associateIssueCmd.MarkFlagRequired("release")
32 log.Fatalf(err.Error())
34 associateIssueCmd.Flags().IntP("issue", "i", 0, "Redmine issue ID")
35 err = associateIssueCmd.MarkFlagRequired("issue")
37 log.Fatalf(err.Error())
39 issuesCmd.AddCommand(associateIssueCmd)
41 findAndAssociateIssuesCmd.Flags().IntP("release", "r", 0, "Redmine release ID")
42 err = findAndAssociateIssuesCmd.MarkFlagRequired("release")
44 log.Fatalf(err.Error())
46 findAndAssociateIssuesCmd.Flags().StringP("previous-release-tag", "p", "", "Semantic version number of the previous release")
47 err = findAndAssociateIssuesCmd.MarkFlagRequired("previous-release-tag")
49 log.Fatalf(err.Error())
51 findAndAssociateIssuesCmd.Flags().StringP("new-release-commit", "n", "", "Git commit for the new release")
52 err = findAndAssociateIssuesCmd.MarkFlagRequired("new-release-commit")
54 log.Fatalf(err.Error())
56 findAndAssociateIssuesCmd.Flags().BoolP("auto-set", "a", false, "Associate issues without existing release without prompting")
57 findAndAssociateIssuesCmd.Flags().BoolP("skip-release-change", "s", false, "Skip issues already assigned to another release (do not prompt)")
58 issuesCmd.AddCommand(findAndAssociateIssuesCmd)
61 var redmineCmd = &cobra.Command{
63 Short: "Manage Redmine",
64 Long: "Manage Redmine.\n" +
65 "\nThe REDMINE_ENDPOINT environment variable must be set to the base URL of your redmine server." +
66 "\nThe REDMINE_APIKEY environment variable must be set to your redmine API key.",
67 PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
68 if conf.Endpoint == "" {
71 fmt.Println("Error: the REDMINE_ENDPOINT environment variable must be set to the base URL of your redmine server")
74 if conf.Apikey == "" {
77 fmt.Println("Error: the REDMINE_APIKEY environment variable must be set to your redmine API key")
84 var issuesCmd = &cobra.Command{
86 Short: "Manage Redmine issues",
87 Long: "Manage Redmine issues.\n" +
88 "\nThe REDMINE_ENDPOINT environment variable must be set to the base URL of your redmine server." +
89 "\nThe REDMINE_APIKEY environment variable must be set to your redmine API key.",
92 var associateIssueCmd = &cobra.Command{
94 Short: "Associate an issue with a release",
95 Long: "Associate an issue with a release.\n" +
96 "\nThe REDMINE_ENDPOINT environment variable must be set to the base URL of your redmine server." +
97 "\nThe REDMINE_APIKEY environment variable must be set to your redmine API key.",
98 Run: func(cmd *cobra.Command, args []string) {
99 issueID, err := cmd.Flags().GetInt("issue")
101 fmt.Printf("Error converting Redmine issue ID to integer: %s", err)
105 releaseID, err := cmd.Flags().GetInt("release")
107 fmt.Printf("Error converting Redmine release ID to integer: %s", err)
111 redmine := redmine.NewClient(conf.Endpoint, conf.Apikey)
113 i, err := redmine.GetIssue(issueID)
115 fmt.Printf("%s\n", err.Error())
120 if i.Release == nil || i.Release["release"].ID == 0 {
122 } else if i.Release["release"].ID != releaseID {
126 err = redmine.SetRelease(*i, releaseID)
128 fmt.Printf("%s\n", err.Error())
131 fmt.Printf("[changed] release for issue %d set to %d\n", i.ID, releaseID)
134 fmt.Printf("[ok] release for issue %d was already set to %d, not updating\n", i.ID, i.Release["release"].ID)
139 func checkError(err error) {
141 fmt.Printf("%s\n", err.Error())
146 var findAndAssociateIssuesCmd = &cobra.Command{
147 Use: "find-and-associate",
148 Short: "Find all issue numbers to associate with a release, and associate them",
149 Long: "Find all issue numbers to associate with a release, and associate them.\n" +
150 "\nThe REDMINE_ENDPOINT environment variable must be set to the base URL of your redmine server." +
151 "\nThe REDMINE_APIKEY environment variable must be set to your redmine API key.",
152 Run: func(cmd *cobra.Command, args []string) {
153 previousReleaseTag, err := cmd.Flags().GetString("previous-release-tag")
155 log.Fatal(fmt.Errorf("Error retrieving previous release: %s", err))
159 newReleaseCommitHash, err := cmd.Flags().GetString("new-release-commit")
161 log.Fatal(fmt.Errorf("Error retrieving new release: %s", err))
164 releaseID, err := cmd.Flags().GetInt("release")
166 log.Fatal(fmt.Errorf("Error converting Redmine release ID to integer: %s", err))
170 autoSet, err := cmd.Flags().GetBool("auto-set")
172 log.Fatal(fmt.Errorf("Error getting auto-set value: %s", err))
175 skipReleaseChange, err := cmd.Flags().GetBool("skip-release-change")
177 log.Fatal(fmt.Errorf("Error getting skip-release-change value: %s", err))
181 if len(previousReleaseTag) < 5 || len(previousReleaseTag) > 8 {
182 log.Fatal(fmt.Errorf("The previous-release-tag argument is of an unexpected format. Expecting a semantic version (e.g. 2.3.0)"))
185 if len(newReleaseCommitHash) != 7 && len(newReleaseCommitHash) != 40 {
186 log.Fatal(fmt.Errorf("The new-release-commit argument is of an unexpected format. Expecting a git commit hash (7 or 40 digits long)"))
190 // Clone the repo in memory
191 fmt.Println("Cloning https://github.com/arvados/arvados.git")
192 repo, err := git.Clone(memory.NewStorage(), nil, &git.CloneOptions{
193 URL: "https://github.com/arvados/arvados.git",
196 fmt.Println("... done")
198 start, err := repo.ResolveRevision(plumbing.Revision("refs/tags/" + previousReleaseTag))
200 fmt.Printf("previous-release-tag: %s (%s)\n", previousReleaseTag, start)
201 fmt.Printf("new-release-commit: %s\n", newReleaseCommitHash)
204 // Build the exclusion list
205 seen := make(map[plumbing.Hash]bool)
206 excludeIter, err := repo.Log(&git.LogOptions{From: *start, Order: git.LogOrderCommitterTime})
208 excludeIter.ForEach(func(c *object.Commit) error {
213 // isValid returns merge commits that are not in the exclusion list
214 var isValid object.CommitFilter = func(commit *object.Commit) bool {
215 _, ok := seen[commit.Hash]
217 // use len(commit.ParentHashes) to only get merge commits
218 return !ok && len(commit.ParentHashes) >= 2
221 headCommit, err := repo.CommitObject(plumbing.NewHash(newReleaseCommitHash))
224 iter := object.NewFilterCommitIter(headCommit, &isValid, nil)
226 issues := make(map[int]bool)
227 re := regexp.MustCompile(`Merge branch `)
228 reNotMain := regexp.MustCompile(`Merge branch .(main|master)`)
229 reIssueRef := regexp.MustCompile(`(Closes|closes|Refs|refs|Fixes|fixes) #(\d+)`)
230 err = iter.ForEach(func(c *object.Commit) error {
231 // We have a git commit hook that requires an issue reference on merge commits
232 if re.MatchString(c.Message) && !reNotMain.MatchString(c.Message) {
233 m := reIssueRef.FindStringSubmatch(c.Message)
235 i, err := strconv.Atoi(m[2])
243 if c.Hash == *start {
244 return storer.ErrStop
250 // Sort the issue map keys
251 keys := make([]int, 0, len(issues))
252 for k := range issues {
253 keys = append(keys, k)
257 redmine := redmine.NewClient(conf.Endpoint, conf.Apikey)
259 for c, k := range keys {
260 fmt.Printf("%d (%d/%d): ", k, c+1, len(keys))
261 // Look up the issue, see if it is already associated with the desired release
263 i, err := redmine.GetIssue(k)
266 fmt.Printf("[error] unable to retrieve issue: %s\n", err.Error())
267 fmt.Println("============================================")
270 fmt.Println(i.Subject)
272 if i.Release != nil && i.Release["release"].ID != 0 {
273 if i.Release["release"].ID == releaseID {
274 fmt.Printf("[ok] release is already set to %d, nothing to do\n", i.Release["release"].ID)
275 } else if !skipReleaseChange {
276 fmt.Printf("%s/issues/%d\n", conf.Endpoint, k)
278 prompt := &survey.Confirm{
279 Message: fmt.Sprintf("release is set to %d, do you want to change it to %d ?", i.Release["release"].ID, releaseID),
281 err = survey.AskOne(prompt, &confirm)
286 err = redmine.SetRelease(*i, releaseID)
290 fmt.Printf("[changed] release for issue %d set to %d\n", i.ID, releaseID)
294 fmt.Printf("[ok] release is set to %d, not changing it to %d\n", i.Release["release"].ID, releaseID)
297 fmt.Printf("%s/issues/%d\n", conf.Endpoint, k)
300 prompt := &survey.Confirm{
301 Message: fmt.Sprintf("Release is not set, do you want to set it to %d ?", releaseID),
303 err = survey.AskOne(prompt, &confirm)
308 if confirm || autoSet {
309 err = redmine.SetRelease(*i, releaseID)
313 fmt.Printf("[changed] release for issue %d set to %d\n", i.ID, releaseID)
317 fmt.Println("============================================")