forked from arschles/deisrel
-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #101 from krancour/bulk-mv-milestone
feat(git): Add bulk reassignment of issues and PRs from one milestone to another
- Loading branch information
Showing
5 changed files
with
291 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters