github.com/pengwynn/gh@v1.0.1-0.20140118055701-14327ca3942e/commands/release.go (about)

     1  package commands
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"github.com/jingweno/gh/github"
     7  	"github.com/jingweno/gh/utils"
     8  	"github.com/jingweno/go-octokit/octokit"
     9  	"io"
    10  	"net/http"
    11  	"os"
    12  	"path/filepath"
    13  	"strings"
    14  	"sync"
    15  	"sync/atomic"
    16  )
    17  
    18  var (
    19  	cmdRelease = &Command{
    20  		Run:   release,
    21  		Usage: "release",
    22  		Short: "Retrieve releases from GitHub",
    23  		Long:  `Retrieves releases from GitHub for the project that the "origin" remote points to.`}
    24  
    25  	cmdCreateRelease = &Command{
    26  		Key:   "create",
    27  		Run:   createRelease,
    28  		Usage: "release create [-d] [-p] [-a <ASSETS_DIR>] [-m <MESSAGE>|-f <FILE>] <TAG>",
    29  		Short: "Create a new release in GitHub",
    30  		Long: `Creates a new release in GitHub for the project that the "origin" remote points to.
    31  It requires the name of the tag to release as a first argument.
    32  
    33  Specify the assets to include in the release from a directory via "-a". Without
    34  "-a", it finds assets from "releases/TAG" of the current directory.
    35  
    36  Without <MESSAGE> or <FILE>, a text editor will open in which title and body
    37  of the release can be entered in the same manner as git commit message.
    38  
    39  If "-d" is given, it creates a draft release.
    40  
    41  If "-p" is given, it creates a pre-release.
    42  `}
    43  
    44  	flagReleaseDraft,
    45  	flagReleasePrerelease bool
    46  
    47  	flagReleaseAssetsDir,
    48  	flagReleaseMessage,
    49  	flagReleaseFile string
    50  )
    51  
    52  func init() {
    53  	cmdCreateRelease.Flag.BoolVarP(&flagReleaseDraft, "draft", "d", false, "DRAFT")
    54  	cmdCreateRelease.Flag.BoolVarP(&flagReleasePrerelease, "prerelease", "p", false, "PRERELEASE")
    55  	cmdCreateRelease.Flag.StringVarP(&flagReleaseAssetsDir, "assets", "a", "", "ASSETS_DIR")
    56  	cmdCreateRelease.Flag.StringVarP(&flagReleaseMessage, "message", "m", "", "MESSAGE")
    57  	cmdCreateRelease.Flag.StringVarP(&flagReleaseFile, "file", "f", "", "FILE")
    58  
    59  	cmdRelease.Use(cmdCreateRelease)
    60  	CmdRunner.Use(cmdRelease)
    61  }
    62  
    63  func release(cmd *Command, args *Args) {
    64  	runInLocalRepo(func(localRepo *github.GitHubRepo, project *github.Project, gh *github.Client) {
    65  		if args.Noop {
    66  			fmt.Printf("Would request list of releases for %s\n", project)
    67  		} else {
    68  			releases, err := gh.Releases(project)
    69  			utils.Check(err)
    70  			var outputs []string
    71  			for _, release := range releases {
    72  				out := fmt.Sprintf("%s (%s)\n%s", release.Name, release.TagName, release.Body)
    73  				outputs = append(outputs, out)
    74  			}
    75  
    76  			fmt.Println(strings.Join(outputs, "\n\n"))
    77  		}
    78  	})
    79  }
    80  
    81  func createRelease(cmd *Command, args *Args) {
    82  	if args.IsParamsEmpty() {
    83  		utils.Check(fmt.Errorf("Missed argument TAG"))
    84  		return
    85  	}
    86  
    87  	tag := args.LastParam()
    88  
    89  	assetsDir, err := getAssetsDirectory(flagReleaseAssetsDir, tag)
    90  	utils.Check(err)
    91  
    92  	runInLocalRepo(func(localRepo *github.GitHubRepo, project *github.Project, gh *github.Client) {
    93  		currentBranch, err := localRepo.CurrentBranch()
    94  		utils.Check(err)
    95  		branchName := currentBranch.ShortName()
    96  
    97  		title, body, err := getTitleAndBodyFromFlags(flagReleaseMessage, flagReleaseFile)
    98  		utils.Check(err)
    99  
   100  		if title == "" {
   101  			title, body, err = writeReleaseTitleAndBody(project, tag, branchName)
   102  			utils.Check(err)
   103  		}
   104  
   105  		params := octokit.ReleaseParams{
   106  			TagName:         tag,
   107  			TargetCommitish: branchName,
   108  			Name:            title,
   109  			Body:            body,
   110  			Draft:           flagReleaseDraft,
   111  			Prerelease:      flagReleasePrerelease}
   112  
   113  		finalRelease, err := gh.CreateRelease(project, params)
   114  		utils.Check(err)
   115  
   116  		uploadReleaseAssets(gh, finalRelease, assetsDir)
   117  
   118  		fmt.Printf("\n\nRelease created: %s", finalRelease.HTMLURL)
   119  	})
   120  }
   121  
   122  func writeReleaseTitleAndBody(project *github.Project, tag, currentBranch string) (string, string, error) {
   123  	message := `
   124  # Creating release %s for %s from %s
   125  #
   126  # Write a message for this release. The first block
   127  # of the text is the title and the rest is description.
   128  `
   129  	message = fmt.Sprintf(message, tag, project.Name, currentBranch)
   130  
   131  	editor, err := github.NewEditor("RELEASE", message)
   132  	if err != nil {
   133  		return "", "", err
   134  	}
   135  
   136  	return editor.EditTitleAndBody()
   137  }
   138  
   139  func getAssetsDirectory(assetsDir, tag string) (string, error) {
   140  	if assetsDir == "" {
   141  		pwd, err := os.Getwd()
   142  		utils.Check(err)
   143  
   144  		assetsDir = filepath.Join(pwd, "releases", tag)
   145  	}
   146  
   147  	if !isDir(assetsDir) {
   148  		return "", fmt.Errorf("The assets directory doesn't exist: %s", assetsDir)
   149  	}
   150  
   151  	if isEmptyDir(assetsDir) {
   152  		return "", fmt.Errorf("The assets directory is empty: %s", assetsDir)
   153  	}
   154  
   155  	return assetsDir, nil
   156  }
   157  
   158  func uploadReleaseAssets(gh *github.Client, release *octokit.Release, assetsDir string) {
   159  	var wg sync.WaitGroup
   160  	var totalAssets, countAssets uint64
   161  
   162  	filepath.Walk(assetsDir, func(path string, fi os.FileInfo, err error) error {
   163  		if !fi.IsDir() {
   164  			totalAssets += 1
   165  		}
   166  		return nil
   167  	})
   168  
   169  	printUploadProgress(&countAssets, totalAssets)
   170  
   171  	filepath.Walk(assetsDir, func(path string, fi os.FileInfo, err error) error {
   172  		if !fi.IsDir() {
   173  			wg.Add(1)
   174  
   175  			go func() {
   176  				defer func() {
   177  					atomic.AddUint64(&countAssets, uint64(1))
   178  					printUploadProgress(&countAssets, totalAssets)
   179  					wg.Done()
   180  				}()
   181  
   182  				uploadUrl, err := release.UploadURL.Expand(octokit.M{"name": fi.Name()})
   183  				utils.Check(err)
   184  
   185  				contentType := detectContentType(path, fi)
   186  
   187  				file, err := os.Open(path)
   188  				utils.Check(err)
   189  				defer file.Close()
   190  
   191  				err = gh.UploadReleaseAsset(uploadUrl, file, contentType)
   192  				utils.Check(err)
   193  			}()
   194  		}
   195  
   196  		return nil
   197  	})
   198  
   199  	wg.Wait()
   200  }
   201  
   202  func detectContentType(path string, fi os.FileInfo) string {
   203  	file, err := os.Open(path)
   204  	utils.Check(err)
   205  	defer file.Close()
   206  
   207  	fileHeader := &bytes.Buffer{}
   208  	headerSize := int64(512)
   209  	if fi.Size() < headerSize {
   210  		headerSize = fi.Size()
   211  	}
   212  
   213  	// The content type detection only uses 512 bytes at most.
   214  	// This way we avoid copying the whole content for big files.
   215  	_, err = io.CopyN(fileHeader, file, headerSize)
   216  	utils.Check(err)
   217  
   218  	return http.DetectContentType(fileHeader.Bytes())
   219  }
   220  
   221  func printUploadProgress(count *uint64, total uint64) {
   222  	out := fmt.Sprintf("Uploading assets (%d/%d)", atomic.LoadUint64(count), total)
   223  	fmt.Print("\r" + out)
   224  }