Allows creating links in changelog, similar to what Github does for markdown but works for dependencies as well. Signed-off-by: Derek McGowan <derek@mcgstyle.net>
		
			
				
	
	
		
			339 lines
		
	
	
		
			7.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			339 lines
		
	
	
		
			7.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
/*
 | 
						|
   Copyright The containerd Authors.
 | 
						|
 | 
						|
   Licensed under the Apache License, Version 2.0 (the "License");
 | 
						|
   you may not use this file except in compliance with the License.
 | 
						|
   You may obtain a copy of the License at
 | 
						|
 | 
						|
       http://www.apache.org/licenses/LICENSE-2.0
 | 
						|
 | 
						|
   Unless required by applicable law or agreed to in writing, software
 | 
						|
   distributed under the License is distributed on an "AS IS" BASIS,
 | 
						|
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
   See the License for the specific language governing permissions and
 | 
						|
   limitations under the License.
 | 
						|
*/
 | 
						|
 | 
						|
package main
 | 
						|
 | 
						|
import (
 | 
						|
	"bufio"
 | 
						|
	"bytes"
 | 
						|
	"fmt"
 | 
						|
	"io"
 | 
						|
	"io/ioutil"
 | 
						|
	"os"
 | 
						|
	"os/exec"
 | 
						|
	"path/filepath"
 | 
						|
	"regexp"
 | 
						|
	"sort"
 | 
						|
	"strings"
 | 
						|
 | 
						|
	"github.com/BurntSushi/toml"
 | 
						|
	"github.com/pkg/errors"
 | 
						|
	"github.com/sirupsen/logrus"
 | 
						|
	"github.com/urfave/cli"
 | 
						|
)
 | 
						|
 | 
						|
