Move tools to contrib (#4073)

Also rename sdkch to clog
This commit is contained in:
Alessio Treglia
2019-04-09 10:41:00 +01:00
committed by GitHub
parent ae2999931b
commit 1505c2b2e6
6 changed files with 13 additions and 15 deletions
+69
View File
@@ -0,0 +1,69 @@
###
# Find OS and Go environment
# GO contains the Go binary
# FS contains the OS file separator
###
ifeq ($(OS),Windows_NT)
GO := $(shell where go.exe 2> NUL)
FS := \\
else
GO := $(shell command -v go 2> /dev/null)
FS := /
endif
ifeq ($(GO),)
$(error could not find go. Is it in PATH? $(GO))
endif
GOPATH ?= $(shell $(GO) env GOPATH)
GITHUBDIR := $(GOPATH)$(FS)src$(FS)github.com
GOLANGCI_LINT_VERSION := v1.15.0
GOLANGCI_LINT_HASHSUM := ac897cadc180bf0c1a4bf27776c410debad27205b22856b861d41d39d06509cf
###
# Functions
###
go_get = $(if $(findstring Windows_NT,$(OS)),\
IF NOT EXIST $(GITHUBDIR)$(FS)$(1)$(FS) ( mkdir $(GITHUBDIR)$(FS)$(1) ) else (cd .) &\
IF NOT EXIST $(GITHUBDIR)$(FS)$(1)$(FS)$(2)$(FS) ( cd $(GITHUBDIR)$(FS)$(1) && git clone https://github.com/$(1)/$(2) ) else (cd .) &\
,\
mkdir -p $(GITHUBDIR)$(FS)$(1) &&\
(test ! -d $(GITHUBDIR)$(FS)$(1)$(FS)$(2) && cd $(GITHUBDIR)$(FS)$(1) && git clone https://github.com/$(1)/$(2)) || true &&\
)\
cd $(GITHUBDIR)$(FS)$(1)$(FS)$(2) && git fetch origin && git checkout -q $(3)
go_install = $(call go_get,$(1),$(2),$(3)) && cd $(GITHUBDIR)$(FS)$(1)$(FS)$(2) && $(GO) install
mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST)))
mkfile_dir := $(shell cd $(shell dirname $(mkfile_path)); pwd)
###
# tools
###
all: tools
tools: tools-stamp
tools-stamp: $(GOBIN)/golangci-lint $(GOBIN)/statik $(GOBIN)/goimports $(GOBIN)/gosum $(GOBIN)/clog
touch $@
$(GOBIN)/golangci-lint: contrib/devtools/install-golangci-lint.sh $(GOBIN)/gosum
bash contrib/devtools/install-golangci-lint.sh $(GOBIN) $(GOLANGCI_LINT_VERSION) $(GOLANGCI_LINT_HASHSUM)
$(GOBIN)/statik:
$(call go_install,rakyll,statik,v0.1.5)
$(GOBIN)/goimports:
go get golang.org/x/tools/cmd/goimports@v0.0.0-20190114222345-bf090417da8b
$(GOBIN)/gosum:
go install -mod=readonly ./contrib/devtools/gosum/
$(GOBIN)/clog:
go install -mod=readonly ./contrib/devtools/clog/
tools-clean:
cd $(GOBIN) && rm -f golangci-lint statik goimports gosum clog
rm -f tools-stamp
.PHONY: all tools tools-clean
+50
View File
@@ -0,0 +1,50 @@
# sdkch
Simple tool to maintain modular changelogs
# Usage
```
$ sdkch -help
usage: sdkch [-d directory] [-prune] command
Maintain unreleased changelog entries in a modular fashion.
Commands:
add section stanza [message] Add an entry file.
If message is empty, start the editor to edit
the message.
generate [version] Generate a changelog in Markdown format and print
it to STDOUT. version defaults to UNRELEASED.
Sections Stanzas
--- ---
breaking gaia
features gaiacli
improvements gaiarest
bugfixes sdk
tendermint
Flags:
-d string
entry files directory (default "/home/alessio/work/tendermint/src/github.com/cosmos/cosmos-sdk/.pending")
-prune
prune old entries after changelog generation
```
## Add a new entry
You can either drop a text file in the appropriate directory or use the `add` command:
```bash
$ sdkch add features gaiacli '#3452 New cool gaiacli command'
```
If no message is provided, a new entry file is opened in an editor is started
## Generate the full changelog
```bash
$ sdkch generate v0.30.0
```
The `-prune` flag would remove the old entry files after the changelog is generated.
+427
View File
@@ -0,0 +1,427 @@
package main
import (
"bufio"
"errors"
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"github.com/spf13/cobra"
)
const (
entriesDirName = ".pending"
ghLinkPattern = `#([0-9]+)`
ghLinkExpanded = `[\#$1](https://github.com/cosmos/cosmos-sdk/issues/$1)`
maxEntryFilenameLength = 20
)
var (
progName string
verboseLog *log.Logger
entriesDir string
verboseLogging bool
interactiveMode bool
// sections name-title map
sections = map[string]string{
"breaking": "Breaking Changes",
"features": "New features",
"improvements": "Improvements",
"bugfixes": "Bugfixes",
}
// stanzas name-title map
stanzas = map[string]string{
"gaia": "Gaia",
"gaiacli": "Gaia CLI",
"gaiarest": "Gaia REST API",
"sdk": "SDK",
"tendermint": "Tendermint",
}
// RootCmd represents the base command when called without any subcommands
RootCmd = &cobra.Command{
Use: "clog",
Short: "Maintain unreleased changelog entries in a modular fashion.",
}
// command to add a pending log entry
AddCmd = &cobra.Command{
Use: "add [section] [stanza] [message]",
Short: "Add an entry file.",
Long: `Add an entry file. If message is empty, start the editor to edit the message.
Sections Stanzas
--- ---
breaking gaia
features gaiacli
improvements gaiarest
bugfixes sdk
tendermint`,
Args: cobra.MaximumNArgs(3),
RunE: func(cmd *cobra.Command, args []string) error {
if interactiveMode {
return addEntryFileFromConsoleInput()
}
if len(args) < 2 {
log.Println("must include at least 2 arguments when not in interactive mode")
return nil
}
sectionDir, stanzaDir := args[0], args[1]
err := validateSectionStanzaDirs(sectionDir, stanzaDir)
if err != nil {
return err
}
if len(args) == 3 {
return addSinglelineEntryFile(sectionDir, stanzaDir, strings.TrimSpace(args[2]))
}
return addEntryFile(sectionDir, stanzaDir)
},
}
// command to generate the changelog
GenerateCmd = &cobra.Command{
Use: "generate",
Short: "Generate a changelog in Markdown format and print it to STDOUT. version defaults to UNRELEASED.",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
version := "UNRELEASED"
if flag.NArg() > 1 {
version = strings.Join(flag.Args()[1:], " ")
}
return generateChangelog(version)
},
}
// command to delete empty sub-directories recursively
PruneCmd = &cobra.Command{
Use: "prune",
Short: "Delete empty sub-directories recursively.",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return pruneEmptyDirectories()
},
}
)
func init() {
RootCmd.AddCommand(AddCmd)
RootCmd.AddCommand(GenerateCmd)
RootCmd.AddCommand(PruneCmd)
cwd, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
AddCmd.Flags().BoolVarP(&interactiveMode, "interactive", "i", false, "get the section/stanza/message with interactive CLI prompts")
RootCmd.PersistentFlags().BoolVarP(&verboseLogging, "verbose-logging", "v", false, "enable verbose logging")
RootCmd.PersistentFlags().StringVarP(&entriesDir, "entries-dir", "d", filepath.Join(cwd, entriesDirName), "entry files directory")
}
func main() {
logPrefix := fmt.Sprintf("%s: ", filepath.Base(progName))
log.SetFlags(0)
log.SetPrefix(logPrefix)
flag.Parse()
verboseLog = log.New(ioutil.Discard, "", 0)
if verboseLogging {
verboseLog.SetOutput(os.Stderr)
verboseLog.SetPrefix(logPrefix)
}
if err := RootCmd.Execute(); err != nil {
log.Fatal(err)
}
}
func addEntryFileFromConsoleInput() error {
reader := bufio.NewReader(os.Stdin)
fmt.Println("Please enter the section (either: \"breaking\", \"features\", \"improvements\", \"bugfixes\")")
sectionDir, _ := reader.ReadString('\n')
sectionDir = strings.TrimSpace(sectionDir)
if _, ok := sections[sectionDir]; !ok {
return errors.New("invalid section, please try again")
}
fmt.Println("Please enter the stanza (either: \"gaia\", \"gaiacli\", \"gaiarest\", \"sdk\", \"tendermint\")")
stanzaDir, _ := reader.ReadString('\n')
stanzaDir = strings.TrimSpace(stanzaDir)
if _, ok := stanzas[stanzaDir]; !ok {
return errors.New("invalid stanza, please try again")
}
fmt.Println("Please enter the changelog message (or press enter to write in default $EDITOR)")
message, _ := reader.ReadString('\n')
message = strings.TrimSpace(message)
if message == "" {
return addEntryFile(sectionDir, stanzaDir)
}
return addSinglelineEntryFile(sectionDir, stanzaDir, message)
}
func addSinglelineEntryFile(sectionDir, stanzaDir, message string) error {
filename := filepath.Join(
filepath.Join(entriesDir, sectionDir, stanzaDir),
generateFileName(message),
)
return writeEntryFile(filename, []byte(message))
}
func addEntryFile(sectionDir, stanzaDir string) error {
bs, err := readUserInputFromEditor()
if err != nil {
return err
}
firstLine := strings.TrimSpace(strings.Split(string(bs), "\n")[0])
filename := filepath.Join(
filepath.Join(entriesDir, sectionDir, stanzaDir),
generateFileName(firstLine),
)
return writeEntryFile(filename, bs)
}
func generateFileName(line string) string {
var chunks []string
filenameInvalidChars := regexp.MustCompile(`[^a-zA-Z0-9-_]`)
subsWithInvalidCharsRemoved := strings.Split(filenameInvalidChars.ReplaceAllString(line, " "), " ")
for _, sub := range subsWithInvalidCharsRemoved {
sub = strings.TrimSpace(sub)
if len(sub) != 0 {
chunks = append(chunks, sub)
}
}
ret := strings.Join(chunks, "-")
if len(ret) > maxEntryFilenameLength {
return ret[:maxEntryFilenameLength]
}
return ret
}
func directoryContents(dirPath string) ([]os.FileInfo, error) {
contents, err := ioutil.ReadDir(dirPath)
if err != nil && !os.IsNotExist(err) {
return nil, fmt.Errorf("couldn't read directory %s: %v", dirPath, err)
}
if len(contents) == 0 {
return nil, nil
}
// Filter out hidden files
newContents := contents[:0]
for _, f := range contents {
if f.Name()[0] != '.' { // skip hidden files
newContents = append(newContents, f)
}
}
for i := len(newContents); i < len(contents); i++ {
contents[i] = nil
}
return newContents, nil
}
func generateChangelog(version string) error {
fmt.Printf("# %s\n\n", version)
for sectionDir, sectionTitle := range sections {
sectionTitlePrinted := false
for stanzaDir, stanzaTitle := range stanzas {
path := filepath.Join(entriesDir, sectionDir, stanzaDir)
files, err := directoryContents(path)
if err != nil {
return err
}
if len(files) == 0 {
continue
}
if !sectionTitlePrinted {
fmt.Printf("## %s\n\n", sectionTitle)
sectionTitlePrinted = true
}
fmt.Printf("### %s\n\n", stanzaTitle)
for _, f := range files {
verboseLog.Println("processing", f.Name())
filename := filepath.Join(path, f.Name())
if err := indentAndPrintFile(filename); err != nil {
return err
}
}
fmt.Println()
}
}
return nil
}
func pruneEmptyDirectories() error {
for sectionDir := range sections {
for stanzaDir := range stanzas {
err := mustPruneDirIfEmpty(filepath.Join(entriesDir, sectionDir, stanzaDir))
if err != nil {
return err
}
}
return mustPruneDirIfEmpty(filepath.Join(entriesDir, sectionDir))
}
return nil
}
// nolint: errcheck
func indentAndPrintFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
scanner := bufio.NewScanner(file)
firstLine := true
ghLinkRe := regexp.MustCompile(ghLinkPattern)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if len(line) == 0 {
continue
}
linkified := ghLinkRe.ReplaceAllString(line, ghLinkExpanded)
if firstLine {
fmt.Printf("* %s\n", linkified)
firstLine = false
continue
}
fmt.Printf(" %s\n", linkified)
}
return scanner.Err()
}
// nolint: errcheck
func writeEntryFile(filename string, bs []byte) error {
if err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil {
return err
}
outFile, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
if err != nil {
return err
}
defer outFile.Close()
if _, err := outFile.Write(bs); err != nil {
return err
}
log.Printf("Unreleased changelog entry written to: %s\n", filename)
log.Println("To modify this entry please edit or delete the above file directly.")
return nil
}
func validateSectionStanzaDirs(sectionDir, stanzaDir string) error {
if _, ok := sections[sectionDir]; !ok {
return fmt.Errorf("invalid section -- %s", sectionDir)
}
if _, ok := stanzas[stanzaDir]; !ok {
return fmt.Errorf("invalid stanza -- %s", stanzaDir)
}
return nil
}
// nolint: errcheck
func readUserInputFromEditor() ([]byte, error) {
tempfilename, err := launchUserEditor()
if err != nil {
return []byte{}, fmt.Errorf("couldn't open an editor: %v", err)
}
defer os.Remove(tempfilename)
bs, err := ioutil.ReadFile(tempfilename)
if err != nil {
return []byte{}, fmt.Errorf("error: %v", err)
}
return bs, nil
}
// nolint: errcheck
func launchUserEditor() (string, error) {
editor, err := exec.LookPath("editor")
if err != nil {
editor = ""
}
if editor == "" {
editor = os.Getenv("VISUAL")
}
if editor == "" {
editor = os.Getenv("EDITOR")
}
if editor == "" {
return "", errors.New("no editor set, make sure that either " +
"VISUAL or EDITOR variables is set and pointing to a correct editor")
}
tempfile, err := ioutil.TempFile("", "clog_*")
tempfilename := tempfile.Name()
if err != nil {
return "", err
}
tempfile.Close()
cmd := exec.Command(editor, tempfilename)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
if err := cmd.Run(); err != nil {
os.Remove(tempfilename)
return "", err
}
fileInfo, err := os.Stat(tempfilename)
if err != nil {
os.Remove(tempfilename)
return "", err
}
if fileInfo.Size() == 0 {
return "", errors.New("aborting due to empty message")
}
return tempfilename, nil
}
func mustPruneDirIfEmpty(path string) error {
contents, err := directoryContents(path)
if err != nil {
return err
}
if len(contents) != 0 {
return nil
}
if err := os.Remove(path); err != nil {
if !os.IsNotExist(err) {
return err
}
return nil
}
log.Println(path, "removed")
return nil
}
// DONTCOVER
+27
View File
@@ -0,0 +1,27 @@
package main
import (
"crypto/sha256"
"fmt"
"io"
"log"
"os"
)
// DONTCOVER
// nolint: errcheck
func main() {
f, err := os.Open(os.Args[1])
if err != nil {
log.Fatal(err)
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
log.Fatal(err)
}
fmt.Printf("%x", h.Sum(nil))
}
+22
View File
@@ -0,0 +1,22 @@
#!/bin/bash
set -euo pipefail
installer="$(mktemp)"
trap "rm -f ${installer}" EXIT
GOBIN="${1}"
VERSION="${2}"
HASHSUM="${3}"
CURL="$(which curl)"
GOSUM="$(which gosum)"
echo "Downloading golangci-lint ${VERSION} installer ..." >&2
"${CURL}" -sfL "https://raw.githubusercontent.com/golangci/golangci-lint/${VERSION}/install.sh" > "${installer}"
echo "Checking hashsum ..." >&2
[ "${HASHSUM}" = "$(${GOSUM} ${installer})" ]
chmod +x "${installer}"
echo "Launching installer ..." >&2
exec "${installer}" -d -b "${GOBIN}" "${VERSION}"