Skip to content

Commit

Permalink
Merge pull request #101 from krancour/bulk-mv-milestone
Browse files Browse the repository at this point in the history
feat(git): Add bulk reassignment of issues and PRs from one milestone to another
  • Loading branch information
krancour authored Jun 28, 2016
2 parents 1273e5f + 1554cbc commit 2c4ec68
Show file tree
Hide file tree
Showing 5 changed files with 291 additions and 1 deletion.
2 changes: 2 additions & 0 deletions actions/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ const (
GHOrgFlag = "ghOrg"
// StagingDirFlag represents the '-stagingDir' flag
StagingDirFlag = "stagingDir"
// IncludeClosed represents the '--includeClosed' flag
IncludeClosed = "includeClosed"
)

const (
Expand Down
66 changes: 66 additions & 0 deletions actions/move_milestones.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package actions

import (
"fmt"
"log"
"sync"

"github.com/codegangsta/cli"
"github.com/deis/deisrel/git"
"github.com/google/go-github/github"
)

func MoveMilestone(ghClient *github.Client) func(c *cli.Context) error {
return func(c *cli.Context) error {
oldMilestone := c.Args().Get(0)
newMilestone := c.Args().Get(1)
if oldMilestone == "" || newMilestone == "" {
log.Fatal("Usage: mv <old-milestone> <new-milestone>")
}
ok := true
if !c.Bool(YesFlag) {
var err error
ok, err = prompt()
if err != nil {
log.Fatal(err)
}
}
if ok {
var wg sync.WaitGroup
done := make(chan bool)
errCh := make(chan error)
defer close(errCh)
for _, repo := range allGitRepoNames {
wg.Add(1)
go func(repo string) {
defer wg.Done()
if err := git.MoveMilestone(ghClient, repo, oldMilestone, newMilestone, c.Bool(IncludeClosed)); err != nil {
errCh <- fmt.Errorf("Error moving %s issues from milestone %s to milestone %s: %s", repo, oldMilestone, newMilestone, err)
}
}(repo)
}
go func() {
wg.Wait()
close(done)
}()
errs := []error{}
for {
select {
case <-done:
if len(errs) > 0 {
var errStr string
for _, err := range errs {
log.Println(err)
errStr = fmt.Sprintf("%s%s\n", errStr, err)
}
return fmt.Errorf(errStr)
}
return nil
case err := <-errCh:
errs = append(errs, err)
}
}
}
return nil
}
}
96 changes: 96 additions & 0 deletions git/move_milestones.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package git

import (
"fmt"
"sync"

"github.com/google/go-github/github"
)

func MoveMilestone(ghClient *github.Client, repo string, oldMilestoneName string, newMilestoneName string, includeClosed bool) error {
is := ghClient.Issues
milestones, _, err := is.ListMilestones("deis", repo, &github.MilestoneListOptions{})
if err != nil {
return err
}
oldMilestone, err := getMilestoneFromMilestoneList(milestones, oldMilestoneName)
if err != nil {
return err
}
newMilestone, err := getMilestoneFromMilestoneList(milestones, newMilestoneName)
if err != nil {
return err
}
issueState := "open"
if includeClosed {
issueState = "all"
}
// This list will ALSO include PRs:
oldMilestoneIssues, _, err := is.ListByRepo("deis", repo, &github.IssueListByRepoOptions{
Milestone: fmt.Sprintf("%d", *oldMilestone.Number),
State: issueState,
ListOptions: github.ListOptions{
PerPage: 10000,
},
})
if err != nil {
return err
}
var wg sync.WaitGroup
done := make(chan bool)
errCh := make(chan error)
defer close(errCh)
for _, issue := range oldMilestoneIssues {
wg.Add(1)
go func(issue github.Issue) {
defer wg.Done()
ir := &github.IssueRequest{
Milestone: newMilestone.Number,
}
if _, _, err := is.Edit("deis", repo, *issue.Number, ir); err != nil {
errCh <- err
}
}(issue)
}
go func() {
wg.Wait()
close(done)
}()
errs := []error{}
for {
select {
case <-done:
if len(errs) > 0 {
var errStr string
for _, err := range errs {
errStr = fmt.Sprintf("%s%s\n", errStr, err)
}
return fmt.Errorf(errStr)
}
return nil
case err := <-errCh:
errs = append(errs, err)
}
}
}

func getMilestoneFromMilestoneList(milestones []github.Milestone, milestoneName string) (*github.Milestone, error) {
for _, milestone := range milestones {
if *milestone.Title == milestoneName {
return &milestone, nil
}
}
return nil, newErrMilestoneNotFound(milestoneName)
}

type errMilestoneNotFound struct {
milestone string
}

func newErrMilestoneNotFound(milestone string) errMilestoneNotFound {
return errMilestoneNotFound{milestone: milestone}
}

func (e errMilestoneNotFound) Error() string {
return fmt.Sprintf("milestone %s not found", e.milestone)
}
105 changes: 105 additions & 0 deletions git/move_milestones_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package git

import (
"fmt"
"net/http"
"reflect"
"testing"

"github.com/deis/deisrel/testutil"
)

var (
repo = "controller"
issueNumber = 1
oldMilestone = "v2.1"
newMilestone = "v2.2"
)

func TestMoveMilestoneWithOldMilestoneNotFound(t *testing.T) {
ts := testutil.NewTestServer()
defer ts.Close()

ts.Mux.HandleFunc(fmt.Sprintf("/repos/deis/%s/milestones", repo), func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
t.Errorf("Request method: %v, want GET", r.Method)
}
// Return a list of milestones
fmt.Fprintf(w, `[ { "title": "%s" } ]`, newMilestone)
})

