From 77df81fcbd1db51a6f410baff4e6fabfbdad54d3 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Thu, 19 Dec 2013 15:49:36 -0800 Subject: [PATCH 01/21] Add command to create releases. --- README.md | 4 ++- commands/commands.go | 1 + commands/release.go | 72 +++++++++++++++++++++++++++++++------------- 3 files changed, 55 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 9cee4b9..1342ef4 100644 --- a/README.md +++ b/README.md @@ -320,9 +320,11 @@ For more details, run `gh help alias`. ### gh release (beta) - $ gh release + $ gh releases > (prints a list of releases of YOUR_USER/CURRENT_REPO) + $ gh release TAG + > (creates a new release for the given tag) ### gh issues (beta) diff --git a/commands/commands.go b/commands/commands.go index 4fd570f..0fb578e 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -77,6 +77,7 @@ var GitHub = []*Command{ cmdCiStatus, cmdBrowse, cmdCompare, + cmdReleases, cmdRelease, cmdIssue, } diff --git a/commands/release.go b/commands/release.go index 778edf4..1159b8a 100644 --- a/commands/release.go +++ b/commands/release.go @@ -8,34 +8,64 @@ import ( "strings" ) -var cmdRelease = &Command{ - Run: release, - Usage: "release", - Short: "Manipulate releases on GitHub", - Long: `Manipulates releases on GitHub for the project that the "origin" remote -points to. -`, +var ( + cmdReleases = &Command{ + Run: releases, + Usage: "releases", + Short: "Retrieve releases from GitHub", + Long: `Retrieve releases from GitHub for the project that the "origin" remote points to.`} + + cmdRelease = &Command{ + Run: release, + Usage: "release TAG [-d] [-p] [-a ]", + Short: "Create a new release in GitHub", + Long: `Create a new release in GitHub for the project that the "origin" remote points to. +- It requires the name of the tag to release as a first argument. +- The assets to include in the release are taken from releases/TAG or from the directory specified by -a. +- Use the flag -d to create a draft. +- Use the flag -p to create a prerelease. +`} + + flagReleaseDraft, + flagReleasePrerelease bool + flagReleaseAssetsDir string +) + +func init() { + cmdRelease.Flag.BoolVar(&flagReleaseDraft, "d", false, "DRAFT") + cmdRelease.Flag.BoolVar(&flagReleasePrerelease, "p", false, "PRERELEASE") + cmdRelease.Flag.StringVar(&flagReleaseAssetsDir, "a", "", "ASSETS_DIR") +} + +func releases(cmd *Command, args *Args) { + runInLocalRepo(func(project *github.Project, gh *github.Client) { + if args.Noop { + fmt.Printf("Would request list of releases for %s\n", project) + } else { + releases, err := gh.Releases(project) + utils.Check(err) + var outputs []string + for _, release := range releases { + out := fmt.Sprintf("%s (%s)\n%s", release.Name, release.TagName, release.Body) + outputs = append(outputs, out) + } + + fmt.Println(strings.Join(outputs, "\n\n")) + } + }) } func release(cmd *Command, args *Args) { + tag := args.LastParam() +} + +func runInLocalRepo(fn func(project *github.Project, client *github.Client)) { localRepo := github.LocalRepo() project, err := localRepo.CurrentProject() utils.Check(err) - gh := github.NewClient(project.Host) - if args.Noop { - fmt.Printf("Would request list of releases for %s\n", project) - } else { - releases, err := gh.Releases(project) - utils.Check(err) - var outputs []string - for _, release := range releases { - out := fmt.Sprintf("%s (%s)\n%s", release.Name, release.TagName, release.Body) - outputs = append(outputs, out) - } - - fmt.Println(strings.Join(outputs, "\n\n")) - } + client := github.NewClient(project.Host) + fn(project, client) os.Exit(0) } From 6b38fec34b3d9e00f227aeb31246b2d5d7a4cf29 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Thu, 19 Dec 2013 16:54:14 -0800 Subject: [PATCH 02/21] Extract editor logic from the pull request commands. --- commands/pull_request.go | 137 ++----------------------------------- utils/editor.go | 142 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+), 131 deletions(-) create mode 100644 utils/editor.go diff --git a/commands/pull_request.go b/commands/pull_request.go index 0999ae3..645befd 100644 --- a/commands/pull_request.go +++ b/commands/pull_request.go @@ -145,25 +145,8 @@ func pullRequest(cmd *Command, args *Args) { //headProject = github.NewProject("", headProject.Name, headProject.Host) } - var title, body string - - if flagPullRequestMessage != "" { - title, body = readMsg(flagPullRequestMessage) - } - - if flagPullRequestFile != "" { - var ( - content []byte - err error - ) - if flagPullRequestFile == "-" { - content, err = ioutil.ReadAll(os.Stdin) - } else { - content, err = ioutil.ReadFile(flagPullRequestFile) - } - utils.Check(err) - title, body = readMsg(string(content)) - } + title, body := utils.GetTitleAndBodyFromFlags(flagPullRequestMessage, flagPullRequestFile) + utils.Check(err) fullBase := fmt.Sprintf("%s:%s", baseProject.Owner, base) fullHead := fmt.Sprintf("%s:%s", headProject.Owner, head) @@ -179,10 +162,8 @@ func pullRequest(cmd *Command, args *Args) { if title == "" && flagPullRequestIssue == "" { commits, _ := git.RefList(base, head) - t, b, err := writePullRequestTitleAndBody(base, head, fullBase, fullHead, commits) + title, body, err = writePullRequestTitleAndBody(base, head, fullBase, fullHead, commits) utils.Check(err) - title = t - body = b } if title == "" && flagPullRequestIssue == "" { @@ -211,34 +192,9 @@ func pullRequest(cmd *Command, args *Args) { } func writePullRequestTitleAndBody(base, head, fullBase, fullHead string, commits []string) (title, body string, err error) { - messageFile, err := git.PullReqMsgFile() - if err != nil { - return - } - defer os.Remove(messageFile) - - err = writePullRequestChanges(base, head, fullBase, fullHead, commits, messageFile) - if err != nil { - return - } - - editor, err := git.Editor() - if err != nil { - return - } - - err = editTitleAndBody(editor, messageFile) - if err != nil { - err = fmt.Errorf("error using text editor for pull request message") - return - } - - title, body, err = readTitleAndBody(messageFile) - if err != nil { - return - } - - return + return utils.GetTitleAndBodyFromEditor(func(messageFile string) error { + writePullRequestChanges(base, head, fullBase, fullHead, commits, messageFile) + }) } func writePullRequestChanges(base, head, fullBase, fullHead string, commits []string, messageFile string) error { @@ -282,87 +238,6 @@ func writePullRequestChanges(base, head, fullBase, fullHead string, commits []st return ioutil.WriteFile(messageFile, []byte(message), 0644) } -func editTitleAndBody(editor, messageFile string) error { - editCmd := cmd.New(editor) - r := regexp.MustCompile("[mg]?vi[m]$") - if r.MatchString(editor) { - editCmd.WithArg("-c") - editCmd.WithArg("set ft=gitcommit tw=0 wrap lbr") - } - editCmd.WithArg(messageFile) - - return editCmd.Exec() -} - -func readTitleAndBody(messageFile string) (title, body string, err error) { - f, err := os.Open(messageFile) - defer f.Close() - if err != nil { - return "", "", err - } - - reader := bufio.NewReader(f) - - return readTitleAndBodyFrom(reader) -} - -func readTitleAndBodyFrom(reader *bufio.Reader) (title, body string, err error) { - r := regexp.MustCompile("\\S") - var titleParts, bodyParts []string - - line, err := readLine(reader) - for err == nil { - if strings.HasPrefix(line, "#") { - break - } - - if len(bodyParts) == 0 && r.MatchString(line) { - titleParts = append(titleParts, line) - } else { - bodyParts = append(bodyParts, line) - } - - line, err = readLine(reader) - } - - if err == io.EOF { - err = nil - } - - title = strings.Join(titleParts, " ") - title = strings.TrimSpace(title) - - body = strings.Join(bodyParts, "\n") - body = strings.TrimSpace(body) - - return -} - -func readLine(r *bufio.Reader) (string, error) { - var ( - isPrefix = true - err error - line, ln []byte - ) - - for isPrefix && err == nil { - line, isPrefix, err = r.ReadLine() - ln = append(ln, line...) - } - - return string(ln), err -} - -func readMsg(msg string) (title, body string) { - split := strings.SplitN(msg, "\n\n", 2) - title = strings.TrimSpace(split[0]) - if len(split) > 1 { - body = strings.TrimSpace(split[1]) - } - - return -} - func parsePullRequestProject(context *github.Project, s string) (p *github.Project, ref string) { p = context ref = s diff --git a/utils/editor.go b/utils/editor.go new file mode 100644 index 0000000..4d43388 --- /dev/null +++ b/utils/editor.go @@ -0,0 +1,142 @@ +package utils + +import ( + "github.com/jingweno/gh/git" + "io/ioutil" + "os" +) + +func GetTitleAndBodyFromFlags(messageFlag, fileFlag string) (title, body string, err error) { + if messageFlag != "" { + title, body = readMsg(messageFlag) + } else if fileFlag != "" { + var ( + content []byte + err error + ) + + if fileFlag == "-" { + content, err = ioutil.ReadAll(os.Stdin) + } else { + content, err = ioutil.ReadFile(fileFlag) + } + utils.Check(err) + title, body = readMsg(string(content)) + } + + return +} + +func GetTitleAndBodyFromEditor(fn func(messageFile string) error) (title, body string, err error) { + messageFile, err := git.PullReqMsgFile() + if err != nil { + return + } + defer os.Remove(messageFile) + + if fn != nil { + err = fn(messageFile) + if err != nil { + return + } + } + + editor, err := git.Editor() + if err != nil { + return + } + + err = editTitleAndBody(editor, messageFile) + if err != nil { + err = fmt.Errorf("error using text editor for release message") + return + } + + title, body, err = readTitleAndBody(messageFile) + if err != nil { + return + } + + return +} + +func editTitleAndBody(editor, messageFile string) error { + editCmd := cmd.New(editor) + r := regexp.MustCompile("[mg]?vi[m]$") + if r.MatchString(editor) { + editCmd.WithArg("-c") + editCmd.WithArg("set ft=gitcommit tw=0 wrap lbr") + } + editCmd.WithArg(messageFile) + + return editCmd.Exec() +} + +func readTitleAndBody(messageFile string) (title, body string, err error) { + f, err := os.Open(messageFile) + defer f.Close() + if err != nil { + return "", "", err + } + + reader := bufio.NewReader(f) + + return readTitleAndBodyFrom(reader) +} + +func readTitleAndBodyFrom(reader *bufio.Reader) (title, body string, err error) { + r := regexp.MustCompile("\\S") + var titleParts, bodyParts []string + + line, err := readLine(reader) + for err == nil { + if strings.HasPrefix(line, "#") { + break + } + + if len(bodyParts) == 0 && r.MatchString(line) { + titleParts = append(titleParts, line) + } else { + bodyParts = append(bodyParts, line) + } + + line, err = readLine(reader) + } + + if err == io.EOF { + err = nil + } + + title = strings.Join(titleParts, " ") + title = strings.TrimSpace(title) + + body = strings.Join(bodyParts, "\n") + body = strings.TrimSpace(body) + + return +} + +func readLine(r *bufio.Reader) (string, error) { + var ( + isPrefix = true + err error + line, ln []byte + ) + + for isPrefix && err == nil { + line, isPrefix, err = r.ReadLine() + ln = append(ln, line...) + } + + return string(ln), err +} + +func readMsg(msg string) (title, body string) { + split := strings.SplitN(msg, "\n\n", 2) + title = strings.TrimSpace(split[0]) + if len(split) > 1 { + body = strings.TrimSpace(split[1]) + } + + return +} From c98cb5b83a42d4bb766d489dd00e63d9cbd4e796 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Thu, 19 Dec 2013 16:55:31 -0800 Subject: [PATCH 03/21] Create a new release based on the local branch. --- commands/release.go | 38 +++++++++++++++++++++++++++++++++----- github/client.go | 15 +++++++++++++++ 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/commands/release.go b/commands/release.go index 1159b8a..9fee843 100644 --- a/commands/release.go +++ b/commands/release.go @@ -17,7 +17,7 @@ var ( cmdRelease = &Command{ Run: release, - Usage: "release TAG [-d] [-p] [-a ]", + Usage: "release TAG [-d] [-p] [-a ] [-m |-f ]", Short: "Create a new release in GitHub", Long: `Create a new release in GitHub for the project that the "origin" remote points to. - It requires the name of the tag to release as a first argument. @@ -28,17 +28,22 @@ var ( flagReleaseDraft, flagReleasePrerelease bool - flagReleaseAssetsDir string + + //flagReleaseAssetsDir, + flagReleaseMessage, + flagReleaseFile string ) func init() { cmdRelease.Flag.BoolVar(&flagReleaseDraft, "d", false, "DRAFT") cmdRelease.Flag.BoolVar(&flagReleasePrerelease, "p", false, "PRERELEASE") cmdRelease.Flag.StringVar(&flagReleaseAssetsDir, "a", "", "ASSETS_DIR") + cmrRelease.Flag.StringVar(&flagReleaseMessage, "m", "", "MESSAGE") + cmrRelease.Flag.StringVar(&flagReleaseFile, "f", "", "FILE") } func releases(cmd *Command, args *Args) { - runInLocalRepo(func(project *github.Project, gh *github.Client) { + runInLocalRepo(func(localRepo *github.LocalRepo, project *github.Project, gh *github.Client) { if args.Noop { fmt.Printf("Would request list of releases for %s\n", project) } else { @@ -57,15 +62,38 @@ func releases(cmd *Command, args *Args) { func release(cmd *Command, args *Args) { tag := args.LastParam() + + runInLocalRepo(func(localRepo *github.GitHubRepo, project *github.Project, gh *github.Client) { + currentBranch, err := localRepo.CurrentBranch() + utils.Check(err) + + title, body, err := utils.GetTitleAndBodyFromFlags(flagReleaseMessage, flagReleaseFile) + utils.Check(err) + + if title == "" { + title, body, err := utils.GetTitleAndBodyFromEditor(nil) + utils.Check(err) + } + + params := octokit.ReleaseParams{ + TagName: tag, + TargetCommitish: currentBranch, + Name: title, + Body: body, + Draft: flagReleaseDraft, + Prerelease: flagReleasePrerelease} + + gh.CreateRelease(project, params) + }) } -func runInLocalRepo(fn func(project *github.Project, client *github.Client)) { +func runInLocalRepo(fn func(localRepo *github.GitHubRepo, project *github.Project, client *github.Client)) { localRepo := github.LocalRepo() project, err := localRepo.CurrentProject() utils.Check(err) client := github.NewClient(project.Host) - fn(project, client) + fn(localRepo, project, client) os.Exit(0) } diff --git a/github/client.go b/github/client.go index 862f9d8..eed1459 100644 --- a/github/client.go +++ b/github/client.go @@ -123,6 +123,21 @@ func (client *Client) Releases(project *Project) (releases []octokit.Release, er return } +func (client *Client) CreateRelease(project *github.Project, params octokit.ReleaseParams) (release octokit.Release, err error) { + url, err := octokit.ReleasesURL.Expand(octokit.M{"owner": project.Owner, "repo": project.Name}) + if err != nil { + return + } + + release, result := client.octokit().Releases(client.requestURL(url)).Create(params) + if result.HasError() { + err = result.Err + return + } + + return +} + func (client *Client) CIStatus(project *Project, sha string) (status *octokit.Status, err error) { url, err := octokit.StatusesURL.Expand(octokit.M{"owner": project.Owner, "repo": project.Name, "ref": sha}) if err != nil { From 3a433ea2ba4a2c4449cb877b8476b8cf1163aa80 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Fri, 20 Dec 2013 10:23:12 -0800 Subject: [PATCH 04/21] Add the release command do the help. --- commands/help.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/commands/help.go b/commands/help.go index c4bd0a4..a84412c 100644 --- a/commands/help.go +++ b/commands/help.go @@ -82,7 +82,8 @@ GitHub Commands: browse Open a GitHub page in the default browser compare Open a compare page on GitHub ci-status Show the CI status of a commit - release Manipulate releases (beta) + releases List releases for this repo (beta) + release Create releases for this repo (beta) issue Manipulate issues (beta) See 'git help ' for more information on a specific command. From 86335254c6185dc2c09b7ca17d01f1e07ee4f025 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Fri, 20 Dec 2013 11:13:50 -0800 Subject: [PATCH 05/21] Move the editor the github package. This avoids double importing issues. --- commands/pull_request.go | 10 +-- commands/pull_request_test.go | 18 ---- github/editor.go | 149 ++++++++++++++++++++++++++++++++++ github/editor_test.go | 24 ++++++ 4 files changed, 176 insertions(+), 25 deletions(-) create mode 100644 github/editor.go create mode 100644 github/editor_test.go diff --git a/commands/pull_request.go b/commands/pull_request.go index 645befd..131097e 100644 --- a/commands/pull_request.go +++ b/commands/pull_request.go @@ -1,15 +1,11 @@ package commands import ( - "bufio" "fmt" - "github.com/jingweno/gh/cmd" "github.com/jingweno/gh/git" "github.com/jingweno/gh/github" "github.com/jingweno/gh/utils" - "io" "io/ioutil" - "os" "reflect" "regexp" "strings" @@ -145,7 +141,7 @@ func pullRequest(cmd *Command, args *Args) { //headProject = github.NewProject("", headProject.Name, headProject.Host) } - title, body := utils.GetTitleAndBodyFromFlags(flagPullRequestMessage, flagPullRequestFile) + title, body, err := github.GetTitleAndBodyFromFlags(flagPullRequestMessage, flagPullRequestFile) utils.Check(err) fullBase := fmt.Sprintf("%s:%s", baseProject.Owner, base) @@ -192,8 +188,8 @@ func pullRequest(cmd *Command, args *Args) { } func writePullRequestTitleAndBody(base, head, fullBase, fullHead string, commits []string) (title, body string, err error) { - return utils.GetTitleAndBodyFromEditor(func(messageFile string) error { - writePullRequestChanges(base, head, fullBase, fullHead, commits, messageFile) + return github.GetTitleAndBodyFromEditor(func(messageFile string) error { + return writePullRequestChanges(base, head, fullBase, fullHead, commits, messageFile) }) } diff --git a/commands/pull_request_test.go b/commands/pull_request_test.go index 94e09fc..a8d4502 100644 --- a/commands/pull_request_test.go +++ b/commands/pull_request_test.go @@ -1,29 +1,11 @@ package commands import ( - "bufio" "github.com/bmizerany/assert" "github.com/jingweno/gh/github" - "strings" "testing" ) -func TestReadTitleAndBody(t *testing.T) { - message := `A title -A title continues - -A body -A body continues -# comment -` - r := strings.NewReader(message) - reader := bufio.NewReader(r) - title, body, err := readTitleAndBodyFrom(reader) - assert.Equal(t, nil, err) - assert.Equal(t, "A title A title continues", title) - assert.Equal(t, "A body\nA body continues", body) -} - func TestParsePullRequestProject(t *testing.T) { c := &github.Project{Host: "github.com", Owner: "jingweno", Name: "gh"} diff --git a/github/editor.go b/github/editor.go new file mode 100644 index 0000000..1008d1d --- /dev/null +++ b/github/editor.go @@ -0,0 +1,149 @@ +package github + +import ( + "bufio" + "fmt" + "github.com/jingweno/gh/cmd" + "github.com/jingweno/gh/git" + "github.com/jingweno/gh/utils" + "io" + "io/ioutil" + "os" + "regexp" + "strings" +) + +func GetTitleAndBodyFromFlags(messageFlag, fileFlag string) (title, body string, err error) { + if messageFlag != "" { + title, body = readMsg(messageFlag) + } else if fileFlag != "" { + var ( + content []byte + err error + ) + + if fileFlag == "-" { + content, err = ioutil.ReadAll(os.Stdin) + } else { + content, err = ioutil.ReadFile(fileFlag) + } + utils.Check(err) + title, body = readMsg(string(content)) + } + + return +} + +func GetTitleAndBodyFromEditor(fn func(messageFile string) error) (title, body string, err error) { + messageFile, err := git.PullReqMsgFile() + if err != nil { + return + } + defer os.Remove(messageFile) + + if fn != nil { + err = fn(messageFile) + if err != nil { + return + } + } + + editor, err := git.Editor() + if err != nil { + return + } + + err = editTitleAndBody(editor, messageFile) + if err != nil { + err = fmt.Errorf("error using text editor for release message") + return + } + + title, body, err = readTitleAndBody(messageFile) + if err != nil { + return + } + + return +} + +func editTitleAndBody(editor, messageFile string) error { + editCmd := cmd.New(editor) + r := regexp.MustCompile("[mg]?vi[m]$") + if r.MatchString(editor) { + editCmd.WithArg("-c") + editCmd.WithArg("set ft=gitcommit tw=0 wrap lbr") + } + editCmd.WithArg(messageFile) + + return editCmd.Exec() +} + +func readTitleAndBody(messageFile string) (title, body string, err error) { + f, err := os.Open(messageFile) + defer f.Close() + if err != nil { + return "", "", err + } + + reader := bufio.NewReader(f) + + return readTitleAndBodyFrom(reader) +} + +func readTitleAndBodyFrom(reader *bufio.Reader) (title, body string, err error) { + r := regexp.MustCompile("\\S") + var titleParts, bodyParts []string + + line, err := readLine(reader) + for err == nil { + if strings.HasPrefix(line, "#") { + break + } + + if len(bodyParts) == 0 && r.MatchString(line) { + titleParts = append(titleParts, line) + } else { + bodyParts = append(bodyParts, line) + } + + line, err = readLine(reader) + } + + if err == io.EOF { + err = nil + } + + title = strings.Join(titleParts, " ") + title = strings.TrimSpace(title) + + body = strings.Join(bodyParts, "\n") + body = strings.TrimSpace(body) + + return +} + +func readLine(r *bufio.Reader) (string, error) { + var ( + isPrefix = true + err error + line, ln []byte + ) + + for isPrefix && err == nil { + line, isPrefix, err = r.ReadLine() + ln = append(ln, line...) + } + + return string(ln), err +} + +func readMsg(msg string) (title, body string) { + split := strings.SplitN(msg, "\n\n", 2) + title = strings.TrimSpace(split[0]) + if len(split) > 1 { + body = strings.TrimSpace(split[1]) + } + + return +} diff --git a/github/editor_test.go b/github/editor_test.go new file mode 100644 index 0000000..91a18e6 --- /dev/null +++ b/github/editor_test.go @@ -0,0 +1,24 @@ +package github + +import ( + "bufio" + "github.com/bmizerany/assert" + "strings" + "testing" +) + +func TestReadTitleAndBody(t *testing.T) { + message := `A title +A title continues + +A body +A body continues +# comment +` + r := strings.NewReader(message) + reader := bufio.NewReader(r) + title, body, err := readTitleAndBodyFrom(reader) + assert.Equal(t, nil, err) + assert.Equal(t, "A title A title continues", title) + assert.Equal(t, "A body\nA body continues", body) +} From c81119a1db121e6f431626ceb5ab349581896605 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Fri, 20 Dec 2013 11:15:41 -0800 Subject: [PATCH 06/21] Add release creation and assets uploading. --- commands/release.go | 60 ++++++++++++++--- commands/release_test.go | 60 +++++++++++++++++ github/client.go | 40 ++++++++++- utils/editor.go | 142 --------------------------------------- utils/utils.go | 14 ++++ utils/utils_test.go | 37 ++++++++++ 6 files changed, 202 insertions(+), 151 deletions(-) create mode 100644 commands/release_test.go delete mode 100644 utils/editor.go diff --git a/commands/release.go b/commands/release.go index 9fee843..fd95583 100644 --- a/commands/release.go +++ b/commands/release.go @@ -4,7 +4,9 @@ import ( "fmt" "github.com/jingweno/gh/github" "github.com/jingweno/gh/utils" + "github.com/jingweno/go-octokit/octokit" "os" + "path/filepath" "strings" ) @@ -29,7 +31,7 @@ var ( flagReleaseDraft, flagReleasePrerelease bool - //flagReleaseAssetsDir, + flagReleaseAssetsDir, flagReleaseMessage, flagReleaseFile string ) @@ -38,12 +40,12 @@ func init() { cmdRelease.Flag.BoolVar(&flagReleaseDraft, "d", false, "DRAFT") cmdRelease.Flag.BoolVar(&flagReleasePrerelease, "p", false, "PRERELEASE") cmdRelease.Flag.StringVar(&flagReleaseAssetsDir, "a", "", "ASSETS_DIR") - cmrRelease.Flag.StringVar(&flagReleaseMessage, "m", "", "MESSAGE") - cmrRelease.Flag.StringVar(&flagReleaseFile, "f", "", "FILE") + cmdRelease.Flag.StringVar(&flagReleaseMessage, "m", "", "MESSAGE") + cmdRelease.Flag.StringVar(&flagReleaseFile, "f", "", "FILE") } func releases(cmd *Command, args *Args) { - runInLocalRepo(func(localRepo *github.LocalRepo, project *github.Project, gh *github.Client) { + runInLocalRepo(func(localRepo *github.GitHubRepo, project *github.Project, gh *github.Client) { if args.Noop { fmt.Printf("Would request list of releases for %s\n", project) } else { @@ -63,27 +65,33 @@ func releases(cmd *Command, args *Args) { func release(cmd *Command, args *Args) { tag := args.LastParam() + assetsDir, err := getAssetsDirectory(flagReleaseAssetsDir, tag) + utils.Check(err) + runInLocalRepo(func(localRepo *github.GitHubRepo, project *github.Project, gh *github.Client) { currentBranch, err := localRepo.CurrentBranch() utils.Check(err) - title, body, err := utils.GetTitleAndBodyFromFlags(flagReleaseMessage, flagReleaseFile) + title, body, err := github.GetTitleAndBodyFromFlags(flagReleaseMessage, flagReleaseFile) utils.Check(err) if title == "" { - title, body, err := utils.GetTitleAndBodyFromEditor(nil) + title, body, err = github.GetTitleAndBodyFromEditor(nil) utils.Check(err) } params := octokit.ReleaseParams{ TagName: tag, - TargetCommitish: currentBranch, + TargetCommitish: currentBranch.ShortName(), Name: title, Body: body, Draft: flagReleaseDraft, Prerelease: flagReleasePrerelease} - gh.CreateRelease(project, params) + finalRelease, err := gh.CreateRelease(project, params) + utils.Check(err) + + uploadReleaseAssets(gh, finalRelease, assetsDir) }) } @@ -97,3 +105,39 @@ func runInLocalRepo(fn func(localRepo *github.GitHubRepo, project *github.Projec os.Exit(0) } + +func getAssetsDirectory(assetsDir, tag string) (string, error) { + if assetsDir == "" { + pwd, err := os.Getwd() + utils.Check(err) + + assetsDir = filepath.Join(pwd, "releases", tag) + } + + if !utils.IsDir(assetsDir) { + return "", fmt.Errorf("The assets directory doesn't exist: %s", assetsDir) + } + + if utils.IsEmptyDir(assetsDir) { + return "", fmt.Errorf("The assets directory is empty: %s", assetsDir) + } + + return assetsDir, nil +} + +func uploadReleaseAssets(gh *github.Client, release *octokit.Release, assetsDir string) { + filepath.Walk(assetsDir, func(path string, fi os.FileInfo, err error) error { + if !fi.IsDir() { + fmt.Printf("- Uploading asset %s\n", fi.Name()) + + file, err := os.Open(path) + utils.Check(err) + defer file.Close() + + err = gh.UploadReleaseAsset(release, file, fi) + utils.Check(err) + } + + return nil + }) +} diff --git a/commands/release_test.go b/commands/release_test.go new file mode 100644 index 0000000..3d23871 --- /dev/null +++ b/commands/release_test.go @@ -0,0 +1,60 @@ +package commands + +import ( + "github.com/bmizerany/assert" + "io/ioutil" + "os" + "path/filepath" + "testing" +) + +func TestAssetsDirWithoutFlag(t *testing.T) { + dir := createTempDir(t) + pwd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + defer func() { + os.Chdir(pwd) + os.RemoveAll(dir) + }() + + os.Chdir(dir) + + tagDir := filepath.Join(dir, "releases", "v1.0.0") + assertAssetsDirSelected(t, tagDir, "") +} + +func TestAssetsDirWithFlag(t *testing.T) { + dir := createTempDir(t) + defer os.RemoveAll(dir) + + tagDir := filepath.Join(dir, "releases", "v1.0.0") + assertAssetsDirSelected(t, tagDir, tagDir) +} + +func assertAssetsDirSelected(t *testing.T, expectedDir, flagDir string) { + assets, err := getAssetsDirectory(flagDir, "v1.0.0") + assert.NotEqual(t, nil, err) // Error if it doesn't exist + + os.MkdirAll(expectedDir, 0755) + assets, err = getAssetsDirectory(flagDir, "v1.0.0") + assert.NotEqual(t, nil, err) // Error if it's empty + + ioutil.TempFile(expectedDir, "gh-test") + assets, err = getAssetsDirectory(flagDir, "v1.0.0") + + fiExpected, err := os.Stat(expectedDir) + fiAssets, err := os.Stat(assets) + + assert.Equal(t, nil, err) + assert.T(t, os.SameFile(fiExpected, fiAssets)) +} + +func createTempDir(t *testing.T) string { + dir, err := ioutil.TempDir("", "gh-test-") + if err != nil { + t.Fatal(err) + } + return dir +} diff --git a/github/client.go b/github/client.go index eed1459..dc1e808 100644 --- a/github/client.go +++ b/github/client.go @@ -2,7 +2,10 @@ package github import ( "fmt" + "github.com/jingweno/gh/utils" "github.com/jingweno/go-octokit/octokit" + "io/ioutil" + "net/http" "net/url" "os" ) @@ -123,7 +126,7 @@ func (client *Client) Releases(project *Project) (releases []octokit.Release, er return } -func (client *Client) CreateRelease(project *github.Project, params octokit.ReleaseParams) (release octokit.Release, err error) { +func (client *Client) CreateRelease(project *Project, params octokit.ReleaseParams) (release *octokit.Release, err error) { url, err := octokit.ReleasesURL.Expand(octokit.M{"owner": project.Owner, "repo": project.Name}) if err != nil { return @@ -138,6 +141,41 @@ func (client *Client) CreateRelease(project *github.Project, params octokit.Rele return } +func (client *Client) UploadReleaseAsset(release *octokit.Release, asset *os.File, fi os.FileInfo) (err error) { + uploadUrl, err := octokit.Hyperlink(release.UploadURL).Expand(octokit.M{"name": fi.Name()}) + utils.Check(err) + + c := client.octokit() + + content, err := ioutil.ReadAll(asset) + utils.Check(err) + + contentType := http.DetectContentType(content) + + fmt.Printf("-- Uploading %s to %s\n", contentType, uploadUrl.String()) + request, err := http.NewRequest("POST", uploadUrl.String(), asset) + utils.Check(err) + + request.Header.Add("Content-Type", contentType) + + if c.AuthMethod != nil { + request.Header.Add("Authorization", c.AuthMethod.String()) + } + + if basicAuth, ok := c.AuthMethod.(octokit.BasicAuth); ok && basicAuth.OneTimePassword != "" { + request.Header.Add("X-GitHub-OTP", basicAuth.OneTimePassword) + } + + httpClient := &http.Client{} + response, err := httpClient.Do(request) + utils.Check(err) + + if response.Status != "201" { + return fmt.Errorf("Error uploading the release asset, status %s", response.Status) + } + return nil +} + func (client *Client) CIStatus(project *Project, sha string) (status *octokit.Status, err error) { url, err := octokit.StatusesURL.Expand(octokit.M{"owner": project.Owner, "repo": project.Name, "ref": sha}) if err != nil { diff --git a/utils/editor.go b/utils/editor.go deleted file mode 100644 index 4d43388..0000000 --- a/utils/editor.go +++ /dev/null @@ -1,142 +0,0 @@ -package utils - -import ( - "github.com/jingweno/gh/git" - "io/ioutil" - "os" -) - -func GetTitleAndBodyFromFlags(messageFlag, fileFlag string) (title, body string, err error) { - if messageFlag != "" { - title, body = readMsg(messageFlag) - } else if fileFlag != "" { - var ( - content []byte - err error - ) - - if fileFlag == "-" { - content, err = ioutil.ReadAll(os.Stdin) - } else { - content, err = ioutil.ReadFile(fileFlag) - } - utils.Check(err) - title, body = readMsg(string(content)) - } - - return -} - -func GetTitleAndBodyFromEditor(fn func(messageFile string) error) (title, body string, err error) { - messageFile, err := git.PullReqMsgFile() - if err != nil { - return - } - defer os.Remove(messageFile) - - if fn != nil { - err = fn(messageFile) - if err != nil { - return - } - } - - editor, err := git.Editor() - if err != nil { - return - } - - err = editTitleAndBody(editor, messageFile) - if err != nil { - err = fmt.Errorf("error using text editor for release message") - return - } - - title, body, err = readTitleAndBody(messageFile) - if err != nil { - return - } - - return -} - -func editTitleAndBody(editor, messageFile string) error { - editCmd := cmd.New(editor) - r := regexp.MustCompile("[mg]?vi[m]$") - if r.MatchString(editor) { - editCmd.WithArg("-c") - editCmd.WithArg("set ft=gitcommit tw=0 wrap lbr") - } - editCmd.WithArg(messageFile) - - return editCmd.Exec() -} - -func readTitleAndBody(messageFile string) (title, body string, err error) { - f, err := os.Open(messageFile) - defer f.Close() - if err != nil { - return "", "", err - } - - reader := bufio.NewReader(f) - - return readTitleAndBodyFrom(reader) -} - -func readTitleAndBodyFrom(reader *bufio.Reader) (title, body string, err error) { - r := regexp.MustCompile("\\S") - var titleParts, bodyParts []string - - line, err := readLine(reader) - for err == nil { - if strings.HasPrefix(line, "#") { - break - } - - if len(bodyParts) == 0 && r.MatchString(line) { - titleParts = append(titleParts, line) - } else { - bodyParts = append(bodyParts, line) - } - - line, err = readLine(reader) - } - - if err == io.EOF { - err = nil - } - - title = strings.Join(titleParts, " ") - title = strings.TrimSpace(title) - - body = strings.Join(bodyParts, "\n") - body = strings.TrimSpace(body) - - return -} - -func readLine(r *bufio.Reader) (string, error) { - var ( - isPrefix = true - err error - line, ln []byte - ) - - for isPrefix && err == nil { - line, isPrefix, err = r.ReadLine() - ln = append(ln, line...) - } - - return string(ln), err -} - -func readMsg(msg string) (title, body string) { - split := strings.SplitN(msg, "\n\n", 2) - title = strings.TrimSpace(split[0]) - if len(split) > 1 { - body = strings.TrimSpace(split[1]) - } - - return -} diff --git a/utils/utils.go b/utils/utils.go index bf93ba1..e77f7a1 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -64,3 +64,17 @@ func DirName() (string, error) { name = strings.Replace(name, " ", "-", -1) return name, nil } + +func IsDir(path string) bool { + fi, err := os.Stat(path) + if err != nil || !fi.IsDir() { + return false + } + return true +} + +func IsEmptyDir(path string) bool { + fullPath := filepath.Join(path, "*") + match, _ := filepath.Glob(fullPath) + return match == nil +} diff --git a/utils/utils_test.go b/utils/utils_test.go index f54ba0c..2a79762 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -2,6 +2,8 @@ package utils import ( "github.com/bmizerany/assert" + "io/ioutil" + "os" "testing" ) @@ -16,3 +18,38 @@ func TestSearchBrowserLauncher(t *testing.T) { func TestConcatPaths(t *testing.T) { assert.Equal(t, "foo/bar/baz", ConcatPaths("foo", "bar", "baz")) } + +func TestIsDir(t *testing.T) { + dir := createTempDir(t) + defer os.RemoveAll(dir) + + assert.T(t, IsDir(dir)) + + file, _ := ioutil.TempFile(dir, "gh-utils-test-") + assert.Equal(t, false, IsDir(file.Name())) + + assert.Equal(t, false, IsDir("")) +} + +func TestDirIsNotEmpty(t *testing.T) { + dir := createTempDir(t) + defer os.RemoveAll(dir) + ioutil.TempFile(dir, "gh-utils-test-") + + assert.Equal(t, false, IsEmptyDir(dir)) +} + +func TestDirIsEmpty(t *testing.T) { + dir := createTempDir(t) + defer os.RemoveAll(dir) + + assert.T(t, IsEmptyDir(dir)) +} + +func createTempDir(t *testing.T) string { + dir, err := ioutil.TempDir("", "gh-utils-test-") + if err != nil { + t.Fatal(err) + } + return dir +} From 3ba86a3274c94ce55c9ca0e6747c0a1bad3ffe7f Mon Sep 17 00:00:00 2001 From: David Calavera Date: Fri, 20 Dec 2013 14:22:02 -0800 Subject: [PATCH 07/21] Read only part of the asset to detect the content type. --- commands/release.go | 28 ++++++++++++++++++++++++++-- github/client.go | 14 ++++---------- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/commands/release.go b/commands/release.go index fd95583..f44f3d8 100644 --- a/commands/release.go +++ b/commands/release.go @@ -1,10 +1,13 @@ package commands import ( + "bytes" "fmt" "github.com/jingweno/gh/github" "github.com/jingweno/gh/utils" "github.com/jingweno/go-octokit/octokit" + "io" + "net/http" "os" "path/filepath" "strings" @@ -92,6 +95,8 @@ func release(cmd *Command, args *Args) { utils.Check(err) uploadReleaseAssets(gh, finalRelease, assetsDir) + + fmt.Printf("Release created: %s", finalRelease.HTMLURL) }) } @@ -128,16 +133,35 @@ func getAssetsDirectory(assetsDir, tag string) (string, error) { func uploadReleaseAssets(gh *github.Client, release *octokit.Release, assetsDir string) { filepath.Walk(assetsDir, func(path string, fi os.FileInfo, err error) error { if !fi.IsDir() { - fmt.Printf("- Uploading asset %s\n", fi.Name()) + contentType := detectContentType(path, fi) file, err := os.Open(path) utils.Check(err) defer file.Close() - err = gh.UploadReleaseAsset(release, file, fi) + err = gh.UploadReleaseAsset(release, file, fi, contentType) utils.Check(err) } return nil }) } + +func detectContentType(path string, fi os.FileInfo) string { + file, err := os.Open(path) + utils.Check(err) + defer file.Close() + + fileHeader := &bytes.Buffer{} + headerSize := int64(512) + if fi.Size() < headerSize { + headerSize = fi.Size() + } + + // The content type detection only uses 512 bytes at most. + // This way we avoid copying the whole content for big files. + _, err = io.CopyN(fileHeader, file, headerSize) + utils.Check(err) + + return http.DetectContentType(fileHeader.Bytes()) +} diff --git a/github/client.go b/github/client.go index dc1e808..fdea628 100644 --- a/github/client.go +++ b/github/client.go @@ -4,7 +4,6 @@ import ( "fmt" "github.com/jingweno/gh/utils" "github.com/jingweno/go-octokit/octokit" - "io/ioutil" "net/http" "net/url" "os" @@ -141,22 +140,17 @@ func (client *Client) CreateRelease(project *Project, params octokit.ReleasePara return } -func (client *Client) UploadReleaseAsset(release *octokit.Release, asset *os.File, fi os.FileInfo) (err error) { +func (client *Client) UploadReleaseAsset(release *octokit.Release, asset *os.File, fi os.FileInfo, contentType string) (err error) { uploadUrl, err := octokit.Hyperlink(release.UploadURL).Expand(octokit.M{"name": fi.Name()}) utils.Check(err) c := client.octokit() - content, err := ioutil.ReadAll(asset) - utils.Check(err) - - contentType := http.DetectContentType(content) - - fmt.Printf("-- Uploading %s to %s\n", contentType, uploadUrl.String()) request, err := http.NewRequest("POST", uploadUrl.String(), asset) utils.Check(err) request.Header.Add("Content-Type", contentType) + request.ContentLength = fi.Size() if c.AuthMethod != nil { request.Header.Add("Authorization", c.AuthMethod.String()) @@ -170,8 +164,8 @@ func (client *Client) UploadReleaseAsset(release *octokit.Release, asset *os.Fil response, err := httpClient.Do(request) utils.Check(err) - if response.Status != "201" { - return fmt.Errorf("Error uploading the release asset, status %s", response.Status) + if response.StatusCode != 201 { + return fmt.Errorf("Error uploading the release asset %s, status %s", fi.Name(), response.Status) } return nil } From 54bc22d38f60c543bbedd51dc801faac389efda0 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Fri, 20 Dec 2013 15:19:00 -0800 Subject: [PATCH 08/21] Invert flags order. --- commands/release.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/release.go b/commands/release.go index f44f3d8..bccc372 100644 --- a/commands/release.go +++ b/commands/release.go @@ -22,7 +22,7 @@ var ( cmdRelease = &Command{ Run: release, - Usage: "release TAG [-d] [-p] [-a ] [-m |-f ]", + Usage: "release [-d] [-p] [-a ] [-m |-f ] TAG", Short: "Create a new release in GitHub", Long: `Create a new release in GitHub for the project that the "origin" remote points to. - It requires the name of the tag to release as a first argument. From a57b44572f3b5badc6404adbf087d1a337a3c7c4 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Fri, 20 Dec 2013 15:22:53 -0800 Subject: [PATCH 09/21] Remove double status output. --- github/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/github/client.go b/github/client.go index fdea628..97e377f 100644 --- a/github/client.go +++ b/github/client.go @@ -165,7 +165,7 @@ func (client *Client) UploadReleaseAsset(release *octokit.Release, asset *os.Fil utils.Check(err) if response.StatusCode != 201 { - return fmt.Errorf("Error uploading the release asset %s, status %s", fi.Name(), response.Status) + return fmt.Errorf("Error uploading the release asset %s, %s", fi.Name(), response.Status) } return nil } From 9a11e930b24ba15d99b3cba3649250ddc247019b Mon Sep 17 00:00:00 2001 From: David Calavera Date: Fri, 20 Dec 2013 15:27:03 -0800 Subject: [PATCH 10/21] Use a more generic message when the edit fail. --- features/pull_request.feature | 2 +- github/editor.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/features/pull_request.feature b/features/pull_request.feature index ee21f2a..34d1c79 100644 --- a/features/pull_request.feature +++ b/features/pull_request.feature @@ -160,7 +160,7 @@ Feature: hub pull-request Given the text editor exits with error status And an empty file named ".git/PULLREQ_EDITMSG" When I run `hub pull-request` - Then the stderr should contain "error using text editor for pull request message" + Then the stderr should contain "error using text editor for title/body message" And the exit status should be 1 And the file ".git/PULLREQ_EDITMSG" should not exist diff --git a/github/editor.go b/github/editor.go index 1008d1d..1c95256 100644 --- a/github/editor.go +++ b/github/editor.go @@ -55,7 +55,7 @@ func GetTitleAndBodyFromEditor(fn func(messageFile string) error) (title, body s err = editTitleAndBody(editor, messageFile) if err != nil { - err = fmt.Errorf("error using text editor for release message") + err = fmt.Errorf("error using text editor for title/body message") return } From db71cf2a15689762deb1f9cf8f0bf7a2628462f0 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Fri, 20 Dec 2013 15:39:10 -0800 Subject: [PATCH 11/21] Write a message in the editor showing what you're doing. --- commands/release.go | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/commands/release.go b/commands/release.go index bccc372..232f58d 100644 --- a/commands/release.go +++ b/commands/release.go @@ -7,6 +7,7 @@ import ( "github.com/jingweno/gh/utils" "github.com/jingweno/go-octokit/octokit" "io" + "io/ioutil" "net/http" "os" "path/filepath" @@ -74,18 +75,19 @@ func release(cmd *Command, args *Args) { runInLocalRepo(func(localRepo *github.GitHubRepo, project *github.Project, gh *github.Client) { currentBranch, err := localRepo.CurrentBranch() utils.Check(err) + branchName := currentBranch.ShortName() title, body, err := github.GetTitleAndBodyFromFlags(flagReleaseMessage, flagReleaseFile) utils.Check(err) if title == "" { - title, body, err = github.GetTitleAndBodyFromEditor(nil) + title, body, err = writeReleaseTitleAndBody(project, tag, branchName) utils.Check(err) } params := octokit.ReleaseParams{ TagName: tag, - TargetCommitish: currentBranch.ShortName(), + TargetCommitish: branchName, Name: title, Body: body, Draft: flagReleaseDraft, @@ -100,6 +102,20 @@ func release(cmd *Command, args *Args) { }) } +func writeReleaseTitleAndBody(project *github.Project, tag, currentBranch string) (string, string, error) { + return github.GetTitleAndBodyFromEditor(func(messageFile string) error { + message := ` +# Creating release %s for %s from %s +# +# Write a message for this release. The first block +# of the text is the title and the rest is description. +` + message = fmt.Sprintf(message, tag, project.Name, currentBranch) + + return ioutil.WriteFile(messageFile, []byte(message), 0644) + }) +} + func runInLocalRepo(fn func(localRepo *github.GitHubRepo, project *github.Project, client *github.Client)) { localRepo := github.LocalRepo() project, err := localRepo.CurrentProject() From 26b32b7491ae6b7c972f71650de86046516a53af Mon Sep 17 00:00:00 2001 From: David Calavera Date: Fri, 20 Dec 2013 15:48:21 -0800 Subject: [PATCH 12/21] Use WaitGroup to upload each asset in a different goroutine. --- commands/release.go | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/commands/release.go b/commands/release.go index 232f58d..782472c 100644 --- a/commands/release.go +++ b/commands/release.go @@ -12,6 +12,7 @@ import ( "os" "path/filepath" "strings" + "sync" ) var ( @@ -147,20 +148,29 @@ func getAssetsDirectory(assetsDir, tag string) (string, error) { } func uploadReleaseAssets(gh *github.Client, release *octokit.Release, assetsDir string) { + var wg sync.WaitGroup + filepath.Walk(assetsDir, func(path string, fi os.FileInfo, err error) error { if !fi.IsDir() { - contentType := detectContentType(path, fi) + wg.Add(1) - file, err := os.Open(path) - utils.Check(err) - defer file.Close() + go func() { + defer wg.Done() + contentType := detectContentType(path, fi) - err = gh.UploadReleaseAsset(release, file, fi, contentType) - utils.Check(err) + file, err := os.Open(path) + utils.Check(err) + defer file.Close() + + err = gh.UploadReleaseAsset(release, file, fi, contentType) + utils.Check(err) + }() } return nil }) + + wg.Wait() } func detectContentType(path string, fi os.FileInfo) string { From 1f711d911b0e4b7e1294709c6b113b5e10fd1550 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Fri, 20 Dec 2013 16:41:14 -0800 Subject: [PATCH 13/21] Remove double isDir helper. --- commands/release.go | 2 +- utils/utils.go | 8 -------- utils/utils_test.go | 12 ------------ 3 files changed, 1 insertion(+), 21 deletions(-) diff --git a/commands/release.go b/commands/release.go index 782472c..93c4c96 100644 --- a/commands/release.go +++ b/commands/release.go @@ -136,7 +136,7 @@ func getAssetsDirectory(assetsDir, tag string) (string, error) { assetsDir = filepath.Join(pwd, "releases", tag) } - if !utils.IsDir(assetsDir) { + if !isDir(assetsDir) { return "", fmt.Errorf("The assets directory doesn't exist: %s", assetsDir) } diff --git a/utils/utils.go b/utils/utils.go index e77f7a1..0011540 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -65,14 +65,6 @@ func DirName() (string, error) { return name, nil } -func IsDir(path string) bool { - fi, err := os.Stat(path) - if err != nil || !fi.IsDir() { - return false - } - return true -} - func IsEmptyDir(path string) bool { fullPath := filepath.Join(path, "*") match, _ := filepath.Glob(fullPath) diff --git a/utils/utils_test.go b/utils/utils_test.go index 2a79762..dc8ed48 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -19,18 +19,6 @@ func TestConcatPaths(t *testing.T) { assert.Equal(t, "foo/bar/baz", ConcatPaths("foo", "bar", "baz")) } -func TestIsDir(t *testing.T) { - dir := createTempDir(t) - defer os.RemoveAll(dir) - - assert.T(t, IsDir(dir)) - - file, _ := ioutil.TempFile(dir, "gh-utils-test-") - assert.Equal(t, false, IsDir(file.Name())) - - assert.Equal(t, false, IsDir("")) -} - func TestDirIsNotEmpty(t *testing.T) { dir := createTempDir(t) defer os.RemoveAll(dir) From 4010d6ffcc5af64a83bff9ba5cb08929674cb173 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Fri, 20 Dec 2013 16:45:48 -0800 Subject: [PATCH 14/21] Move `isEmptyDir` to the command utilities. Removes duplicated test helper. --- commands/release.go | 2 +- commands/release_test.go | 8 -------- commands/utils.go | 7 +++++++ commands/utils_test.go | 31 +++++++++++++++++++++++++++++++ utils/utils.go | 6 ------ utils/utils_test.go | 25 ------------------------- 6 files changed, 39 insertions(+), 40 deletions(-) create mode 100644 commands/utils_test.go diff --git a/commands/release.go b/commands/release.go index 93c4c96..0479930 100644 --- a/commands/release.go +++ b/commands/release.go @@ -140,7 +140,7 @@ func getAssetsDirectory(assetsDir, tag string) (string, error) { return "", fmt.Errorf("The assets directory doesn't exist: %s", assetsDir) } - if utils.IsEmptyDir(assetsDir) { + if isEmptyDir(assetsDir) { return "", fmt.Errorf("The assets directory is empty: %s", assetsDir) } diff --git a/commands/release_test.go b/commands/release_test.go index 3d23871..1225fa6 100644 --- a/commands/release_test.go +++ b/commands/release_test.go @@ -50,11 +50,3 @@ func assertAssetsDirSelected(t *testing.T, expectedDir, flagDir string) { assert.Equal(t, nil, err) assert.T(t, os.SameFile(fiExpected, fiAssets)) } - -func createTempDir(t *testing.T) string { - dir, err := ioutil.TempDir("", "gh-test-") - if err != nil { - t.Fatal(err) - } - return dir -} diff --git a/commands/utils.go b/commands/utils.go index ce9c47f..0374271 100644 --- a/commands/utils.go +++ b/commands/utils.go @@ -5,6 +5,7 @@ import ( "github.com/jingweno/gh/utils" "github.com/jingweno/go-octokit/octokit" "os" + "path/filepath" "strings" ) @@ -46,3 +47,9 @@ func hasGitRemote(name string) bool { return false } + +func isEmptyDir(path string) bool { + fullPath := filepath.Join(path, "*") + match, _ := filepath.Glob(fullPath) + return match == nil +} diff --git a/commands/utils_test.go b/commands/utils_test.go new file mode 100644 index 0000000..f5131a6 --- /dev/null +++ b/commands/utils_test.go @@ -0,0 +1,31 @@ +package commands + +import ( + "github.com/bmizerany/assert" + "io/ioutil" + "os" + "testing" +) + +func TestDirIsNotEmpty(t *testing.T) { + dir := createTempDir(t) + defer os.RemoveAll(dir) + ioutil.TempFile(dir, "gh-utils-test-") + + assert.Equal(t, false, isEmptyDir(dir)) +} + +func TestDirIsEmpty(t *testing.T) { + dir := createTempDir(t) + defer os.RemoveAll(dir) + + assert.T(t, isEmptyDir(dir)) +} + +func createTempDir(t *testing.T) string { + dir, err := ioutil.TempDir("", "gh-utils-test-") + if err != nil { + t.Fatal(err) + } + return dir +} diff --git a/utils/utils.go b/utils/utils.go index 0011540..bf93ba1 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -64,9 +64,3 @@ func DirName() (string, error) { name = strings.Replace(name, " ", "-", -1) return name, nil } - -func IsEmptyDir(path string) bool { - fullPath := filepath.Join(path, "*") - match, _ := filepath.Glob(fullPath) - return match == nil -} diff --git a/utils/utils_test.go b/utils/utils_test.go index dc8ed48..f54ba0c 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -2,8 +2,6 @@ package utils import ( "github.com/bmizerany/assert" - "io/ioutil" - "os" "testing" ) @@ -18,26 +16,3 @@ func TestSearchBrowserLauncher(t *testing.T) { func TestConcatPaths(t *testing.T) { assert.Equal(t, "foo/bar/baz", ConcatPaths("foo", "bar", "baz")) } - -func TestDirIsNotEmpty(t *testing.T) { - dir := createTempDir(t) - defer os.RemoveAll(dir) - ioutil.TempFile(dir, "gh-utils-test-") - - assert.Equal(t, false, IsEmptyDir(dir)) -} - -func TestDirIsEmpty(t *testing.T) { - dir := createTempDir(t) - defer os.RemoveAll(dir) - - assert.T(t, IsEmptyDir(dir)) -} - -func createTempDir(t *testing.T) string { - dir, err := ioutil.TempDir("", "gh-utils-test-") - if err != nil { - t.Fatal(err) - } - return dir -} From 95e234a64daf037b41b6a6f368afeb8342b664da Mon Sep 17 00:00:00 2001 From: David Calavera Date: Mon, 23 Dec 2013 15:58:28 -0800 Subject: [PATCH 15/21] Assert directory truthy. --- commands/utils_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/utils_test.go b/commands/utils_test.go index f5131a6..741f745 100644 --- a/commands/utils_test.go +++ b/commands/utils_test.go @@ -12,7 +12,7 @@ func TestDirIsNotEmpty(t *testing.T) { defer os.RemoveAll(dir) ioutil.TempFile(dir, "gh-utils-test-") - assert.Equal(t, false, isEmptyDir(dir)) + assert.T(t, !isEmptyDir(dir)) } func TestDirIsEmpty(t *testing.T) { From b436b8db5fb978db5422058621cde1cda5b1fd70 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Mon, 23 Dec 2013 15:59:44 -0800 Subject: [PATCH 16/21] Do not double return errors. --- github/client.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/github/client.go b/github/client.go index 97e377f..4ad6933 100644 --- a/github/client.go +++ b/github/client.go @@ -165,9 +165,9 @@ func (client *Client) UploadReleaseAsset(release *octokit.Release, asset *os.Fil utils.Check(err) if response.StatusCode != 201 { - return fmt.Errorf("Error uploading the release asset %s, %s", fi.Name(), response.Status) + err = fmt.Errorf("Error uploading the release asset %s, %s", fi.Name(), response.Status) } - return nil + return } func (client *Client) CIStatus(project *Project, sha string) (status *octokit.Status, err error) { From 248a2043e59ea715f00587c3788b8123fa189c89 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Mon, 23 Dec 2013 21:37:56 -0800 Subject: [PATCH 17/21] Identify each message editor separatedly. --- commands/pull_request.go | 2 +- commands/release.go | 2 +- git/git.go | 9 --------- git/git_test.go | 5 ----- github/editor.go | 14 ++++++++++++-- github/editor_test.go | 5 +++++ 6 files changed, 19 insertions(+), 18 deletions(-) diff --git a/commands/pull_request.go b/commands/pull_request.go index 131097e..cf8e023 100644 --- a/commands/pull_request.go +++ b/commands/pull_request.go @@ -188,7 +188,7 @@ func pullRequest(cmd *Command, args *Args) { } func writePullRequestTitleAndBody(base, head, fullBase, fullHead string, commits []string) (title, body string, err error) { - return github.GetTitleAndBodyFromEditor(func(messageFile string) error { + return github.GetTitleAndBodyFromEditor("PULLREQ", func(messageFile string) error { return writePullRequestChanges(base, head, fullBase, fullHead, commits, messageFile) }) } diff --git a/commands/release.go b/commands/release.go index 0479930..ff40cc0 100644 --- a/commands/release.go +++ b/commands/release.go @@ -104,7 +104,7 @@ func release(cmd *Command, args *Args) { } func writeReleaseTitleAndBody(project *github.Project, tag, currentBranch string) (string, string, error) { - return github.GetTitleAndBodyFromEditor(func(messageFile string) error { + return github.GetTitleAndBodyFromEditor("RELEASE", func(messageFile string) error { message := ` # Creating release %s for %s from %s # diff --git a/git/git.go b/git/git.go index ba89889..707fbdd 100644 --- a/git/git.go +++ b/git/git.go @@ -31,15 +31,6 @@ func Dir() (string, error) { return gitDir, nil } -func PullReqMsgFile() (string, error) { - gitDir, err := Dir() - if err != nil { - return "", err - } - - return filepath.Join(gitDir, "PULLREQ_EDITMSG"), nil -} - func Editor() (string, error) { output, err := execGitCmd("var", "GIT_EDITOR") if err != nil { diff --git a/git/git_test.go b/git/git_test.go index cced2d9..f0da24b 100644 --- a/git/git_test.go +++ b/git/git_test.go @@ -11,11 +11,6 @@ func TestGitDir(t *testing.T) { assert.T(t, strings.Contains(gitDir, ".git")) } -func TestGitPullReqMsgFile(t *testing.T) { - gitPullReqMsgFile, _ := PullReqMsgFile() - assert.T(t, strings.Contains(gitPullReqMsgFile, "PULLREQ_EDITMSG")) -} - func TestGitEditor(t *testing.T) { gitEditor, err := Editor() if err == nil { diff --git a/github/editor.go b/github/editor.go index 1c95256..8c0547b 100644 --- a/github/editor.go +++ b/github/editor.go @@ -9,6 +9,7 @@ import ( "io" "io/ioutil" "os" + "path/filepath" "regexp" "strings" ) @@ -34,8 +35,8 @@ func GetTitleAndBodyFromFlags(messageFlag, fileFlag string) (title, body string, return } -func GetTitleAndBodyFromEditor(fn func(messageFile string) error) (title, body string, err error) { - messageFile, err := git.PullReqMsgFile() +func GetTitleAndBodyFromEditor(about string, fn func(messageFile string) error) (title, body string, err error) { + messageFile, err := getMessageFile(about) if err != nil { return } @@ -147,3 +148,12 @@ func readMsg(msg string) (title, body string) { return } + +func getMessageFile(about string) (string, error) { + gitDir, err := git.Dir() + if err != nil { + return "", err + } + + return filepath.Join(gitDir, fmt.Sprintf("%s_EDITMSG", about)), nil +} diff --git a/github/editor_test.go b/github/editor_test.go index 91a18e6..bf10072 100644 --- a/github/editor_test.go +++ b/github/editor_test.go @@ -22,3 +22,8 @@ A body continues assert.Equal(t, "A title A title continues", title) assert.Equal(t, "A body\nA body continues", body) } + +func TestGetMessageFile(t *testing.T) { + gitPullReqMsgFile, _ := getMessageFile("PULLREQ") + assert.T(t, strings.Contains(gitPullReqMsgFile, "PULLREQ_EDITMSG")) +} From 216f3ee41fcf29b56a6f87d3b816191a6a40c14f Mon Sep 17 00:00:00 2001 From: David Calavera Date: Tue, 24 Dec 2013 13:31:24 -0800 Subject: [PATCH 18/21] Update go-octokit. --- Godeps/Godeps.json | 2 +- .../jingweno/go-octokit/octokit/client.go | 60 +++++++++++--- .../go-octokit/octokit/octokit_test.go | 4 + .../jingweno/go-octokit/octokit/releases.go | 2 +- .../go-octokit/octokit/releases_test.go | 2 +- .../jingweno/go-octokit/octokit/request.go | 78 +++++++------------ .../jingweno/go-octokit/octokit/response.go | 17 ++++ .../jingweno/go-octokit/octokit/uploads.go | 20 +++++ .../go-octokit/octokit/uploads_test.go | 41 ++++++++++ 9 files changed, 160 insertions(+), 66 deletions(-) create mode 100644 Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/uploads.go create mode 100644 Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/uploads_test.go diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 5702c1a..8abc166 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -36,7 +36,7 @@ { "ImportPath": "github.com/jingweno/go-octokit/octokit", "Comment": "v0.4.0-41-g85bc6b5", - "Rev": "85bc6b536f8e8cb24d4db262e4dfea3d7d2d8138" + "Rev": "74f0495a72d8a2dfce059ebf718fd60dd3800f41" }, { "ImportPath": "github.com/jtacoma/uritemplates", diff --git a/Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/client.go b/Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/client.go index 2a98dbd..bdfe83c 100644 --- a/Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/client.go +++ b/Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/client.go @@ -5,6 +5,7 @@ import ( "github.com/lostisland/go-sawyer/hypermedia" "net/http" "net/url" + "os" ) func NewClient(authMethod AuthMethod) *Client { @@ -24,21 +25,11 @@ type Client struct { } func (c *Client) NewRequest(urlStr string) (req *Request, err error) { - sawyerReq, err := c.sawyerClient.NewRequest(urlStr) + sawyerReq, err := c.newSawyerRequest(urlStr) if err != nil { return } - sawyerReq.Header.Add("Accept", defaultMediaType) - sawyerReq.Header.Add("User-Agent", c.UserAgent) - if c.AuthMethod != nil { - sawyerReq.Header.Add("Authorization", c.AuthMethod.String()) - } - - if basicAuth, ok := c.AuthMethod.(BasicAuth); ok && basicAuth.OneTimePassword != "" { - sawyerReq.Header.Add("X-GitHub-OTP", basicAuth.OneTimePassword) - } - req = &Request{sawyerReq: sawyerReq} return } @@ -79,6 +70,43 @@ func (c *Client) patch(url *url.URL, input interface{}, output interface{}) (res }) } +func (c *Client) upload(uploadUrl *url.URL, asset *os.File, contentType string) (result *Result) { + req, err := c.newSawyerRequest(uploadUrl.String()) + if err != nil { + result = newResult(nil, err) + return + } + + fi, err := asset.Stat() + if err != nil { + result = newResult(nil, err) + return + } + + req.Header.Add("Content-Type", contentType) + req.ContentLength = fi.Size() + + req.Body = asset + sawyerResp := req.Post() + + resp, err := NewResponse(sawyerResp) + return newResult(resp, err) +} + +func (c *Client) newSawyerRequest(urlStr string) (sawyerReq *sawyer.Request, err error) { + sawyerReq, err = c.sawyerClient.NewRequest(urlStr) + if err != nil { + return + } + + sawyerReq.Header.Add("Accept", defaultMediaType) + sawyerReq.Header.Add("User-Agent", c.UserAgent) + + c.addAuthenticationHeaders(sawyerReq.Header) + + return +} + func sendRequest(c *Client, url *url.URL, fn func(r *Request) (*Response, error)) (result *Result) { req, err := c.NewRequest(url.String()) if err != nil { @@ -91,3 +119,13 @@ func sendRequest(c *Client, url *url.URL, fn func(r *Request) (*Response, error) return } + +func (c *Client) addAuthenticationHeaders(header http.Header) { + if c.AuthMethod != nil { + header.Add("Authorization", c.AuthMethod.String()) + } + + if basicAuth, ok := c.AuthMethod.(BasicAuth); ok && basicAuth.OneTimePassword != "" { + header.Add("X-GitHub-OTP", basicAuth.OneTimePassword) + } +} diff --git a/Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/octokit_test.go b/Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/octokit_test.go index 4a27722..e7673d5 100644 --- a/Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/octokit_test.go +++ b/Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/octokit_test.go @@ -59,6 +59,10 @@ func respondWithJSON(w http.ResponseWriter, s string) { respondWith(w, s) } +func respondWithStatus(w http.ResponseWriter, statusCode int) { + w.WriteHeader(statusCode) +} + func respondWith(w http.ResponseWriter, s string) { fmt.Fprint(w, s) } diff --git a/Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/releases.go b/Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/releases.go index 5c0b360..adfb4e5 100644 --- a/Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/releases.go +++ b/Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/releases.go @@ -17,7 +17,7 @@ type Release struct { URL string `json:"url,omitempty"` HTMLURL string `json:"html_url,omitempty"` AssetsURL string `json:"assets_url,omitempty"` - UploadURL string `json:"upload_url,omitempty"` + UploadURL Hyperlink `json:"upload_url,omitempty"` TagName string `json:"tag_name,omitempty"` TargetCommitish string `json:"target_commitish,omitempty"` Name string `json:"name,omitempty"` diff --git a/Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/releases_test.go b/Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/releases_test.go index f14caac..3191e5e 100644 --- a/Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/releases_test.go +++ b/Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/releases_test.go @@ -32,7 +32,7 @@ func TestReleasesService_All(t *testing.T) { assert.Equal(t, "* Windows works!: https://github.com/jingweno/gh/commit/6cb80cb09fd9f624a64d85438157955751a9ac70", firstRelease.Body) assert.Equal(t, "https://api.github.com/repos/jingweno/gh/releases/50013", firstRelease.URL) assert.Equal(t, "https://api.github.com/repos/jingweno/gh/releases/50013/assets", firstRelease.AssetsURL) - assert.Equal(t, "https://uploads.github.com/repos/jingweno/gh/releases/50013/assets{?name}", firstRelease.UploadURL) + assert.Equal(t, "https://uploads.github.com/repos/jingweno/gh/releases/50013/assets{?name}", string(firstRelease.UploadURL)) assert.Equal(t, "https://github.com/jingweno/gh/releases/v0.23.0", firstRelease.HTMLURL) assert.Equal(t, "2013-09-23 00:59:10 +0000 UTC", firstRelease.CreatedAt.String()) assert.Equal(t, "2013-09-23 01:07:56 +0000 UTC", firstRelease.PublishedAt.String()) diff --git a/Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/request.go b/Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/request.go index 1a42a88..88bff04 100644 --- a/Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/request.go +++ b/Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/request.go @@ -9,73 +9,47 @@ type Request struct { sawyerReq *sawyer.Request } -func (r *Request) Head(output interface{}) (resp *Response, err error) { - resp, err = r.do(sawyer.HeadMethod, nil, output) - return +func (r *Request) Head(output interface{}) (*Response, error) { + return r.createResponse(r.sawyerReq.Head(), output) } -func (r *Request) Get(output interface{}) (resp *Response, err error) { - resp, err = r.do(sawyer.GetMethod, nil, output) - return +func (r *Request) Get(output interface{}) (*Response, error) { + return r.createResponse(r.sawyerReq.Get(), output) } -func (r *Request) Post(input interface{}, output interface{}) (resp *Response, err error) { - resp, err = r.do(sawyer.PostMethod, input, output) - return +func (r *Request) Post(input interface{}, output interface{}) (*Response, error) { + r.setBody(input) + return r.createResponse(r.sawyerReq.Post(), output) } -func (r *Request) Put(input interface{}, output interface{}) (resp *Response, err error) { - resp, err = r.do(sawyer.PutMethod, input, output) - return +func (r *Request) Put(input interface{}, output interface{}) (*Response, error) { + r.setBody(input) + return r.createResponse(r.sawyerReq.Put(), output) } -func (r *Request) Delete(output interface{}) (resp *Response, err error) { - resp, err = r.do(sawyer.DeleteMethod, nil, output) - return +func (r *Request) Delete(output interface{}) (*Response, error) { + return r.createResponse(r.sawyerReq.Delete(), output) } -func (r *Request) Patch(input interface{}, output interface{}) (resp *Response, err error) { - resp, err = r.do(sawyer.PatchMethod, input, output) - return +func (r *Request) Patch(input interface{}, output interface{}) (*Response, error) { + r.setBody(input) + return r.createResponse(r.sawyerReq.Patch(), output) } -func (r *Request) do(method string, input interface{}, output interface{}) (resp *Response, err error) { - var sawyerResp *sawyer.Response - switch method { - case sawyer.HeadMethod: - sawyerResp = r.sawyerReq.Head() - case sawyer.GetMethod: - sawyerResp = r.sawyerReq.Get() - case sawyer.PostMethod: - mtype, _ := mediatype.Parse(defaultMediaType) - r.sawyerReq.SetBody(mtype, input) - sawyerResp = r.sawyerReq.Post() - case sawyer.PutMethod: - mtype, _ := mediatype.Parse(defaultMediaType) - r.sawyerReq.SetBody(mtype, input) - sawyerResp = r.sawyerReq.Put() - case sawyer.PatchMethod: - mtype, _ := mediatype.Parse(defaultMediaType) - r.sawyerReq.SetBody(mtype, input) - sawyerResp = r.sawyerReq.Patch() - case sawyer.DeleteMethod: - sawyerResp = r.sawyerReq.Delete() - case sawyer.OptionsMethod: - sawyerResp = r.sawyerReq.Options() - } +func (r *Request) Options(output interface{}) (*Response, error) { + return r.createResponse(r.sawyerReq.Options(), output) +} - if sawyerResp.IsError() { - err = sawyerResp.ResponseError - return - } +func (r *Request) setBody(input interface{}) { + mtype, _ := mediatype.Parse(defaultMediaType) + r.sawyerReq.SetBody(mtype, input) +} - if sawyerResp.IsApiError() { - err = NewResponseError(sawyerResp) - return +func (r *Request) createResponse(sawyerResp *sawyer.Response, output interface{}) (resp *Response, err error) { + resp, err = NewResponse(sawyerResp) + if err == nil { + err = sawyerResp.Decode(output) } - resp = &Response{Response: sawyerResp.Response, MediaType: sawyerResp.MediaType, MediaHeader: sawyerResp.MediaHeader} - err = sawyerResp.Decode(output) - return } diff --git a/Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/response.go b/Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/response.go index ad77528..a93091f 100644 --- a/Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/response.go +++ b/Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/response.go @@ -1,6 +1,7 @@ package octokit import ( + "github.com/lostisland/go-sawyer" "github.com/lostisland/go-sawyer/mediaheader" "github.com/lostisland/go-sawyer/mediatype" "net/http" @@ -11,3 +12,19 @@ type Response struct { MediaHeader *mediaheader.MediaHeader *http.Response } + +func NewResponse(sawyerResp *sawyer.Response) (resp *Response, err error) { + if sawyerResp.IsError() { + err = sawyerResp.ResponseError + return + } + + if sawyerResp.IsApiError() { + err = NewResponseError(sawyerResp) + return + } + + resp = &Response{Response: sawyerResp.Response, MediaType: sawyerResp.MediaType, MediaHeader: sawyerResp.MediaHeader} + + return +} diff --git a/Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/uploads.go b/Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/uploads.go new file mode 100644 index 0000000..ca3c1cc --- /dev/null +++ b/Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/uploads.go @@ -0,0 +1,20 @@ +package octokit + +import ( + "net/url" + "os" +) + +// Create an UploadsService with the base url.URL +func (c *Client) Uploads(url *url.URL) *UploadsService { + return &UploadsService{client: c, URL: url} +} + +type UploadsService struct { + client *Client + URL *url.URL +} + +func (u *UploadsService) UploadAsset(asset *os.File, contentType string) (result *Result) { + return u.client.upload(u.URL, asset, contentType) +} diff --git a/Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/uploads_test.go b/Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/uploads_test.go new file mode 100644 index 0000000..56447ea --- /dev/null +++ b/Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/uploads_test.go @@ -0,0 +1,41 @@ +package octokit + +import ( + "fmt" + "github.com/bmizerany/assert" + "io/ioutil" + "net/http" + "os" + "testing" +) + +func TestUploadsService_UploadAsset(t *testing.T) { + setup() + defer tearDown() + + file, err := ioutil.TempFile("", "octokit-test-upload-") + assert.Equal(t, nil, err) + file.WriteString("this is a test") + + fi, err := file.Stat() + assert.Equal(t, nil, err) + file.Close() + + mux.HandleFunc("/repos/octokit/Hello-World/releases/123/assets", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testHeader(t, r, "Content-Type", "text/plain") + assert.Equal(t, fi.Size(), r.ContentLength) + respondWithStatus(w, 201) + }) + + link := Hyperlink("/repos/octokit/Hello-World/releases/123/assets{?name}") + url, err := link.Expand(M{"name": fi.Name()}) + assert.Equal(t, nil, err) + + open, _ := os.Open(file.Name()) + result := client.Uploads(url).UploadAsset(open, "text/plain") + fmt.Println(result) + assert.T(t, !result.HasError()) + + assert.Equal(t, 201, result.Response.StatusCode) +} From 1adffb9c628ef43d6c5853a204a686d7a63fbba9 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Tue, 24 Dec 2013 13:42:36 -0800 Subject: [PATCH 19/21] Use the upload service in octokit. --- commands/release.go | 5 ++++- github/client.go | 33 ++++----------------------------- 2 files changed, 8 insertions(+), 30 deletions(-) diff --git a/commands/release.go b/commands/release.go index ff40cc0..517a865 100644 --- a/commands/release.go +++ b/commands/release.go @@ -156,13 +156,16 @@ func uploadReleaseAssets(gh *github.Client, release *octokit.Release, assetsDir go func() { defer wg.Done() + uploadUrl, err := release.UploadURL.Expand(octokit.M{"name": fi.Name()}) + utils.Check(err) + contentType := detectContentType(path, fi) file, err := os.Open(path) utils.Check(err) defer file.Close() - err = gh.UploadReleaseAsset(release, file, fi, contentType) + err = gh.UploadReleaseAsset(uploadUrl, file, contentType) utils.Check(err) }() } diff --git a/github/client.go b/github/client.go index 4ad6933..e97c6c6 100644 --- a/github/client.go +++ b/github/client.go @@ -2,9 +2,7 @@ package github import ( "fmt" - "github.com/jingweno/gh/utils" "github.com/jingweno/go-octokit/octokit" - "net/http" "net/url" "os" ) @@ -119,7 +117,6 @@ func (client *Client) Releases(project *Project) (releases []octokit.Release, er releases, result := client.octokit().Releases(client.requestURL(url)).All() if result.HasError() { err = result.Err - return } return @@ -134,38 +131,16 @@ func (client *Client) CreateRelease(project *Project, params octokit.ReleasePara release, result := client.octokit().Releases(client.requestURL(url)).Create(params) if result.HasError() { err = result.Err - return } return } -func (client *Client) UploadReleaseAsset(release *octokit.Release, asset *os.File, fi os.FileInfo, contentType string) (err error) { - uploadUrl, err := octokit.Hyperlink(release.UploadURL).Expand(octokit.M{"name": fi.Name()}) - utils.Check(err) - +func (client *Client) UploadReleaseAsset(uploadUrl *url.URL, asset *os.File, contentType string) (err error) { c := client.octokit() - - request, err := http.NewRequest("POST", uploadUrl.String(), asset) - utils.Check(err) - - request.Header.Add("Content-Type", contentType) - request.ContentLength = fi.Size() - - if c.AuthMethod != nil { - request.Header.Add("Authorization", c.AuthMethod.String()) - } - - if basicAuth, ok := c.AuthMethod.(octokit.BasicAuth); ok && basicAuth.OneTimePassword != "" { - request.Header.Add("X-GitHub-OTP", basicAuth.OneTimePassword) - } - - httpClient := &http.Client{} - response, err := httpClient.Do(request) - utils.Check(err) - - if response.StatusCode != 201 { - err = fmt.Errorf("Error uploading the release asset %s, %s", fi.Name(), response.Status) + result := c.Uploads(uploadUrl).UploadAsset(asset, contentType) + if result.HasError() { + err = result.Err } return } From ecf536f978f31af15d4937981c8eed3a55d3aa86 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Tue, 24 Dec 2013 15:13:40 -0800 Subject: [PATCH 20/21] Add upload indicator. --- commands/release.go | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/commands/release.go b/commands/release.go index 517a865..c686f91 100644 --- a/commands/release.go +++ b/commands/release.go @@ -13,6 +13,7 @@ import ( "path/filepath" "strings" "sync" + "sync/atomic" ) var ( @@ -99,7 +100,7 @@ func release(cmd *Command, args *Args) { uploadReleaseAssets(gh, finalRelease, assetsDir) - fmt.Printf("Release created: %s", finalRelease.HTMLURL) + fmt.Printf("\n\nRelease created: %s", finalRelease.HTMLURL) }) } @@ -149,13 +150,28 @@ func getAssetsDirectory(assetsDir, tag string) (string, error) { func uploadReleaseAssets(gh *github.Client, release *octokit.Release, assetsDir string) { var wg sync.WaitGroup + var totalAssets, countAssets uint64 + + filepath.Walk(assetsDir, func(path string, fi os.FileInfo, err error) error { + if !fi.IsDir() { + totalAssets += 1 + } + return nil + }) filepath.Walk(assetsDir, func(path string, fi os.FileInfo, err error) error { if !fi.IsDir() { wg.Add(1) go func() { - defer wg.Done() + defer func() { + atomic.AddUint64(&countAssets, uint64(1)) + printUploadProgress(&countAssets, totalAssets) + wg.Done() + }() + + printUploadProgress(&countAssets, totalAssets) + uploadUrl, err := release.UploadURL.Expand(octokit.M{"name": fi.Name()}) utils.Check(err) @@ -194,3 +210,8 @@ func detectContentType(path string, fi os.FileInfo) string { return http.DetectContentType(fileHeader.Bytes()) } + +func printUploadProgress(count *uint64, total uint64) { + out := fmt.Sprintf("Uploading assets (%d/%d)", atomic.LoadUint64(count), total) + fmt.Print("\r" + out) +} From e13b02686adf08c8e3a6c6fce76530be4db4f2ae Mon Sep 17 00:00:00 2001 From: David Calavera Date: Tue, 24 Dec 2013 15:36:36 -0800 Subject: [PATCH 21/21] Add check for empty arguments. --- commands/release.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/commands/release.go b/commands/release.go index c686f91..267fb28 100644 --- a/commands/release.go +++ b/commands/release.go @@ -69,6 +69,11 @@ func releases(cmd *Command, args *Args) { } func release(cmd *Command, args *Args) { + if args.IsParamsEmpty() { + utils.Check(fmt.Errorf("Missed argument TAG")) + return + } + tag := args.LastParam() assetsDir, err := getAssetsDirectory(flagReleaseAssetsDir, tag)