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

import (
4
	"bytes"
5
	"fmt"
6 7
	"io"
	"net/http"
8
	"os"
9
	"path/filepath"
10
	"strings"
11
	"sync"
D
David Calavera 已提交
12
	"sync/atomic"
13 14 15 16

	"github.com/github/hub/github"
	"github.com/github/hub/utils"
	"github.com/octokit/go-octokit/octokit"
17 18
)

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

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

	flagReleaseDraft,
	flagReleasePrerelease bool
47

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

func init() {
54 55 56 57 58
	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")
59

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

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

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

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

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

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

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

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

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

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

		uploadReleaseAssets(gh, finalRelease, assetsDir)
118

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

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

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

	return editor.EditTitleAndBody()
138 139
}

140 141 142 143 144 145 146 147
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 已提交
148
	if !isDir(assetsDir) {
149 150 151
		return "", fmt.Errorf("The assets directory doesn't exist: %s", assetsDir)
	}

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

	return assetsDir, nil
}

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

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

170 171
	printUploadProgress(&countAssets, totalAssets)

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

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

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

186
				contentType := detectContentType(path, fi)
187

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

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

		return nil
	})
199 200

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

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 已提交
221 222 223 224 225

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