Merge PR #3828: Add sdkch to maintain PENDING.md effectively and free of file conflicts

Closes: #2380
This commit is contained in:
Alessio Treglia 2019-03-14 14:24:53 +01:00 committed by Christopher Goes
parent 54ac1d2fe8
commit 648b43288b
14 changed files with 387 additions and 79 deletions

View File

@ -9,7 +9,7 @@ v If a checkbox is n/a - please still include it but + a little note why
- [ ] Linked to github-issue with discussion and accepted design OR link to spec that describes this work.
- [ ] Wrote tests
- [ ] Updated relevant documentation (`docs/`)
- [ ] Added entries in `PENDING.md` with issue #
- [ ] Added a relevant changelog entry: `sdkch add [section] [stanza] [message]`
- [ ] rereviewed `Files changed` in the github PR explorer
______

View File

@ -0,0 +1 @@
#3837 Fix `WithdrawValidatorCommission` to properly set the validator's remaining commission.

View File

@ -0,0 +1 @@
#3813 New sdk.NewCoins safe constructor to replace bare sdk.Coins{} declarations.

View File

@ -0,0 +1 @@
#3808 `gaiad` and `gaiacli` integration tests use ./build/ binaries.

View File

@ -0,0 +1 @@
#3841 Add indent to JSON of `gaiacli keys [add|show|list]`

View File

@ -0,0 +1 @@
#3859 Add newline to echo of `gaiacli keys ...`

View File

@ -0,0 +1 @@
#3801 `baseapp` safety improvements

View File

@ -0,0 +1 @@
#3820 Make Coins.IsAllGT() more robust and consistent.

View File

@ -0,0 +1 @@
#3828 New sdkch tool to maintain changelogs

View File

@ -0,0 +1 @@
#3864 Make Coins.IsAllGTE() more consistent.

View File

@ -118,13 +118,16 @@ update_dev_tools:
go get -u github.com/tendermint/lint/golint
devtools: devtools-stamp
devtools-clean: tools-clean
devtools-stamp: tools
@echo "--> Downloading linters (this may take awhile)"
$(GOPATH)/src/github.com/alecthomas/gometalinter/scripts/install.sh -b $(GOBIN)
go get github.com/tendermint/lint/golint
go install ./cmd/sdkch
touch $@
devtools-clean: tools-clean
rm -f devtools-stamp
vendor-deps: tools
@echo "--> Generating vendor directory via dep ensure"
@rm -rf .vendor-new

View File

