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

import (
4
	"bytes"
5 6 7
	"fmt"
	"github.com/jingweno/gh/github"
	"github.com/jingweno/gh/utils"
8
	"github.com/jingweno/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 23 24
		Short: "Retrieve releases from GitHub",
		Long:  `Retrieve releases from GitHub for the project that the "origin" remote points to.`}

25
	cmdCreateRelease = &Command{
26
		Key:   "create",
27 28
		Run:   createRelease,
		Usage: "release create [-d] [-p] [-a <ASSETS_DIR>] [-m <MESSAGE>|-f <FILE>] TAG",
D
David Calavera 已提交
29 30 31 32 33 34 35 36 37 38
		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
39

40
	flagReleaseAssetsDir,
41 42
	flagReleaseMessage,
	flagReleaseFile string
D
David Calavera 已提交
43 44 45
)

func init() {
46 47 48 49 50
	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")
51

52
	cmdRelease.Use(cmdCreateRelease)
53
	CmdRunner.Use(cmdRelease)
D
David Calavera 已提交
54 55
}

56
func release(cmd *Command, args *Args) {
D
David Calavera 已提交
57
	RunInLocalRepo(func(localRepo *github.GitHubRepo, project *github.Project, gh *github.Client) {
D
David Calavera 已提交
58 59 60 61 62 63 64 65 66 67 68 69 70 71
		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"))
		}
	})
72 73
}

74
func createRelease(cmd *Command, args *Args) {
D
David Calavera 已提交
75 76 77 78 79
	if args.IsParamsEmpty() {
		utils.Check(fmt.Errorf("Missed argument TAG"))
		return
	}

D
David Calavera 已提交
80
	tag := args.LastParam()
81

82 83 84
	assetsDir, err := getAssetsDirectory(flagReleaseAssetsDir, tag)
	utils.Check(err)

D
David Calavera 已提交
85
	RunInLocalRepo(func(localRepo *github.GitHubRepo, project *github.Project, gh *github.Client) {
86 87
		currentBranch, err := localRepo.CurrentBranch()
		utils.Check(err)
88
		branchName := currentBranch.ShortName()
89

90
		title, body, err := github.GetTitleAndBodyFromFlags(flagReleaseMessage, flagReleaseFile)
91 92 93
		utils.Check(err)

		if title == "" {
94
			title, body, err = writeReleaseTitleAndBody(project, tag, branchName)
95 96 97 98 99
			utils.Check(err)
		}

		params := octokit.ReleaseParams{
			TagName:         tag,
100
			TargetCommitish: branchName,
101 102 103 104 105
			Name:            title,
			Body:            body,
			Draft:           flagReleaseDraft,
			Prerelease:      flagReleasePrerelease}

106 107 108 109
		finalRelease, err := gh.CreateRelease(project, params)
		utils.Check(err)

		uploadReleaseAssets(gh, finalRelease, assetsDir)
110

D
David Calavera 已提交
111
		fmt.Printf("\n\nRelease created: %s", finalRelease.HTMLURL)
112
	})
D
David Calavera 已提交
113 114
}

115
func writeReleaseTitleAndBody(project *github.Project, tag, currentBranch string) (string, string, error) {
116
	message := `
117 118 119 120 121
# 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.
`
122
	message = fmt.Sprintf(message, tag, project.Name, currentBranch)
123

124
	return github.GetTitleAndBodyFromEditor("RELEASE", message)
125 126
}

127 128 129 130 131 132 133 134
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 已提交
135
	if !isDir(assetsDir) {
136 137 138
		return "", fmt.Errorf("The assets directory doesn't exist: %s", assetsDir)
	}

139
	if isEmptyDir(assetsDir) {
140 141 142 143 144 145 146
		return "", fmt.Errorf("The assets directory is empty: %s", assetsDir)
	}

	return assetsDir, nil
}

func uploadReleaseAssets(gh *github.Client, release *octokit.Release, assetsDir string) {
147
	var wg sync.WaitGroup
D
David Calavera 已提交
148 149 150 151 152 153 154 155
	var totalAssets, countAssets uint64

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

157 158
	printUploadProgress(&countAssets, totalAssets)

159 160
	filepath.Walk(assetsDir, func(path string, fi os.FileInfo, err error) error {
		if !fi.IsDir() {
161
			wg.Add(1)
162

163
			go func() {
D
David Calavera 已提交
164 165 166 167 168 169
				defer func() {
					atomic.AddUint64(&countAssets, uint64(1))
					printUploadProgress(&countAssets, totalAssets)
					wg.Done()
				}()

170 171 172
				uploadUrl, err := release.UploadURL.Expand(octokit.M{"name": fi.Name()})
				utils.Check(err)

173
				contentType := detectContentType(path, fi)
174

175 176 177 178
				file, err := os.Open(path)
				utils.Check(err)
				defer file.Close()

179
				err = gh.UploadReleaseAsset(uploadUrl, file, contentType)
180 181
				utils.Check(err)
			}()
182 183 184 185
		}

		return nil
	})
186 187

	wg.Wait()
188
}
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207

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 已提交
208 209 210 211 212

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