expected := "git.errMilestoneNotFound"
if err := MoveMilestone(ts.Client, repo, oldMilestone, newMilestone, false); err == nil {
t.Error("Did not receive expected error message")
} else if errType := reflect.TypeOf(err).String(); errType != expected {
t.Errorf("Expected a %s, but got a %s", expected, errType)
}
}

func TestMoveMilestoneWithNewMilestoneNotFound(t *testing.T) {
ts := testutil.NewTestServer()
defer ts.Close()

ts.Mux.HandleFunc(fmt.Sprintf("/repos/deis/%s/milestones", repo), func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
t.Errorf("Request method: %v, want GET", r.Method)
}
// Return a list of milestones
fmt.Fprintf(w, `[ { "title": "%s" } ]`, oldMilestone)
})

expected := "git.errMilestoneNotFound"
if err := MoveMilestone(ts.Client, repo, oldMilestone, newMilestone, false); err == nil {
t.Error("Did not receive expected error message")
} else if errType := reflect.TypeOf(err).String(); errType != expected {
t.Errorf("Expected a %s, but got a %s", expected, errType)
}
}

func TestMoveMilestone(t *testing.T) {
ts := testutil.NewTestServer()
defer ts.Close()

ts.Mux.HandleFunc(fmt.Sprintf("/repos/deis/%s/milestones", repo), func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
t.Errorf("Request method: %v, want GET", r.Method)
}
// Return a list of milestones
fmt.Fprintf(w, `
[
{
"number": 1,
"title": "%s"
},
{
"number": 2,
"title": "%s"
}
]`, oldMilestone, newMilestone)
})

ts.Mux.HandleFunc(fmt.Sprintf("/repos/deis/%s/issues", repo), func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
t.Errorf("Request method: %v, want GET", r.Method)
}
// Return a list of issues
fmt.Fprintf(w, `
[
{
"number": %d
}
]`, issueNumber)
})

ts.Mux.HandleFunc(fmt.Sprintf("/repos/deis/%s/issues/%d", repo, issueNumber), func(w http.ResponseWriter, r *http.Request) {
if r.Method != "PATCH" {
t.Errorf("Request method: %v, want PATCH", r.Method)
}
// Return a list of issues
fmt.Fprint(w, "{}")
})

if err := MoveMilestone(ts.Client, repo, oldMilestone, newMilestone, false); err != nil {
t.Error(err)
}
}
23 changes: 22 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,25 @@ func main() {
},
},
},
cli.Command{
Name: "milestone",
Subcommands: []cli.Command{
cli.Command{
Name: "mv",
Action: actions.MoveMilestone(ghClient),
Flags: []cli.Flag{
cli.BoolFlag{
Name: actions.YesFlag,
Usage: "If true, skip the prompt requesting permission",
},
cli.BoolFlag{
Name: actions.IncludeClosed,
Usage: "If true, moved closed issues as well as open ones",
},
},
},
},
},
},
},
cli.Command{
Expand Down Expand Up @@ -150,5 +169,7 @@ func main() {
},
}

app.Run(os.Args)
if err := app.Run(os.Args); err != nil {
os.Exit(1)
}
}

0 comments on commit 2c4ec68

Please sign in to comment.