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

Reimplement `release create` with simpleApi instead of go-octokit

上级 0fb96e05
package commands
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/github/hub/git"
"github.com/github/hub/github"
"github.com/github/hub/ui"
"github.com/github/hub/utils"
"github.com/octokit/go-octokit/octokit"
)
var (
......@@ -51,6 +46,9 @@ With '--include-drafs', include draft releases in the listing.
-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.
......@@ -175,204 +173,99 @@ func showRelease(cmd *Command, args *Args) {
}
func createRelease(cmd *Command, args *Args) {
if args.IsParamsEmpty() {
utils.Check(fmt.Errorf("Missed argument TAG"))
tagName := args.LastParam()
if tagName == "" {
utils.Check(fmt.Errorf("Missing argument TAG"))
return
}
tag := args.LastParam()
runInLocalRepo(func(localRepo *github.GitHubRepo, project *github.Project, client *github.Client) {
release, err := client.Release(project, tag)
utils.Check(err)
if release == nil {
commitish := flagReleaseCommitish
if commitish == "" {
currentBranch, err := localRepo.CurrentBranch()
utils.Check(err)
commitish = currentBranch.ShortName()
}
title, body, err := getTitleAndBodyFromFlags(flagReleaseMessage, flagReleaseFile)
utils.Check(err)
var editor *github.Editor
if title == "" {
cs := git.CommentChar()
message, err := renderReleaseTpl(cs, tag, project.Name, commitish)
utils.Check(err)
editor, err = github.NewEditor("RELEASE", "release", message)
utils.Check(err)
title, body, err = editor.EditTitleAndBody()
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)
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)
paths = append(paths, p...)
}
uploader := assetUploader{
Client: client,
Release: release,
}
err = uploader.UploadAll(paths)
if err != nil {
ui.Println("")
utils.Check(err)
}
}
ui.Printf("\n%s\n", release.HTMLURL)
})
}
type assetUploader struct {
Client *github.Client
Release *octokit.Release
}
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)
}
localRepo, err := github.LocalRepo()
utils.Check(err)
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)
}
project, err := localRepo.CurrentProject()
utils.Check(err)
if count == total {
break
}
}
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 flagReleaseMessage != "" {
title, body = readMsg(flagReleaseMessage)
} else if flagReleaseFile != "" {
title, body, err = readMsgFromFile(flagReleaseMessage)
utils.Check(err)
} else {
errUploadChan <- path
}
}
func (a *assetUploader) Upload(path string) error {
contentType, err := a.detectContentType(path)
if err != nil {
return err
}
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
uploadUrl, err := a.Release.UploadURL.Expand(octokit.M{"name": filepath.Base(path)})
if err != nil {
return err
}
cs := git.CommentChar()
message, err := renderReleaseTpl(cs, tagName, project.String(), commitish)
utils.Check(err)
return a.Client.UploadReleaseAsset(uploadUrl, f, contentType)
}
editor, err := github.NewEditor("RELEASE", "release", message)
utils.Check(err)
func (a *assetUploader) detectContentType(path string) (string, error) {
file, err := os.Open(path)
if err != nil {
return "", err
title, body, err = editor.EditTitleAndBody()
utils.Check(err)
}
defer file.Close()
fi, err := file.Stat()
if err != nil {
return "", err
if title == "" {
utils.Check(fmt.Errorf("Aborting release due to empty release title"))
}
fileHeader := &bytes.Buffer{}
headerSize := int64(512)
if fi.Size() < headerSize {
headerSize = fi.Size()
params := &github.Release{
TagName: tagName,
TargetCommitish: commitish,
Name: title,
Body: body,
Draft: flagReleaseDraft,
Prerelease: flagReleasePrerelease,
}
// 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
}
var release *github.Release
t := http.DetectContentType(fileHeader.Bytes())
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)
return strings.Split(t, ";")[0], nil
}
if editor != nil {
defer editor.DeleteFile()
}
func (a *assetUploader) printUploadProgress(count int, total int) {
out := fmt.Sprintf("Uploading assets (%d/%d)", count, total)
fmt.Print("\r" + out)
}
ui.Println(release.HtmlUrl)
}
type assetFinder struct {
uploadAssets(gh, release, flagReleaseAssets, args)
os.Exit(0)
}
func (a *assetFinder) Find(path string) ([]string, error) {
result := make([]string, 0)
err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
if err != nil {
return 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 !info.IsDir() {
result = append(result, path)
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 {
ui.Errorf("Attaching release asset `%s'...\n", asset)
_, err := gh.UploadReleaseAsset(release, asset, label)
utils.Check(err)
}
return nil
})
return result, 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,7 +5,8 @@ import (
"text/template"
)
const releaseTmpl = `{{.CS}} Creating release {{.TagName}} for {{.ProjectName}} from {{.BranchName}}
const releaseTmpl = `
{{.CS}} Creating 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.`
......
......@@ -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) {
......
......@@ -6,6 +6,7 @@ import (
"net/url"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/octokit/go-octokit/octokit"
......@@ -246,19 +247,22 @@ func (client *Client) CreateRepository(project *Project, description, homepage s
}
type Release struct {
Name string `json:"name"`
TagName string `json:"tag_name"`
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"`
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"`
}
type ReleaseAsset struct {
Name string `json:"name"`
Label string `json:"label"`
Name string `json:"name"`
Label string `json:"label"`
DownloadUrl string `json:"browser_download_url"`
}
......@@ -302,72 +306,50 @@ func (client *Client) FetchRelease(project *Project, tagName string) (foundRelea
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})
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 != nil {
err = FormatError("getting release", err)
return
}
releases, result := api.Releases(client.requestURL(url)).All()
if result.HasError() {
err = FormatError("creating release", result.Err)
if res.StatusCode != 201 {
err = fmt.Errorf("Unexpected HTTP status code: %d", res.StatusCode)
return
}
for _, release := range releases {
if release.TagName == tagName {
return &release, nil
}
}
release = &Release{}
err = res.Unmarshal(release)
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})
if err != nil {
return
}
api, err := client.api()
func (client *Client) UploadReleaseAsset(release *Release, filename, label string) (asset *ReleaseAsset, 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)
return
}
return
}
func (client *Client) UploadReleaseAsset(uploadUrl *url.URL, asset *os.File, contentType string) (err error) {
fileInfo, err := asset.Stat()
if err != nil {
return
parts := strings.SplitN(release.UploadUrl, "{", 2)
uploadUrl := parts[0]
uploadUrl += "?name=" + url.QueryEscape(filepath.Base(filename))
if label != "" {
uploadUrl += "&label=" + url.QueryEscape(label)
}
api, err := client.api()
res, err := api.PostFile(uploadUrl, filename)
if err != nil {
err = FormatError("uploading asset", err)
return
}
result := api.Uploads(uploadUrl).UploadAsset(asset, contentType, fileInfo.Size())
if result.HasError() {
err = FormatError("uploading asset", result.Err)
if res.StatusCode != 201 {
err = fmt.Errorf("Unexpected HTTP status code: %d", res.StatusCode)
return
}
asset = &ReleaseAsset{}
err = res.Unmarshal(asset)
return
}
......
......@@ -200,6 +200,66 @@ func (c *simpleClient) Get(path string) (res *simpleResponse, err error) {
return
}
func (c *simpleClient) PostJSON(path string, payload interface{}) (res *simpleResponse, err error) {
url, err := url.Parse(path)
if err != nil {
return
}
json, err := json.Marshal(payload)
buf := bytes.NewBuffer(json)
url = c.rootUrl.ResolveReference(url)
req, err := http.NewRequest("POST", url.String(), buf)
if err != nil {
return
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "token "+c.accessToken)
httpResponse, err := c.httpClient.Do(req)
if err == nil {
res = &simpleResponse{httpResponse}
}
return
}
func (c *simpleClient) PostFile(path, filename string) (res *simpleResponse, err error) {
url, err := url.Parse(path)
if err != nil {
return
}
stat, err := os.Stat(filename)
if err != nil {
return
}
fileSize := stat.Size()
file, err := os.Open(filename)
if err != nil {
return
}
defer file.Close()
url = c.rootUrl.ResolveReference(url)
req, err := http.NewRequest("POST", url.String(), file)
if err != nil {
return
}
req.ContentLength = fileSize
req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("Authorization", "token "+c.accessToken)
httpResponse, err := c.httpClient.Do(req)
if err == nil {
res = &simpleResponse{httpResponse}
}
return
}
type simpleResponse struct {
*http.Response
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册