提交 58c77c72 编写于 作者: D David Calavera

Merge pull request jingweno/gh:129 from calavera/gh_release

Add command to create releases.
......@@ -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",
......
......@@ -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)
}
}
......@@ -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)
}
......
......@@ -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"`
......
......@@ -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())
......
......@@ -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
}
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
}
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)
}
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)
}
......@@ -77,6 +77,7 @@ var GitHub = []*Command{
cmdCiStatus,
cmdBrowse,
cmdCompare,
cmdReleases,
cmdRelease,
cmdIssue,
}
......
......@@ -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 <command>' for more information on a specific command.
......
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,25 +141,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, err := github.GetTitleAndBodyFromFlags(flagPullRequestMessage, flagPullRequestFile)
utils.Check(err)
fullBase := fmt.Sprintf("%s:%s", baseProject.Owner, base)
fullHead := fmt.Sprintf("%s:%s", headProject.Owner, head)
......@@ -179,10 +158,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 +188,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 github.GetTitleAndBodyFromEditor("PULLREQ", func(messageFile string) error {
return writePullRequestChanges(base, head, fullBase, fullHead, commits, messageFile)
})
}
func writePullRequestChanges(base, head, fullBase, fullHead string, commits []string, messageFile string) error {
......@@ -282,87 +234,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
......
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"}
......
package commands
import (
"bytes"
"fmt"
"github.com/jingweno/gh/github"
"github.com/jingweno/gh/utils"
"github.com/jingweno/go-octokit/octokit"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"sync/atomic"
)
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 [-d] [-p] [-a <ASSETS_DIR>] [-m <MESSAGE>|-f <FILE>] 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.
- 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,
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")
cmdRelease.Flag.StringVar(&flagReleaseMessage, "m", "", "MESSAGE")
cmdRelease.Flag.StringVar(&flagReleaseFile, "f", "", "FILE")
}
func releases(cmd *Command, args *Args) {
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 {
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) {
if args.IsParamsEmpty() {
utils.Check(fmt.Errorf("Missed argument TAG"))
return
}
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)
branchName := currentBranch.ShortName()
title, body, err := github.GetTitleAndBodyFromFlags(flagReleaseMessage, flagReleaseFile)
utils.Check(err)
if title == "" {
title, body, err = writeReleaseTitleAndBody(project, tag, branchName)
utils.Check(err)
}
params := octokit.ReleaseParams{
TagName: tag,
TargetCommitish: branchName,
Name: title,
Body: body,
Draft: flagReleaseDraft,
Prerelease: flagReleasePrerelease}
finalRelease, err := gh.CreateRelease(project, params)
utils.Check(err)
uploadReleaseAssets(gh, finalRelease, assetsDir)
fmt.Printf("\n\nRelease created: %s", finalRelease.HTMLURL)
})
}
func writeReleaseTitleAndBody(project *github.Project, tag, currentBranch string) (string, string, error) {
return github.GetTitleAndBodyFromEditor("RELEASE", 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()
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)
client := github.NewClient(project.Host)
fn(localRepo, project, client)
os.Exit(0)
}
func getAssetsDirectory(assetsDir, tag string) (string, error) {
if assetsDir == "" {
pwd, err := os.Getwd()
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)
assetsDir = filepath.Join(pwd, "releases", tag)
}
if !isDir(assetsDir) {
return "", fmt.Errorf("The assets directory doesn't exist: %s", assetsDir)
}
if 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) {
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 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)
contentType := detectContentType(path, fi)
file, err := os.Open(path)
utils.Check(err)
defer file.Close()
err = gh.UploadReleaseAsset(uploadUrl, file, contentType)
utils.Check(err)
}()
}
fmt.Println(strings.Join(outputs, "\n\n"))
return nil
})
wg.Wait()
}
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()
}
os.Exit(0)
// 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())
}
func printUploadProgress(count *uint64, total uint64) {
out := fmt.Sprintf("Uploading assets (%d/%d)", atomic.LoadUint64(count), total)
fmt.Print("\r" + out)
}
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))
}
......@@ -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
}
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.T(t, !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
}
......@@ -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 {
......
......@@ -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 {
......
......@@ -117,9 +117,31 @@ 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
}
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
}
release, result := client.octokit().Releases(client.requestURL(url)).Create(params)
if result.HasError() {
err = result.Err
}
return
}
func (client *Client) UploadReleaseAsset(uploadUrl *url.URL, asset *os.File, contentType string) (err error) {
c := client.octokit()
result := c.Uploads(uploadUrl).UploadAsset(asset, contentType)
if result.HasError() {
err = result.Err
}
return
}
......
package github
import (
"bufio"
"fmt"
"github.com/jingweno/gh/cmd"
"github.com/jingweno/gh/git"
"github.com/jingweno/gh/utils"
"io"
"io/ioutil"
"os"
"path/filepath"
"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(about string, fn func(messageFile string) error) (title, body string, err error) {
messageFile, err := getMessageFile(about)
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 title/body 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
}
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
}
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)
}
func TestGetMessageFile(t *testing.T) {
gitPullReqMsgFile, _ := getMessageFile("PULLREQ")
assert.T(t, strings.Contains(gitPullReqMsgFile, "PULLREQ_EDITMSG"))
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册