release.go 5.8 KB
Newer Older
1 2 3
package commands

import (
4
	"bytes"
5
	"fmt"
J
Jingwen Owen Ou 已提交
6 7
	"github.com/github/hub/github"
	"github.com/github/hub/utils"
8
	"github.com/octokit/go-octokit/octokit"
9 10
	"io"
	"net/http"
11
	"os"
12
	"path/filepath"
13
	"strings"
14
	"sync"
D
David Calavera 已提交
15
	"sync/atomic"
16 17
)

D
David Calavera 已提交
18
var (
19 20 21
	cmdRelease = &Command{
		Run:   release,
		Usage: "release",
D
David Calavera 已提交
22
		Short: "Retrieve releases from GitHub",
J
Jingwen Owen Ou 已提交
23
		Long:  `Retrieves releases from GitHub for the project that the "origin" remote points to.`}
D
David Calavera 已提交
24

25
	cmdCreateRelease = &Command{
26
		Key:   "create",
27
		Run:   createRelease,
J
Jingwen Owen Ou 已提交
28
		Usage: "release create [-d] [-p] [-a <ASSETS_DIR>] [-m <MESSAGE>|-f <FILE>] <TAG>",
D
David Calavera 已提交
29
		Short: "Create a new release in GitHub",
J
Jingwen Owen Ou 已提交
30 31 32 33 34 35 36 37 38 39 40 41
		Long: `Creates 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.

Specify the assets to include in the release from a directory via "-a". Without
"-a", it finds assets from "releases/TAG" of the current directory.

Without <MESSAGE> or <FILE>, a text editor will open in which title and body
of the release can be entered in the same manner as git commit message.

If "-d" is given, it creates a draft release.

If "-p" is given, it creates a pre-release.
D
David Calavera 已提交
42 43 44 45
`}

	flagReleaseDraft,
	flagReleasePrerelease bool
46

47
	flagReleaseAssetsDir,
48 49
	flagReleaseMessage,
	flagReleaseFile string
D
David Calavera 已提交
50 51 52
)

func init() {
53 54 55 56 57
	cmdCreateRelease.Flag.BoolVarP(&flagReleaseDraft, "draft", "d", false, "DRAFT")
	cmdCreateRelease.Flag.BoolVarP(&flagReleasePrerelease, "prerelease", "p", false, "PRERELEASE")
	cmdCreateRelease.Flag.StringVarP(&flagReleaseAssetsDir, "assets", "a", "", "ASSETS_DIR")
	cmdCreateRelease.Flag.StringVarP(&flagReleaseMessage, "message", "m", "", "MESSAGE")
	cmdCreateRelease.Flag.StringVarP(&flagReleaseFile, "file", "f", "", "FILE")
58

59
	cmdRelease.Use(cmdCreateRelease)
60
	CmdRunner.Use(cmdRelease)
D
David Calavera 已提交
61 62
}

63
func release(cmd *Command, args *Args) {
64
	runInLocalRepo(func(localRepo *github.GitHubRepo, project *github.Project, gh *github.Client) {
D
David Calavera 已提交
65 66 67 68 69 70 71 72 73 74 75 76 77 78
		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"))
		}
	})
79 80
}

81
func createRelease(cmd *Command, args *Args) {
D
David Calavera 已提交
82 83 84 85 86
	if args.IsParamsEmpty() {
		utils.Check(fmt.Errorf("Missed argument TAG"))
		return
	}

D
David Calavera 已提交
87
	tag := args.LastParam()
88

89 90 91
	assetsDir, err := getAssetsDirectory(flagReleaseAssetsDir, tag)
	utils.Check(err)

92
	runInLocalRepo(func(localRepo *github.GitHubRepo, project *github.Project, gh *github.Client) {
93 94
		currentBranch, err := localRepo.CurrentBranch()
		utils.Check(err)
95
		branchName := currentBranch.ShortName()
96

97
		title, body, err := getTitleAndBodyFromFlags(flagReleaseMessage, flagReleaseFile)
98 99 100
		utils.Check(err)

		if title == "" {
101
			title, body, err = writeReleaseTitleAndBody(project, tag, branchName)
102 103 104 105 106
			utils.Check(err)
		}

		params := octokit.ReleaseParams{
			TagName:         tag,
107
			TargetCommitish: branchName,
108 109 110 111 112
			Name:            title,
			Body:            body,
			Draft:           flagReleaseDraft,
			Prerelease:      flagReleasePrerelease}

113 114 115 116
		finalRelease, err := gh.CreateRelease(project, params)
		utils.Check(err)

		uploadReleaseAssets(gh, finalRelease, assetsDir)
117

D
David Calavera 已提交
118
		fmt.Printf("\n\nRelease created: %s", finalRelease.HTMLURL)
119
	})
D
David Calavera 已提交
120 121
}

122
func writeReleaseTitleAndBody(project *github.Project, tag, currentBranch string) (string, string, error) {
123
	message := `
124 125 126 127 128
# 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.
`
129
	message = fmt.Sprintf(message, tag, project.Name, currentBranch)
130

131 132 133 134 135 136
	editor, err := github.NewEditor("RELEASE", message)
	if err != nil {
		return "", "", err
	}

	return editor.EditTitleAndBody()
137 138
}

139 140 141 142 143 144 145 146
func getAssetsDirectory(assetsDir, tag string) (string, error) {
	if assetsDir == "" {
		pwd, err := os.Getwd()
		utils.Check(err)

		assetsDir = filepath.Join(pwd, "releases", tag)
	}

D
David Calavera 已提交
147
	if !isDir(assetsDir) {
148 149 150
		return "", fmt.Errorf("The assets directory doesn't exist: %s", assetsDir)
	}

151
	if isEmptyDir(assetsDir) {
152 153 154 155 156 157 158
		return "", fmt.Errorf("The assets directory is empty: %s", assetsDir)
	}

	return assetsDir, nil
}

func uploadReleaseAssets(gh *github.Client, release *octokit.Release, assetsDir string) {
159
	var wg sync.WaitGroup
D
David Calavera 已提交
160 161 162 163 164 165 166 167
	var totalAssets, countAssets uint64

	filepath.Walk(assetsDir, func(path string, fi os.FileInfo, err error) error {
		if !fi.IsDir() {
			totalAssets += 1
		}
		return nil
	})
168

169 170
	printUploadProgress(&countAssets, totalAssets)

171 172
	filepath.Walk(assetsDir, func(path string, fi os.FileInfo, err error) error {
		if !fi.IsDir() {
173
			wg.Add(1)
174

175
			go func() {
D
David Calavera 已提交
176 177 178 179 180 181
				defer func() {
					atomic.AddUint64(&countAssets, uint64(1))
					printUploadProgress(&countAssets, totalAssets)
					wg.Done()
				}()

182 183 184
				uploadUrl, err := release.UploadURL.Expand(octokit.M{"name": fi.Name()})
				utils.Check(err)

185
				contentType := detectContentType(path, fi)
186

187 188 189 190
				file, err := os.Open(path)
				utils.Check(err)
				defer file.Close()

191
				err = gh.UploadReleaseAsset(uploadUrl, file, contentType)
192 193
				utils.Check(err)
			}()
194 195 196 197
		}

		return nil
	})
198 199

	wg.Wait()
200
}
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219

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())
}
D
David Calavera 已提交
220 221 222 223 224

func printUploadProgress(count *uint64, total uint64) {
	out := fmt.Sprintf("Uploading assets (%d/%d)", atomic.LoadUint64(count), total)
	fmt.Print("\r" + out)
}