func loadRelease(path string) (*release, error) {
 | 
						|
	var r release
 | 
						|
	if _, err := toml.DecodeFile(path, &r); err != nil {
 | 
						|
		if os.IsNotExist(err) {
 | 
						|
			return nil, errors.New("please specify the release file as the first argument")
 | 
						|
		}
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	return &r, nil
 | 
						|
}
 | 
						|
 | 
						|
func parseTag(path string) string {
 | 
						|
	return strings.TrimSuffix(filepath.Base(path), ".toml")
 | 
						|
}
 | 
						|
 | 
						|
func parseDependencies(r io.Reader) ([]dependency, error) {
 | 
						|
	var deps []dependency
 | 
						|
	s := bufio.NewScanner(r)
 | 
						|
	for s.Scan() {
 | 
						|
		ln := strings.TrimSpace(s.Text())
 | 
						|
		if strings.HasPrefix(ln, "#") || ln == "" {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		cidx := strings.Index(ln, "#")
 | 
						|
		if cidx > 0 {
 | 
						|
			ln = ln[:cidx]
 | 
						|
		}
 | 
						|
		ln = strings.TrimSpace(ln)
 | 
						|
		parts := strings.Fields(ln)
 | 
						|
		if len(parts) != 2 && len(parts) != 3 {
 | 
						|
			return nil, fmt.Errorf("invalid config format: %s", ln)
 | 
						|
		}
 | 
						|
 | 
						|
		var cloneURL string
 | 
						|
		if len(parts) == 3 {
 | 
						|
			cloneURL = parts[2]
 | 
						|
		} else {
 | 
						|
			cloneURL = "git://" + parts[0]
 | 
						|
		}
 | 
						|
 | 
						|
		deps = append(deps, dependency{
 | 
						|
			Name:     parts[0],
 | 
						|
			Commit:   parts[1],
 | 
						|
			CloneURL: cloneURL,
 | 
						|
		})
 | 
						|
	}
 | 
						|
	if err := s.Err(); err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	return deps, nil
 | 
						|
}
 | 
						|
 | 
						|
func getPreviousDeps(previous string) ([]dependency, error) {
 | 
						|
	r, err := fileFromRev(previous, vendorConf)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	return parseDependencies(r)
 | 
						|
}
 | 
						|
 | 
						|
func changelog(previous, commit string) ([]change, error) {
 | 
						|
	raw, err := getChangelog(previous, commit)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	return parseChangelog(raw)
 | 
						|
}
 | 
						|
 | 
						|
func gitChangeDiff(previous, commit string) string {
 | 
						|
	if previous != "" {
 | 
						|
		return fmt.Sprintf("%s..%s", previous, commit)
 | 
						|
	}
 | 
						|
	return commit
 | 
						|
}
 | 
						|
 | 
						|
func getChangelog(previous, commit string) ([]byte, error) {
 | 
						|
	return git("log", "--oneline", gitChangeDiff(previous, commit))
 | 
						|
}
 | 
						|
 | 
						|
func linkifyChanges(c []change, commit, msg func(change) (string, error)) error {
 | 
						|
	for i := range c {
 | 
						|
		commitLink, err := commit(c[i])
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
 | 
						|
		description, err := msg(c[i])
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
 | 
						|
		c[i].Commit = fmt.Sprintf("[`%s`](%s)", c[i].Commit, commitLink)
 | 
						|
		c[i].Description = description
 | 
						|
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func parseChangelog(changelog []byte) ([]change, error) {
 | 
						|
	var (
 | 
						|
		changes []change
 | 
						|
		s       = bufio.NewScanner(bytes.NewReader(changelog))
 | 
						|
	)
 | 
						|
	for s.Scan() {
 | 
						|
		fields := strings.Fields(s.Text())
 | 
						|
		changes = append(changes, change{
 | 
						|
			Commit:      fields[0],
 | 
						|
			Description: strings.Join(fields[1:], " "),
 | 
						|
		})
 | 
						|
	}
 | 
						|
	if err := s.Err(); err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	return changes, nil
 | 
						|
}
 | 
						|
 | 
						|
func fileFromRev(rev, file string) (io.Reader, error) {
 | 
						|
	p, err := git("show", fmt.Sprintf("%s:%s", rev, file))
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	return bytes.NewReader(p), nil
 | 
						|
}
 | 
						|
 | 
						|
var gitConfigs = map[string]string{}
 | 
						|
 | 
						|
func git(args ...string) ([]byte, error) {
 | 
						|
	var gitArgs []string
 | 
						|
	for k, v := range gitConfigs {
 | 
						|
		gitArgs = append(gitArgs, "-c", fmt.Sprintf("%s=%s", k, v))
 | 
						|
	}
 | 
						|
	gitArgs = append(gitArgs, args...)
 | 
						|
	o, err := exec.Command("git", gitArgs...).CombinedOutput()
 | 
						|
	if err != nil {
 | 
						|
		return nil, fmt.Errorf("%s: %s", err, o)
 | 
						|
	}
 | 
						|
	return o, nil
 | 
						|
}
 | 
						|
 | 
						|
func renameDependencies(deps []dependency, renames map[string]projectRename) {
 | 
						|
	if len(renames) == 0 {
 | 
						|
		return
 | 
						|
	}
 | 
						|
	type dep struct {
 | 
						|
		shortname string
 | 
						|
		name      string
 | 
						|
	}
 | 
						|
	renameMap := map[string]dep{}
 | 
						|
	for shortname, rename := range renames {
 | 
						|
		renameMap[rename.Old] = dep{
 | 
						|
			shortname: shortname,
 | 
						|
			name:      rename.New,
 | 
						|
		}
 | 
						|
	}
 | 
						|
	for i := range deps {
 | 
						|
		if updated, ok := renameMap[deps[i].Name]; ok {
 | 
						|
			logrus.Debugf("Renamed %s from %s to %s", updated.shortname, deps[i].Name, updated.name)
 | 
						|
			deps[i].Name = updated.name
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func updatedDeps(previous, deps []dependency) []dependency {
 | 
						|
	var updated []dependency
 | 
						|
	pm, cm := toDepMap(previous), toDepMap(deps)
 | 
						|
	for name, c := range cm {
 | 
						|
		d, ok := pm[name]
 | 
						|
		if !ok {
 | 
						|
			// it is a new dep and should be noted
 | 
						|
			updated = append(updated, c)
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		// it exists, see if its updated
 | 
						|
		if d.Commit != c.Commit {
 | 
						|
			// set the previous commit
 | 
						|
			c.Previous = d.Commit
 | 
						|
			updated = append(updated, c)
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return updated
 | 
						|
}
 | 
						|
 | 
						|
func toDepMap(deps []dependency) map[string]dependency {
 | 
						|
	out := make(map[string]dependency)
 | 
						|
	for _, d := range deps {
 | 
						|
		out[d.Name] = d
 | 
						|
	}
 | 
						|
	return out
 | 
						|
}
 | 
						|
 | 
						|
type contributor struct {
 | 
						|
	name  string
 | 
						|
	email string
 | 
						|
}
 | 
						|
 | 
						|
func addContributors(previous, commit string, contributors map[contributor]int) error {
 | 
						|
	raw, err := git("log", `--format=%aE %aN`, gitChangeDiff(previous, commit))
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	s := bufio.NewScanner(bytes.NewReader(raw))
 | 
						|
	for s.Scan() {
 | 
						|
		p := strings.SplitN(s.Text(), " ", 2)
 | 
						|
		if len(p) != 2 {
 | 
						|
			return errors.Errorf("invalid author line: %q", s.Text())
 | 
						|
		}
 | 
						|
		c := contributor{
 | 
						|
			name:  p[1],
 | 
						|
			email: p[0],
 | 
						|
		}
 | 
						|
		contributors[c] = contributors[c] + 1
 | 
						|
	}
 | 
						|
	if err := s.Err(); err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func orderContributors(contributors map[contributor]int) []string {
 | 
						|
	type contribstat struct {
 | 
						|
		name  string
 | 
						|
		email string
 | 
						|
		count int
 | 
						|
	}
 | 
						|
	all := make([]contribstat, 0, len(contributors))
 | 
						|
	for c, count := range contributors {
 | 
						|
		all = append(all, contribstat{
 | 
						|
			name:  c.name,
 | 
						|
			email: c.email,
 | 
						|
			count: count,
 | 
						|
		})
 | 
						|
	}
 | 
						|
	sort.Slice(all, func(i, j int) bool {
 | 
						|
		if all[i].count == all[j].count {
 | 
						|
			return all[i].name < all[j].name
 | 
						|
		}
 | 
						|
		return all[i].count > all[j].count
 | 
						|
	})
 | 
						|
	names := make([]string, len(all))
 | 
						|
	for i := range names {
 | 
						|
		logrus.Debugf("Contributor: %s <%s> with %d commits", all[i].name, all[i].email, all[i].count)
 | 
						|
		names[i] = all[i].name
 | 
						|
	}
 | 
						|
 | 
						|
	return names
 | 
						|
}
 | 
						|
 | 
						|
// getTemplate will use a builtin template if the template is not specified on the cli
 | 
						|
func getTemplate(context *cli.Context) (string, error) {
 | 
						|
	path := context.GlobalString("template")
 | 
						|
	f, err := os.Open(path)
 | 
						|
	if err != nil {
 | 
						|
		// if the template file does not exist and the path is for the default template then
 | 
						|
		// return the compiled in template
 | 
						|
		if os.IsNotExist(err) && path == defaultTemplateFile {
 | 
						|
			return releaseNotes, nil
 | 
						|
		}
 | 
						|
		return "", err
 | 
						|
	}
 | 
						|
	defer f.Close()
 | 
						|
	data, err := ioutil.ReadAll(f)
 | 
						|
	if err != nil {
 | 
						|
		return "", err
 | 
						|
	}
 | 
						|
	return string(data), nil
 | 
						|
}
 | 
						|
 | 
						|
func githubCommitLink(repo string) func(change) (string, error) {
 | 
						|
	return func(c change) (string, error) {
 | 
						|
		full, err := git("rev-parse", c.Commit)
 | 
						|
		if err != nil {
 | 
						|
			return "", err
 | 
						|
		}
 | 
						|
		commit := strings.TrimSpace(string(full))
 | 
						|
 | 
						|
		return fmt.Sprintf("https://github.com/%s/commit/%s", repo, commit), nil
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func githubPRLink(repo string) func(change) (string, error) {
 | 
						|
	r := regexp.MustCompile("^Merge pull request #[0-9]+")
 | 
						|
	return func(c change) (string, error) {
 | 
						|
		var err error
 | 
						|
		message := r.ReplaceAllStringFunc(c.Description, func(m string) string {
 | 
						|
			idx := strings.Index(m, "#")
 | 
						|
			pr := m[idx+1:]
 | 
						|
 | 
						|
			// TODO: Validate links using github API
 | 
						|
			// TODO: Validate PR merged as commit hash
 | 
						|
			link := fmt.Sprintf("https://github.com/%s/pull/%s", repo, pr)
 | 
						|
 | 
						|
			return fmt.Sprintf("%s [#%s](%s)", m[:idx], pr, link)
 | 
						|
		})
 | 
						|
		if err != nil {
 | 
						|
			return "", err
 | 
						|
		}
 | 
						|
		return message, nil
 | 
						|
	}
 | 
						|
}
 |