提交 f412a353 编写于 作者: M Mislav Marohnić

Merge pull request #1095 from github/release-overhaul

Big overhaul of `hub release` command
......@@ -9,6 +9,7 @@ HELP_CMD = \
man/hub-create.1 \
man/hub-fork.1 \
man/hub-pull-request.1 \
man/hub-release.1 \
HELP_EXT = \
man/hub-am.1 \
......
......@@ -66,6 +66,25 @@ func (c *Command) parseArguments(args *Args) (err error) {
return
}
func (c *Command) FlagPassed(name string) bool {
found := false
c.Flag.Visit(func(f *flag.Flag) {
if f.Name == name {
found = true
}
})
return found
}
func (c *Command) Arg(idx int) string {
args := c.Flag.Args()
if idx < len(args) {
return args[idx]
} else {
return ""
}
}
func (c *Command) Use(subCommand *Command) {
if c.subCommands == nil {
c.subCommands = make(map[string]*Command)
......@@ -95,7 +114,8 @@ func (c *Command) Name() string {
if c.Key != "" {
return c.Key
}
return strings.Split(strings.TrimSpace(c.Usage), " ")[0]
usageLine := strings.Split(strings.TrimSpace(c.Usage), "\n")[0]
return strings.Split(usageLine, " ")[0]
}
func (c *Command) Runnable() bool {
......
package commands
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
......@@ -13,21 +10,41 @@ import (
"github.com/github/hub/github"
"github.com/github/hub/ui"
"github.com/github/hub/utils"
"github.com/octokit/go-octokit/octokit"
)
var (
cmdRelease = &Command{
Run: release,
Usage: "release",
Long: "Retrieve releases from GitHub",
}
Run: listReleases,
Usage: `
release [--include-drafts]
release show <TAG>
release create [-dp] [-a <FILE>] [-m <MESSAGE>|-F <FILE>] [-c <TARGET>] <TAG>
release edit [<options>] <TAG>
`,
Long: `Manage GitHub releases.
cmdCreateRelease = &Command{
Key: "create",
Run: createRelease,
Usage: "release create [-d] [-p] [-a <FILE>] [-m <MESSAGE>|-f <FILE>] [-c <COMMIT>] <TAG>",
Long: `Create a GitHub release.
## Commands:
With no arguments, shows a list of existing releases.
With '--include-drafts', include draft releases in the listing.
* _show_:
Show GitHub release notes for <TAG>.
With '--show-downloads', include the "Downloads" section.
* _create_:
Create a GitHub release for the specified <TAG> name. If git tag <TAG>
doesn't exist, it will be created at <TARGET> (default: current branch).
* _edit_:
Edit the GitHub release for the specified <TAG> name. Accepts the same
options as _create_ command. Publish a draft with '--draft=false'.
When <MESSAGE> or <FILE> are not specified, a text editor will open
pre-populated with current release title and body. To re-use existing title
and body unchanged, pass '-m ""'.
## Options:
-d, --draft
......@@ -39,19 +56,44 @@ var (
-a, --asset <FILE>
Attach a file as an asset for this release.
If <FILE> is in the "<filename>#<text>" format, the text after the '#'
character is taken as asset label.
-m, --message <MESSAGE>
Use the first line of <MESSAGE> as release title, and the rest as release description.
-f, --file <FILE>
-F, --file <FILE>
Read the release title and description from <FILE>.
-c, --commitish <COMMIT>
-c, --commitish <TARGET>
A SHA, tag, or branch name to attach the release to (default: current branch).
<TAG>
The git tag name for this release.
`}
## See also:
hub(1), git-tag(1)
`,
}
cmdShowRelease = &Command{
Key: "show",
Run: showRelease,
}
cmdCreateRelease = &Command{
Key: "create",
Run: createRelease,
}
cmdEditRelease = &Command{
Key: "edit",
Run: editRelease,
}
flagReleaseIncludeDrafts,
flagReleaseShowDownloads,
flagReleaseDraft,
flagReleasePrerelease bool
......@@ -63,234 +105,285 @@ var (
)
func init() {
cmdRelease.Flag.BoolVarP(&flagReleaseIncludeDrafts, "include-drafts", "d", false, "DRAFTS")
cmdShowRelease.Flag.BoolVarP(&flagReleaseShowDownloads, "show-downloads", "d", false, "DRAFTS")
cmdCreateRelease.Flag.BoolVarP(&flagReleaseDraft, "draft", "d", false, "DRAFT")
cmdCreateRelease.Flag.BoolVarP(&flagReleasePrerelease, "prerelease", "p", false, "PRERELEASE")
cmdCreateRelease.Flag.VarP(&flagReleaseAssets, "attach", "a", "ATTACH_ASSETS")
cmdCreateRelease.Flag.StringVarP(&flagReleaseMessage, "message", "m", "", "MESSAGE")
cmdCreateRelease.Flag.StringVarP(&flagReleaseFile, "file", "f", "", "FILE")
cmdCreateRelease.Flag.StringVarP(&flagReleaseFile, "file", "F", "", "FILE")
cmdCreateRelease.Flag.StringVarP(&flagReleaseCommitish, "commitish", "c", "", "COMMITISH")
cmdEditRelease.Flag.BoolVarP(&flagReleaseDraft, "draft", "d", false, "DRAFT")
cmdEditRelease.Flag.BoolVarP(&flagReleasePrerelease, "prerelease", "p", false, "PRERELEASE")
cmdEditRelease.Flag.VarP(&flagReleaseAssets, "attach", "a", "ATTACH_ASSETS")
cmdEditRelease.Flag.StringVarP(&flagReleaseMessage, "message", "m", "", "MESSAGE")
cmdEditRelease.Flag.StringVarP(&flagReleaseFile, "file", "F", "", "FILE")
cmdEditRelease.Flag.StringVarP(&flagReleaseCommitish, "commitish", "c", "", "COMMITISH")
cmdRelease.Use(cmdShowRelease)
cmdRelease.Use(cmdCreateRelease)
cmdRelease.Use(cmdEditRelease)
CmdRunner.Use(cmdRelease)
}
func release(cmd *Command, args *Args) {
runInLocalRepo(func(localRepo *github.GitHubRepo, project *github.Project, client *github.Client) {
if args.Noop {
ui.Printf("Would request list of releases for %s\n", project)
} else {
releases, err := client.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)
}
func listReleases(cmd *Command, args *Args) {
localRepo, err := github.LocalRepo()
utils.Check(err)
ui.Println(strings.Join(outputs, "\n\n"))
}
})
}
project, err := localRepo.MainProject()
utils.Check(err)
func createRelease(cmd *Command, args *Args) {
if args.IsParamsEmpty() {
utils.Check(fmt.Errorf("Missed argument TAG"))
return
}
gh := github.NewClient(project.Host)
tag := args.LastParam()
runInLocalRepo(func(localRepo *github.GitHubRepo, project *github.Project, client *github.Client) {
release, err := client.Release(project, tag)
if args.Noop {
ui.Printf("Would request list of releases for %s\n", project)
} else {
releases, err := gh.FetchReleases(project)
utils.Check(err)
if release == nil {
commitish := flagReleaseCommitish
if commitish == "" {
currentBranch, err := localRepo.CurrentBranch()
utils.Check(err)
commitish = currentBranch.ShortName()
for _, release := range releases {
if !release.Draft || flagReleaseIncludeDrafts {
ui.Println(release.TagName)
}
}
}
title, body, err := getTitleAndBodyFromFlags(flagReleaseMessage, flagReleaseFile)
utils.Check(err)
os.Exit(0)
}
var editor *github.Editor
if title == "" {
cs := git.CommentChar()
message, err := renderReleaseTpl(cs, tag, project.Name, commitish)
utils.Check(err)
func showRelease(cmd *Command, args *Args) {
tagName := cmd.Arg(0)
if tagName == "" {
utils.Check(fmt.Errorf("Missing argument TAG"))
}
editor, err = github.NewEditor("RELEASE", "release", message)
utils.Check(err)
localRepo, err := github.LocalRepo()
utils.Check(err)
title, body, err = editor.EditTitleAndBody()
utils.Check(err)
}
project, err := localRepo.MainProject()
utils.Check(err)
params := octokit.ReleaseParams{
TagName: tag,
TargetCommitish: commitish,
Name: title,
Body: body,
Draft: flagReleaseDraft,
Prerelease: flagReleasePrerelease,
}
release, err = client.CreateRelease(project, params)
utils.Check(err)
gh := github.NewClient(project.Host)
if editor != nil {
defer editor.DeleteFile()
}
}
if len(flagReleaseAssets) > 0 {
paths := make([]string, 0)
for _, asset := range flagReleaseAssets {
finder := assetFinder{}
p, err := finder.Find(asset)
utils.Check(err)
if args.Noop {
ui.Printf("Would display information for `%s' release\n", tagName)
} else {
release, err := gh.FetchRelease(project, tagName)
utils.Check(err)
paths = append(paths, p...)
}
body := strings.TrimSpace(release.Body)
uploader := assetUploader{
Client: client,
Release: release,
ui.Println(release.Name)
if body != "" {
ui.Printf("\n%s\n", body)
}
if flagReleaseShowDownloads {
ui.Printf("\n## Downloads\n\n")
for _, asset := range release.Assets {
ui.Println(asset.DownloadUrl)
}
err = uploader.UploadAll(paths)
if err != nil {
ui.Println("")
utils.Check(err)
if release.ZipballUrl != "" {
ui.Println(release.ZipballUrl)
ui.Println(release.TarballUrl)
}
}
}
ui.Printf("\n%s\n", release.HTMLURL)
})
}
type assetUploader struct {
Client *github.Client
Release *octokit.Release
os.Exit(0)
}
func (a *assetUploader) UploadAll(paths []string) error {
errUploadChan := make(chan string)
successChan := make(chan bool)
total := len(paths)
count := 0
for _, path := range paths {
go a.uploadAsync(path, successChan, errUploadChan)
func createRelease(cmd *Command, args *Args) {
tagName := cmd.Arg(0)
if tagName == "" {
utils.Check(fmt.Errorf("Missing argument TAG"))
return
}
a.printUploadProgress(count, total)
errUploads := make([]string, 0)
for {
select {
case _ = <-successChan:
count++
a.printUploadProgress(count, total)
case errUpload := <-errUploadChan:
errUploads = append(errUploads, errUpload)
count++
a.printUploadProgress(count, total)
}
localRepo, err := github.LocalRepo()
utils.Check(err)
if count == total {
break
}
}
project, err := localRepo.CurrentProject()
utils.Check(err)
gh := github.NewClient(project.Host)
var err error
if len(errUploads) > 0 {
err = fmt.Errorf("Error uploading %s", strings.Join(errUploads, ", "))
commitish := flagReleaseCommitish
if commitish == "" {
currentBranch, err := localRepo.CurrentBranch()
utils.Check(err)
commitish = currentBranch.ShortName()
}
return err
}
var title string
var body string
var editor *github.Editor
func (a *assetUploader) uploadAsync(path string, successChan chan bool, errUploadChan chan string) {
err := a.Upload(path)
if err == nil {
successChan <- true
if cmd.FlagPassed("message") {
title, body = readMsg(flagReleaseMessage)
} else if cmd.FlagPassed("file") {
title, body, err = readMsgFromFile(flagReleaseFile)
utils.Check(err)
} else {
errUploadChan <- path
cs := git.CommentChar()
message, err := renderReleaseTpl("Creating", cs, tagName, project.String(), commitish)
utils.Check(err)
editor, err := github.NewEditor("RELEASE", "release", message)
utils.Check(err)
title, body, err = editor.EditTitleAndBody()
utils.Check(err)
}
}
func (a *assetUploader) Upload(path string) error {
contentType, err := a.detectContentType(path)
if err != nil {
return err
if title == "" {
utils.Check(fmt.Errorf("Aborting release due to empty release title"))
}
f, err := os.Open(path)
if err != nil {
return err
params := &github.Release{
TagName: tagName,
TargetCommitish: commitish,
Name: title,
Body: body,
Draft: flagReleaseDraft,
Prerelease: flagReleasePrerelease,
}
defer f.Close()
uploadUrl, err := a.Release.UploadURL.Expand(octokit.M{"name": filepath.Base(path)})
if err != nil {
return err
var release *github.Release
if args.Noop {
ui.Printf("Would create release `%s' for %s with tag name `%s'\n", title, project, tagName)
} else {
release, err = gh.CreateRelease(project, params)
utils.Check(err)
ui.Println(release.HtmlUrl)
}
return a.Client.UploadReleaseAsset(uploadUrl, f, contentType)
uploadAssets(gh, release, flagReleaseAssets, args)
if editor != nil {
editor.DeleteFile()
}
os.Exit(0)
}
func (a *assetUploader) detectContentType(path string) (string, error) {
file, err := os.Open(path)
if err != nil {
return "", err
func editRelease(cmd *Command, args *Args) {
tagName := cmd.Arg(0)
if tagName == "" {
utils.Check(fmt.Errorf("Missing argument TAG"))
return
}
defer file.Close()
fi, err := file.Stat()
if err != nil {
return "", err
localRepo, err := github.LocalRepo()
utils.Check(err)
project, err := localRepo.CurrentProject()
utils.Check(err)
gh := github.NewClient(project.Host)
release, err := gh.FetchRelease(project, tagName)
utils.Check(err)
params := map[string]interface{}{}
commitish := release.TargetCommitish
if cmd.FlagPassed("commitish") {
params["target_commitish"] = flagReleaseCommitish
commitish = flagReleaseCommitish
}
fileHeader := &bytes.Buffer{}
headerSize := int64(512)
if fi.Size() < headerSize {
headerSize = fi.Size()
if cmd.FlagPassed("draft") {
params["draft"] = flagReleaseDraft
}
// 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)
if err != nil {
return "", err
if cmd.FlagPassed("prerelease") {
params["prerelease"] = flagReleasePrerelease
}
t := http.DetectContentType(fileHeader.Bytes())
var title string
var body string
var editor *github.Editor
return strings.Split(t, ";")[0], nil
}
if cmd.FlagPassed("message") {
title, body = readMsg(flagReleaseMessage)
} else if cmd.FlagPassed("file") {
title, body, err = readMsgFromFile(flagReleaseFile)
utils.Check(err)
func (a *assetUploader) printUploadProgress(count int, total int) {
out := fmt.Sprintf("Uploading assets (%d/%d)", count, total)
fmt.Print("\r" + out)
}
if title == "" {
utils.Check(fmt.Errorf("Aborting editing due to empty release title"))
}
} else {
cs := git.CommentChar()
message, err := renderReleaseTpl("Editing", cs, tagName, project.String(), commitish)
utils.Check(err)
type assetFinder struct {
}
message = fmt.Sprintf("%s\n\n%s\n%s", release.Name, release.Body, message)
editor, err := github.NewEditor("RELEASE", "release", message)
utils.Check(err)
func (a *assetFinder) Find(path string) ([]string, error) {
result := make([]string, 0)
title, body, err = editor.EditTitleAndBody()
utils.Check(err)
err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
if title == "" {
utils.Check(fmt.Errorf("Aborting editing due to empty release title"))
}
}
if title != "" {
params["name"] = title
}
if body != "" {
params["body"] = body
}
if len(params) > 0 {
if args.Noop {
ui.Printf("Would edit release `%s'\n", tagName)
} else {
release, err = gh.EditRelease(release, params)
utils.Check(err)
if !info.IsDir() {
result = append(result, path)
if editor != nil {
editor.DeleteFile()
}
}
}
return nil
})
uploadAssets(gh, release, flagReleaseAssets, args)
os.Exit(0)
}
return result, err
func uploadAssets(gh *github.Client, release *github.Release, assets []string, args *Args) {
for _, asset := range assets {
var label string
parts := strings.SplitN(asset, "#", 2)
asset = parts[0]
if len(parts) > 1 {
label = parts[1]
}
if args.Noop {
if label == "" {
ui.Errorf("Would attach release asset `%s'\n", asset)
} else {
ui.Errorf("Would attach release asset `%s' with label `%s'\n", asset, label)
}
} else {
for _, existingAsset := range release.Assets {
if existingAsset.Name == filepath.Base(asset) {
err := gh.DeleteReleaseAsset(&existingAsset)
utils.Check(err)
break
}
}
ui.Errorf("Attaching release asset `%s'...\n", asset)
_, err := gh.UploadReleaseAsset(release, asset, label)
utils.Check(err)
}
}
}
package commands
import (
"testing"
"github.com/bmizerany/assert"
"github.com/github/hub/fixtures"
)
func TestAssetFinder_Find(t *testing.T) {
finder := assetFinder{}
paths, err := finder.Find(fixtures.Path("release_dir", "file1"))
assert.Equal(t, nil, err)
assert.Equal(t, 1, len(paths))
paths, err = finder.Find(fixtures.Path("release_dir", "dir"))
assert.Equal(t, nil, err)
assert.Equal(t, 3, len(paths))
}
func TestAssetUploader_detectContentType(t *testing.T) {
u := &assetUploader{}
ct, err := u.detectContentType(fixtures.Path("release_dir", "file1"))
assert.Equal(t, nil, err)
assert.Equal(t, "text/plain", ct)
}
......@@ -5,25 +5,28 @@ import (
"text/template"
)
const releaseTmpl = `{{.CS}} Creating release {{.TagName}} for {{.ProjectName}} from {{.BranchName}}
const releaseTmpl = `
{{.CS}} {{.Operation}} release {{.TagName}} for {{.ProjectName}} from {{.BranchName}}
{{.CS}}
{{.CS}} Write a message for this release. The first block of
{{.CS}} text is the title and the rest is the description.`
type releaseMsg struct {
Operation string
CS string
TagName string
ProjectName string
BranchName string
}
func renderReleaseTpl(cs, tagName, projectName, branchName string) (string, error) {
func renderReleaseTpl(operation, cs, tagName, projectName, branchName string) (string, error) {
t, err := template.New("releaseTmpl").Parse(releaseTmpl)
if err != nil {
return "", err
}
msg := &releaseMsg{
Operation: operation,
CS: cs,
TagName: tagName,
ProjectName: projectName,
......
......@@ -7,10 +7,11 @@ import (
)
func TestRenderReleaseTpl(t *testing.T) {
msg, err := renderReleaseTpl("#", "1.0", "hub", "master")
msg, err := renderReleaseTpl("Creating", "#", "1.0", "github/hub", "master")
assert.Equal(t, nil, err)
expMsg := `# Creating release 1.0 for hub from master
expMsg := `
# Creating release 1.0 for github/hub from master
#
# Write a message for this release. The first block of
# text is the title and the rest is the description.`
......
......@@ -80,34 +80,35 @@ func getTitleAndBodyFromFlags(messageFlag, fileFlag string) (title, body string,
if messageFlag != "" {
title, body = readMsg(messageFlag)
} else if fileFlag != "" {
var (
content []byte
err error
)
title, body, err = readMsgFromFile(fileFlag)
}
if fileFlag == "-" {
content, err = ioutil.ReadAll(os.Stdin)
} else {
content, err = ioutil.ReadFile(fileFlag)
}
utils.Check(err)
return
}
title, body = readMsg(string(content))
func readMsgFromFile(filename string) (title, body string, err error) {
var content []byte
if filename == "-" {
content, err = ioutil.ReadAll(os.Stdin)
} else {
content, err = ioutil.ReadFile(filename)
}
if err != nil {
return
}
text := strings.Replace(string(content), "\r\n", "\n", -1)
title, body = readMsg(text)
return
}
func readMsg(msg string) (title, body string) {
s := bufio.NewScanner(strings.NewReader(msg))
if s.Scan() {
title = s.Text()
body = strings.TrimLeft(msg, title)
func readMsg(message string) (title, body string) {
parts := strings.SplitN(message, "\n\n", 2)
title = strings.TrimSpace(title)
body = strings.TrimSpace(body)
title = strings.TrimSpace(strings.Replace(parts[0], "\n", " ", -1))
if len(parts) > 1 {
body = strings.TrimSpace(parts[1])
}
return
}
......
......@@ -21,9 +21,9 @@ func TestReadMsg(t *testing.T) {
assert.Equal(t, "my pull title", title)
assert.Equal(t, "my description\n\nanother line", body)
title, body = readMsg("my pull title\r\n\r\nmy description\r\n\r\nanother line")
title, body = readMsg("my pull\ntitle\n\nmy description\n\nanother line")
assert.Equal(t, "my pull title", title)
assert.Equal(t, "my description\r\n\r\nanother line", body)
assert.Equal(t, "my description\n\nanother line", body)
}
func TestDirIsNotEmpty(t *testing.T) {
......
Feature: hub release
Background:
Given I am in "git://github.com/mislav/will_paginate.git" git repo
And I am "mislav" on github.com with OAuth token "OTOKEN"
Scenario: List non-draft releases
Given the GitHub API server:
"""
get('/repos/mislav/will_paginate/releases') {
json [
{ tag_name: 'v1.2.0',
name: 'will_paginate 1.2.0',
draft: true,
prerelease: false,
},
{ tag_name: 'v1.2.0-pre',
name: 'will_paginate 1.2.0-pre',
draft: false,
prerelease: true,
},
{ tag_name: 'v1.0.2',
name: 'will_paginate 1.0.2',
draft: false,
prerelease: false,
},
]
}
"""
When I successfully run `hub release`
Then the output should contain exactly:
"""
v1.2.0-pre
v1.0.2\n
"""
Scenario: List all releases
Given the GitHub API server:
"""
get('/repos/mislav/will_paginate/releases') {
json [
{ tag_name: 'v1.2.0',
name: 'will_paginate 1.2.0',
draft: true,
prerelease: false,
},
{ tag_name: 'v1.2.0-pre',
name: 'will_paginate 1.2.0-pre',
draft: false,
prerelease: true,
},
{ tag_name: 'v1.0.2',
name: 'will_paginate 1.0.2',
draft: false,
prerelease: false,
},
]
}
"""
When I successfully run `hub release --include-drafts`
Then the output should contain exactly:
"""
v1.2.0
v1.2.0-pre
v1.0.2\n
"""
Scenario: Repository not found when listing releases
Given the GitHub API server:
"""
get('/repos/mislav/will_paginate/releases') {
status 404
json message: "Not Found",
documentation_url: "https://developer.github.com/v3"
}
"""
When I run `hub release`
Then the stderr should contain exactly:
"""
Error fetching releases: Not Found (HTTP 404)
Not Found\n
"""
And the exit status should be 1
Scenario: Server error when listing releases
Given the GitHub API server:
"""
get('/repos/mislav/will_paginate/releases') {
status 504
'<html><title>Its fine</title></html>'
}
"""
When I run `hub release`
Then the stderr should contain exactly:
"""
Error fetching releases: invalid character '<' looking for beginning of value (HTTP 504)\n
"""
And the exit status should be 1
Scenario: Show specific release
Given the GitHub API server:
"""
get('/repos/mislav/will_paginate/releases') {
json [
{ tag_name: 'v1.2.0',
name: 'will_paginate 1.2.0',
draft: true,
prerelease: false,
tarball_url: "https://github.com/mislav/will_paginate/archive/v1.2.0.tar.gz",
zipball_url: "https://github.com/mislav/will_paginate/archive/v1.2.0.zip",
assets: [
{ browser_download_url: "https://github.com/mislav/will_paginate/releases/download/v1.2.0/example.zip",
},
],
body: <<MARKDOWN
### Hello to my release
Here is what's broken:
- everything
MARKDOWN
},
]
}
"""
When I successfully run `hub release show v1.2.0`
Then the output should contain exactly:
"""
will_paginate 1.2.0
### Hello to my release
Here is what's broken:
- everything\n
"""
Scenario: Show specific release including downloads
Given the GitHub API server:
"""
get('/repos/mislav/will_paginate/releases') {
json [
{ tag_name: 'v1.2.0',
name: 'will_paginate 1.2.0',
draft: true,
prerelease: false,
tarball_url: "https://github.com/mislav/will_paginate/archive/v1.2.0.tar.gz",
zipball_url: "https://github.com/mislav/will_paginate/archive/v1.2.0.zip",
assets: [
{ browser_download_url: "https://github.com/mislav/will_paginate/releases/download/v1.2.0/example.zip",
},
],
body: <<MARKDOWN
### Hello to my release
Here is what's broken:
- everything
MARKDOWN
},
]
}
"""
When I successfully run `hub release show v1.2.0 --show-downloads`
Then the output should contain exactly:
"""
will_paginate 1.2.0
### Hello to my release
Here is what's broken:
- everything
## Downloads
https://github.com/mislav/will_paginate/releases/download/v1.2.0/example.zip
https://github.com/mislav/will_paginate/archive/v1.2.0.zip
https://github.com/mislav/will_paginate/archive/v1.2.0.tar.gz\n
"""
Scenario: Create a release
Given the GitHub API server:
"""
post('/repos/mislav/will_paginate/releases') {
assert :draft => true,
:tag_name => "v1.2.0",
:target_commitish => "master",
:name => "will_paginate 1.2.0: Instant Gratification Monkey",
:body => ""
status 201
json :html_url => "https://github.com/mislav/will_paginate/releases/v1.2.0"
}
"""
When I successfully run `hub release create -dm "will_paginate 1.2.0: Instant Gratification Monkey" v1.2.0`
Then the output should contain exactly:
"""
https://github.com/mislav/will_paginate/releases/v1.2.0\n
"""
Scenario: Create a release with assets
Given the GitHub API server:
"""
post('/repos/mislav/will_paginate/releases') {
status 201
json :html_url => "https://github.com/mislav/will_paginate/releases/v1.2.0",
:upload_url => "https://api.github.com/uploads/assets{?name,label}"
}
post('/uploads/assets') {
assert :name => 'hello-1.2.0.tar.gz',
:label => 'Hello World'
status 201
}
"""
And a file named "hello-1.2.0.tar.gz" with:
"""
TARBALL
"""
When I successfully run `hub release create -m "hello" v1.2.0 -a "./hello-1.2.0.tar.gz#Hello World"`
Then the output should contain exactly:
"""
https://github.com/mislav/will_paginate/releases/v1.2.0
Attaching release asset `./hello-1.2.0.tar.gz'...\n
"""
Scenario: Edit existing release
Given the GitHub API server:
"""
get('/repos/mislav/will_paginate/releases') {
json [
{ url: 'https://api.github.com/repos/mislav/will_paginate/releases/123',
tag_name: 'v1.2.0',
name: 'will_paginate 1.2.0',
draft: true,
prerelease: false,
body: <<MARKDOWN
### Hello to my release
Here is what's broken:
- everything
MARKDOWN
},
]
}
patch('/repos/mislav/will_paginate/releases/123') {
assert :name => 'KITTENS EVERYWHERE',
:draft => false,
:prerelease => nil
json({})
}
"""
Given the git commit editor is "vim"
And the text editor adds:
"""
KITTENS EVERYWHERE
"""
When I successfully run `hub release edit --draft=false v1.2.0`
Then there should be no output
Scenario: Edit existing release by uploading assets
Given the GitHub API server:
"""
deleted = false
get('/repos/mislav/will_paginate/releases') {
json [
{ url: 'https://api.github.com/repos/mislav/will_paginate/releases/123',
upload_url: 'https://api.github.com/uploads/assets{?name,label}',
tag_name: 'v1.2.0',
name: 'will_paginate 1.2.0',
draft: true,
prerelease: false,
assets: [
{ url: 'https://api.github.com/repos/mislav/will_paginate/assets/456',
name: 'hello-1.2.0.tar.gz',
},
],
},
]
}
delete('/repos/mislav/will_paginate/assets/456') {
deleted = true
status 204
}
post('/uploads/assets') {
halt 422 unless deleted
assert :name => 'hello-1.2.0.tar.gz',
:label => nil
status 201
}
"""
And a file named "hello-1.2.0.tar.gz" with:
"""
TARBALL
"""
When I successfully run `hub release edit -m "" v1.2.0 -a hello-1.2.0.tar.gz`
Then the output should contain exactly:
"""
Attaching release asset `hello-1.2.0.tar.gz'...\n
"""
......@@ -6,6 +6,7 @@ import (
"net/url"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/octokit/go-octokit/octokit"
......@@ -245,93 +246,128 @@ func (client *Client) CreateRepository(project *Project, description, homepage s
return
}
func (client *Client) Releases(project *Project) (releases []octokit.Release, err error) {
url, err := octokit.ReleasesURL.Expand(octokit.M{"owner": project.Owner, "repo": project.Name})
if err != nil {
return
}
type Release struct {
Name string `json:"name"`
TagName string `json:"tag_name"`
TargetCommitish string `json:"target_commitish"`
Body string `json:"body"`
Draft bool `json:"draft"`
Prerelease bool `json:"prerelease"`
Assets []ReleaseAsset `json:"assets"`
TarballUrl string `json:"tarball_url"`
ZipballUrl string `json:"zipball_url"`
HtmlUrl string `json:"html_url"`
UploadUrl string `json:"upload_url"`
ApiUrl string `json:"url"`
}
api, err := client.api()
type ReleaseAsset struct {
Name string `json:"name"`
Label string `json:"label"`
DownloadUrl string `json:"browser_download_url"`
ApiUrl string `json:"url"`
}
func (client *Client) FetchReleases(project *Project) (response []Release, err error) {
api, err := client.simpleApi()
if err != nil {
err = FormatError("getting release", err)
return
}
releases, result := api.Releases(client.requestURL(url)).All()
if result.HasError() {
err = FormatError("getting release", result.Err)
res, err := api.Get(fmt.Sprintf("repos/%s/%s/releases", project.Owner, project.Name))
if err = checkStatus(200, "fetching releases", res, err); err != nil {
return
}
response = []Release{}
err = res.Unmarshal(&response)
return
}
func (client *Client) Release(project *Project, tagName string) (release *octokit.Release, err error) {
url, err := octokit.ReleasesURL.Expand(octokit.M{"owner": project.Owner, "repo": project.Name})
if err != nil {
return
}
api, err := client.api()
func (client *Client) FetchRelease(project *Project, tagName string) (foundRelease *Release, err error) {
releases, err := client.FetchReleases(project)
if err != nil {
err = FormatError("getting release", err)
return
}
releases, result := api.Releases(client.requestURL(url)).All()
if result.HasError() {
err = FormatError("creating release", result.Err)
return
}
for _, release := range releases {
if release.TagName == tagName {
return &release, nil
foundRelease = &release
break
}
}
if foundRelease == nil {
err = fmt.Errorf("Unable to find release with tag name `%s'", tagName)
}
return
}
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})
func (client *Client) CreateRelease(project *Project, releaseParams *Release) (release *Release, err error) {
api, err := client.simpleApi()
if err != nil {
return
}
api, err := client.api()
res, err := api.PostJSON(fmt.Sprintf("repos/%s/%s/releases", project.Owner, project.Name), releaseParams)
if err = checkStatus(201, "creating release", res, err); err != nil {
return
}
release = &Release{}
err = res.Unmarshal(release)
return
}
func (client *Client) EditRelease(release *Release, releaseParams map[string]interface{}) (updatedRelease *Release, err error) {
api, err := client.simpleApi()
if err != nil {
err = FormatError("creating release", err)
return
}
release, result := api.Releases(client.requestURL(url)).Create(params)
if result.HasError() {
err = FormatError("creating release", result.Err)
res, err := api.PatchJSON(release.ApiUrl, releaseParams)
if err = checkStatus(200, "editing release", res, err); err != nil {
return
}
updatedRelease = &Release{}
err = res.Unmarshal(updatedRelease)
return
}
func (client *Client) UploadReleaseAsset(uploadUrl *url.URL, asset *os.File, contentType string) (err error) {
fileInfo, err := asset.Stat()
func (client *Client) UploadReleaseAsset(release *Release, filename, label string) (asset *ReleaseAsset, err error) {
api, err := client.simpleApi()
if err != nil {
return
}
api, err := client.api()
if err != nil {
err = FormatError("uploading asset", err)
parts := strings.SplitN(release.UploadUrl, "{", 2)
uploadUrl := parts[0]
uploadUrl += "?name=" + url.QueryEscape(filepath.Base(filename))
if label != "" {
uploadUrl += "&label=" + url.QueryEscape(label)
}
res, err := api.PostFile(uploadUrl, filename)
if err = checkStatus(201, "uploading release asset", res, err); err != nil {
return
}
result := api.Uploads(uploadUrl).UploadAsset(asset, contentType, fileInfo.Size())
if result.HasError() {
err = FormatError("uploading asset", result.Err)
asset = &ReleaseAsset{}
err = res.Unmarshal(asset)
return
}
func (client *Client) DeleteReleaseAsset(asset *ReleaseAsset) (err error) {
api, err := client.simpleApi()
if err != nil {
return
}
res, err := api.Delete(asset.ApiUrl)
err = checkStatus(204, "deleting release asset", res, err)
return
}
......@@ -353,11 +389,7 @@ func (client *Client) FetchCIStatus(project *Project, sha string) (status *CISta
}
res, err := api.Get(fmt.Sprintf("repos/%s/%s/commits/%s/status", project.Owner, project.Name, sha))
if err != nil {
return
}
if res.StatusCode != 200 {
err = fmt.Errorf("Unexpected HTTP status code: %d", res.StatusCode)
if err = checkStatus(200, "fetching statuses", res, err); err != nil {
return
}
......@@ -634,6 +666,21 @@ func normalizeHost(host string) string {
return host
}
func checkStatus(expectedStatus int, action string, response *simpleResponse, err error) error {
if err != nil {
return fmt.Errorf("Error %s: %s", action, err.Error())
} else if response.StatusCode != expectedStatus {
errInfo, err := response.ErrorInfo()
if err == nil {
return FormatError(action, errInfo)
} else {
return fmt.Errorf("Error %s: %s (HTTP %d)", action, err.Error(), response.StatusCode)
}
} else {
return nil
}
}
func FormatError(action string, err error) (ee error) {
switch e := err.(type) {
default:
......@@ -641,6 +688,20 @@ func FormatError(action string, err error) (ee error) {
case *AuthError:
return FormatError(action, e.Err)
case *octokit.ResponseError:
info := &errorInfo{
Message: e.Message,
Response: e.Response,
Errors: []fieldError{},
}
for _, err := range e.Errors {
info.Errors = append(info.Errors, fieldError{
Field: err.Field,
Message: err.Message,
Code: err.Code,
})
}
return FormatError(action, info)
case *errorInfo:
statusCode := e.Response.StatusCode
var reason string
if s := strings.SplitN(e.Response.Status, " ", 2); len(s) >= 2 {
......
......@@ -54,7 +54,7 @@ func (t *verboseTransport) RoundTrip(req *http.Request) (resp *http.Response, er
}
func (t *verboseTransport) dumpRequest(req *http.Request) {
info := fmt.Sprintf("> %s %s://%s%s", req.Method, req.URL.Scheme, req.Host, req.URL.Path)
info := fmt.Sprintf("> %s %s://%s%s", req.Method, req.URL.Scheme, req.Host, req.URL.RequestURI())
t.verbosePrintln(info)
t.dumpHeaders(req.Header, ">")
body := t.dumpBody(req.Body)
......@@ -179,18 +179,21 @@ type simpleClient struct {
accessToken string
}
func (c *simpleClient) Get(path string) (res *simpleResponse, err error) {
func (c *simpleClient) performRequest(method, path string, body io.Reader, configure func(*http.Request)) (res *simpleResponse, err error) {
url, err := url.Parse(path)
if err != nil {
return
}
url = c.rootUrl.ResolveReference(url)
req, err := http.NewRequest("GET", url.String(), nil)
req, err := http.NewRequest(method, url.String(), body)
if err != nil {
return
}
req.Header.Set("Authorization", "token "+c.accessToken)
if configure != nil {
configure(req)
}
httpResponse, err := c.httpClient.Do(req)
if err == nil {
......@@ -200,10 +203,72 @@ func (c *simpleClient) Get(path string) (res *simpleResponse, err error) {
return
}
func (c *simpleClient) jsonRequest(method, path string, body interface{}) (*simpleResponse, error) {
json, err := json.Marshal(body)
if err != nil {
return nil, err
}
buf := bytes.NewBuffer(json)
return c.performRequest(method, path, buf, func(req *http.Request) {
req.Header.Set("Content-Type", "application/json")
})
}
func (c *simpleClient) Get(path string) (*simpleResponse, error) {
return c.performRequest("GET", path, nil, nil)
}
func (c *simpleClient) Delete(path string) (*simpleResponse, error) {
return c.performRequest("DELETE", path, nil, nil)
}
func (c *simpleClient) PostJSON(path string, payload interface{}) (*simpleResponse, error) {
return c.jsonRequest("POST", path, payload)
}
func (c *simpleClient) PatchJSON(path string, payload interface{}) (*simpleResponse, error) {
return c.jsonRequest("PATCH", path, payload)
}
func (c *simpleClient) PostFile(path, filename string) (*simpleResponse, error) {
stat, err := os.Stat(filename)
if err != nil {
return nil, err
}
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
return c.performRequest("POST", path, file, func(req *http.Request) {
req.ContentLength = stat.Size()
req.Header.Set("Content-Type", "application/octet-stream")
})
}
type simpleResponse struct {
*http.Response
}
type errorInfo struct {
Message string `json:"message"`
Errors []fieldError `json:"errors"`
Response *http.Response
}
type fieldError struct {
Resource string `json:"resource"`
Message string `json:"message"`
Code string `json:"code"`
Field string `json:"field"`
}
func (e *errorInfo) Error() string {
return e.Message
}
func (res *simpleResponse) Unmarshal(dest interface{}) (err error) {
defer res.Body.Close()
......@@ -214,3 +279,20 @@ func (res *simpleResponse) Unmarshal(dest interface{}) (err error) {
return json.Unmarshal(body, dest)
}
func (res *simpleResponse) ErrorInfo() (msg *errorInfo, err error) {
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return
}
msg = &errorInfo{}
err = json.Unmarshal(body, msg)
if err == nil {
msg.Response = res.Response
}
return
}
#!/usr/bin/env ruby
#!/bin/bash
# Usage: script/cross-compile | script/github-release <name> <version>
#
# Takes in a list of asset filenames + labels via stdin and uploads them to the
# corresponding release on GitHub. The release is created as a draft first if
# missing and its body is the git changelog since the previous tagged release.
require "json"
require "cgi"
def github_host() ENV.fetch("GITHUB_HOST", "https://api.github.com") end
def oauth_token() ENV["GITHUB_OAUTH"] end
def escape(str) CGI.escape(str.to_s) end
def api(*args)
path = args.shift
cmd = ["curl", "-s", "--netrc"]
cmd << ( path.include?("://") ? path : File.join(github_host, path) )
cmd << "-H" << "Authorization: token #{oauth_token}" if oauth_token
if args.last.is_a? Hash
payload = JSON.dump(args.pop)
cmd << "--data" << "@-"
cmd << "-H" << "Content-Type: application/json"
end
cmd.concat args
IO.popen(cmd, payload ? "r+" : "r") do |curl|
if payload
curl.write payload
curl.close_write
end
JSON.parse(curl.read)
end
end
repo = ENV["TRAVIS_REPO_SLUG"]
project_name, version = ARGV
release = api("/repos/#{repo}/releases").find { |rel|
rel.fetch("id") && rel.fetch("tag_name") == "v#{version}"
}
unless release
release = api "/repos/#{repo}/releases",
tag_name: "v#{version}",
name: "#{project_name} #{version}",
body: `script/changelog`,
draft: true,
prerelease: version.include?('-')
end
upload_url = release.fetch("upload_url")
STDIN.each do |line|
filename, label = line.chomp.split("\t", 2)
name = File.basename filename
if asset = release["assets"].find { |a| a.fetch("name") == name }
api asset.fetch("url"), "-X", "DELETE"
end
upload = upload_url.sub(/\{\?.+?\}/, "?name=#{escape name}&label=#{escape label}")
api upload, "-H", "Content-Type: application/octet-stream", "--data-binary", "@#{filename}"
end
set -e
export GITHUB_TOKEN="${GITHUB_OAUTH?}"
project_name="${1?}"
version="${2?}"
[[ $version == *-* ]] && pre=1 || pre=
assets=()
while read -r filename label; do
assets+=( -a "${filename}#${label}" )
done
if hub release --include-drafts | grep -q "^v${version}\$"; then
hub release edit "v${version}" "${assets[@]}"
else
{ echo "${project_name} ${version}"
echo
script/changelog
} | hub release create --draft ${pre:+--prerelease} -F - "v${version}" "${assets[@]}"
fi
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册