@ -1,77 +0,0 @@
# PENDING CHANGELOG
<!----------------------------- BREAKING CHANGES ----------------------------->
## BREAKING CHANGES
### Gaia REST API
### Gaia CLI
### Gaia
### SDK
### Tendermint
<!--------------------------------- FEATURES --------------------------------->
## FEATURES
### Gaia REST API
### Gaia CLI
### Gaia
### SDK
* [\3813](https://github.com/cosmos/cosmos-sdk/pull/3813) New sdk.NewCoins safe constructor to replace bare
sdk.Coins{} declarations.
### Tendermint
<!------------------------------- IMPROVEMENTS ------------------------------->
## IMPROVEMENTS
### Gaia REST API
### Gaia CLI
* [\#3841](https://github.com/cosmos/cosmos-sdk/pull/3841) Add indent to JSON of `gaiacli keys [add|show|list]`
* [\#3859](https://github.com/cosmos/cosmos-sdk/pull/3859) Add newline to echo of `gaiacli keys ...`
### Gaia
* #3808 `gaiad` and `gaiacli` integration tests use ./build/ binaries.
### SDK
* [\#3820] Make Coins.IsAllGT() more robust and consistent.
* [\#3864] Make Coins.IsAllGTE() more consistent.
* #3801 `baseapp` saftey improvements
### Tendermint
### CI/CD
* [\198](https://github.com/cosmos/cosmos-sdk/pull/3832)
<!--------------------------------- BUG FIXES -------------------------------->
## BUG FIXES
### Gaia REST API
### Gaia CLI
### Gaia
### SDK
* [\#3837] Fix `WithdrawValidatorCommission` to properly set the validator's
remaining commission.
### Tendermint

50
cmd/sdkch/README.md Normal file
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.

323
cmd/sdkch/main.go Normal file
View File

@ -0,0 +1,323 @@
package main
import (
"bufio"
"errors"
"flag"
"fmt"
"io/ioutil"
"log"
"math"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
)
const (
entriesDirName = ".pending"
ghLinkPattern = `#([0-9]+)`
ghLinkExpanded = `[\#$1](https://github.com/cosmos/cosmos-sdk/issues/$1)`
maxEntryFilenameLength = 20
)
var (
progName string
entriesDir string
pruneAfterGenerate 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",
}
)
func init() {
progName = filepath.Base(os.Args[0])
cwd, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
flag.StringVar(&entriesDir, "d", filepath.Join(cwd, entriesDirName), "entry files directory")
flag.BoolVar(&pruneAfterGenerate, "prune", false, "prune old entries after changelog generation")
flag.Usage = printUsage
}
func main() {
log.SetFlags(0)
log.SetPrefix(fmt.Sprintf("%s: ", filepath.Base(progName)))
flag.Parse()
if flag.NArg() < 1 {
errInsufficientArgs()
}
cmd := flag.Arg(0)
switch cmd {
case "add":
if flag.NArg() < 3 {
errInsufficientArgs()
}
if flag.NArg() > 4 {
errTooManyArgs()
}
sectionDir, stanzaDir := flag.Arg(1), flag.Arg(2)
validateSectionStanzaDirs(sectionDir, stanzaDir)
if flag.NArg() == 4 {
addSinglelineEntryFile(sectionDir, stanzaDir, strings.TrimSpace(flag.Arg(3)))
return
}
addEntryFile(sectionDir, stanzaDir)
case "generate":
version := "UNRELEASED"
if flag.NArg() > 1 {
version = strings.Join(flag.Args()[1:], " ")
}
generateChangelog(version, pruneAfterGenerate)
default:
unknownCommand(cmd)
}
}
func addSinglelineEntryFile(sectionDir, stanzaDir, message string) {
filename := filepath.Join(
filepath.Join(entriesDir, sectionDir, stanzaDir),
generateFileName(message),
)
writeEntryFile(filename, []byte(message))
}
func addEntryFile(sectionDir, stanzaDir string) {
bs := readUserInput()
firstLine := strings.TrimSpace(strings.Split(string(bs), "\n")[0])
filename := filepath.Join(
filepath.Join(entriesDir, sectionDir, stanzaDir),
generateFileName(firstLine),
)
writeEntryFile(filename, bs)
}
var filenameInvalidChars = regexp.MustCompile(`[^a-zA-Z0-9-_]`)
func generateFileName(line string) string {
var chunks []string
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, "-")
return ret[:int(math.Min(float64(len(ret)), float64(maxEntryFilenameLength)))]
}
func generateChangelog(version string, prune bool) {
fmt.Printf("# %s\n\n", version)
for sectionDir, sectionTitle := range sections {
fmt.Printf("## %s\n\n", sectionTitle)
for stanzaDir, stanzaTitle := range stanzas {
fmt.Printf("### %s\n\n", stanzaTitle)
path := filepath.Join(entriesDir, sectionDir, stanzaDir)
files, err := ioutil.ReadDir(path)
if err != nil && !os.IsNotExist(err) {
log.Fatal(err)
}
for _, f := range files {
if f.Name()[0] == '.' {
continue // skip hidden files
}
filename := filepath.Join(path, f.Name())
if err := indentAndPrintFile(filename); err != nil {
log.Fatal(err)
}
if prune {
if err := os.Remove(filename); err != nil {
fmt.Fprintln(os.Stderr, "couldn't delete file:", filename)
}
}
}
fmt.Println()
}
}
}
// 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) {
if err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil {
log.Fatal(err)
}
outFile, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
if err != nil {
log.Fatal(err)
}
defer outFile.Close()
if _, err := outFile.Write(bs); err != nil {
log.Fatal(err)
}
fmt.Fprintf(os.Stderr, "Unreleased changelog entry written to: %s\n", filename)
fmt.Fprintln(os.Stderr, "To modify this entry please edit or delete the above file directly.")
}
func validateSectionStanzaDirs(sectionDir, stanzaDir string) {
if _, ok := sections[sectionDir]; !ok {
log.Fatalf("invalid section -- %s", sectionDir)
}
if _, ok := stanzas[stanzaDir]; !ok {
log.Fatalf("invalid stanza -- %s", stanzaDir)
}
}
// nolint: errcheck
func readUserInput() []byte {
tempfilename, err := launchUserEditor()
if err != nil {
log.Fatalf("couldn't open an editor: %v", err)
}
defer os.Remove(tempfilename)
bs, err := ioutil.ReadFile(tempfilename)
if err != nil {
log.Fatalf("error: %v", err)
}
return bs
}
// 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("", "sdkch_*")
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 {
log.Fatal("aborting due to empty message")
}
return tempfilename, nil
}
func printUsage() {
usageText := fmt.Sprintf(`usage: %s [-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
`, progName)
fmt.Fprintf(os.Stderr, "%s\nFlags:\n", usageText)
flag.PrintDefaults()
}
func errInsufficientArgs() {
log.Println("insufficient arguments")
printUsage()
os.Exit(1)
}
func errTooManyArgs() {
log.Println("too many arguments")
printUsage()
os.Exit(1)
}
func unknownCommand(cmd string) {
log.Fatalf("unknown command -- '%s'\nTry '%s -help' for more information.", cmd, progName)
}
// DONTCOVER