release.go 5.7 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
	"io"
10
	"io/ioutil"
11
	"net/http"
12
	"os"
13
	"path/filepath"
14
	"strings"
15
	"sync"
D
David Calavera 已提交
16
	"sync/atomic"
17 18
)

D
David Calavera 已提交
19 20 21 22 23 24 25 26 27
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,
D
David Calavera 已提交
28
		Usage: "release [-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 46 47 48
)

func init() {
	cmdRelease.Flag.BoolVar(&flagReleaseDraft, "d", false, "DRAFT")
	cmdRelease.Flag.BoolVar(&flagReleasePrerelease, "p", false, "PRERELEASE")
	cmdRelease.Flag.StringVar(&flagReleaseAssetsDir, "a", "", "ASSETS_DIR")
49 50
	cmdRelease.Flag.StringVar(&flagReleaseMessage, "m", "", "MESSAGE")
	cmdRelease.Flag.StringVar(&flagReleaseFile, "f", "", "FILE")
D
David Calavera 已提交
51 52 53
}

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

func release(cmd *Command, args *Args) {
D
David Calavera 已提交
72
	tag := args.LastParam()
73

74 75 76
	assetsDir, err := getAssetsDirectory(flagReleaseAssetsDir, tag)
	utils.Check(err)

77 78 79
	runInLocalRepo(func(localRepo *github.GitHubRepo, project *github.Project, gh *github.Client) {
		currentBranch, err := localRepo.CurrentBranch()
		utils.Check(err)
80
		branchName := currentBranch.ShortName()
81

82
		title, body, err := github.GetTitleAndBodyFromFlags(flagReleaseMessage, flagReleaseFile)
83 84 85
		utils.Check(err)

		if title == "" {
86
			title, body, err = writeReleaseTitleAndBody(project, tag, branchName)
87 88 89 90 91
			utils.Check(err)
		}

		params := octokit.ReleaseParams{
			TagName:         tag,
92
			TargetCommitish: branchName,
93 94 95 96 97
			Name:            title,
			Body:            body,
			Draft:           flagReleaseDraft,
			Prerelease:      flagReleasePrerelease}

98 99 100 101
		finalRelease, err := gh.CreateRelease(project, params)
		utils.Check(err)

		uploadReleaseAssets(gh, finalRelease, assetsDir)
102

D
David Calavera 已提交
103
		fmt.Printf("\n\nRelease created: %s", finalRelease.HTMLURL)
104
	})
D
David Calavera 已提交
105 106
}

107
func writeReleaseTitleAndBody(project *github.Project, tag, currentBranch string) (string, string, error) {
108
	return github.GetTitleAndBodyFromEditor("RELEASE", func(messageFile string) error {
109 110 111 112 113 114 115 116 117 118 119 120
		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)
	})
}

121
func runInLocalRepo(fn func(localRepo *github.GitHubRepo, project *github.Project, client *github.Client)) {
J
Jingwen Owen Ou 已提交
122 123 124 125
	localRepo := github.LocalRepo()
	project, err := localRepo.CurrentProject()
	utils.Check(err)

D
David Calavera 已提交
126
	client := github.NewClient(project.Host)
127
	fn(localRepo, project, client)
128 129 130

	os.Exit(0)
}
131 132 133 134 135 136 137 138 139

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

144
	if isEmptyDir(assetsDir) {
145 146 147 148 149 150 151
		return "", fmt.Errorf("The assets directory is empty: %s", assetsDir)
	}

	return assetsDir, nil
}

func uploadReleaseAssets(gh *github.Client, release *octokit.Release, assetsDir string) {
152
	var wg sync.WaitGroup
D
David Calavera 已提交
153 154 155 156 157 158 159 160
	var totalAssets, countAssets uint64

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

162 163
	filepath.Walk(assetsDir, func(path string, fi os.FileInfo, err error) error {
		if !fi.IsDir() {
164
			wg.Add(1)
165

166
			go func() {
D
David Calavera 已提交
167 168 169 170 171 172 173 174
				defer func() {
					atomic.AddUint64(&countAssets, uint64(1))
					printUploadProgress(&countAssets, totalAssets)
					wg.Done()
				}()

				printUploadProgress(&countAssets, totalAssets)

175 176 177
				uploadUrl, err := release.UploadURL.Expand(octokit.M{"name": fi.Name()})
				utils.Check(err)

178
				contentType := detectContentType(path, fi)
179

180 181 182 183
				file, err := os.Open(path)
				utils.Check(err)
				defer file.Close()

184
				err = gh.UploadReleaseAsset(uploadUrl, file, contentType)
185 186
				utils.Check(err)
			}()
187 188 189 190
		}

		return nil
	})
191 192

	wg.Wait()
193
}
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212

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 已提交
213 214 215 216 217

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