From 648b43288bb4f86f353bc3236cd61010cd2d32b5 Mon Sep 17 00:00:00 2001 From: Alessio Treglia Date: Thu, 14 Mar 2019 14:24:53 +0100 Subject: [PATCH] Merge PR #3828: Add sdkch to maintain PENDING.md effectively and free of file conflicts Closes: #2380 --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- ...y-set-the-validator-s-remaining-commission | 1 + ...-NewCoins-safe-constructor-to-replace-bare | 1 + ...iacli-integration-tests-use-build-binaries | 1 + .../improvements/gaiacli/3841-Add-indent-to-J | 1 + .../3859-Add-newline-to-echo-of-gaiacli-keys | 1 + .../sdk/3801-baseapp-safety-improvements | 1 + ...e-Coins-IsAllGT-more-robust-and-consistent | 1 + ...3828-New-sdkch-tool-to-maintain-changelogs | 1 + .../3864-Make-Coins-IsAllGTE-more-consistent | 1 + Makefile | 5 +- PENDING.md | 77 ----- cmd/sdkch/README.md | 50 +++ cmd/sdkch/main.go | 323 ++++++++++++++++++ 14 files changed, 387 insertions(+), 79 deletions(-) create mode 100644 .pending/bugfixes/sdk/3837-Fix-WithdrawValidatorCommission-to-properly-set-the-validator-s-remaining-commission create mode 100644 .pending/features/sdk/3813-New-sdk-NewCoins-safe-constructor-to-replace-bare create mode 100644 .pending/improvements/gaia/3808-gaiad-and-gaiacli-integration-tests-use-build-binaries create mode 100644 .pending/improvements/gaiacli/3841-Add-indent-to-J create mode 100644 .pending/improvements/gaiacli/3859-Add-newline-to-echo-of-gaiacli-keys create mode 100644 .pending/improvements/sdk/3801-baseapp-safety-improvements create mode 100644 .pending/improvements/sdk/3820-Make-Coins-IsAllGT-more-robust-and-consistent create mode 100644 .pending/improvements/sdk/3828-New-sdkch-tool-to-maintain-changelogs create mode 100644 .pending/improvements/sdk/3864-Make-Coins-IsAllGTE-more-consistent delete mode 100644 PENDING.md create mode 100644 cmd/sdkch/README.md create mode 100644 cmd/sdkch/main.go diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 3a3d666e0..1e722a2c4 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -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 ______ diff --git a/.pending/bugfixes/sdk/3837-Fix-WithdrawValidatorCommission-to-properly-set-the-validator-s-remaining-commission b/.pending/bugfixes/sdk/3837-Fix-WithdrawValidatorCommission-to-properly-set-the-validator-s-remaining-commission new file mode 100644 index 000000000..4a46c3d2b --- /dev/null +++ b/.pending/bugfixes/sdk/3837-Fix-WithdrawValidatorCommission-to-properly-set-the-validator-s-remaining-commission @@ -0,0 +1 @@ +#3837 Fix `WithdrawValidatorCommission` to properly set the validator's remaining commission. diff --git a/.pending/features/sdk/3813-New-sdk-NewCoins-safe-constructor-to-replace-bare b/.pending/features/sdk/3813-New-sdk-NewCoins-safe-constructor-to-replace-bare new file mode 100644 index 000000000..bd25be4fe --- /dev/null +++ b/.pending/features/sdk/3813-New-sdk-NewCoins-safe-constructor-to-replace-bare @@ -0,0 +1 @@ +#3813 New sdk.NewCoins safe constructor to replace bare sdk.Coins{} declarations. diff --git a/.pending/improvements/gaia/3808-gaiad-and-gaiacli-integration-tests-use-build-binaries b/.pending/improvements/gaia/3808-gaiad-and-gaiacli-integration-tests-use-build-binaries new file mode 100644 index 000000000..bfe056434 --- /dev/null +++ b/.pending/improvements/gaia/3808-gaiad-and-gaiacli-integration-tests-use-build-binaries @@ -0,0 +1 @@ +#3808 `gaiad` and `gaiacli` integration tests use ./build/ binaries. diff --git a/.pending/improvements/gaiacli/3841-Add-indent-to-J b/.pending/improvements/gaiacli/3841-Add-indent-to-J new file mode 100644 index 000000000..b6e9dfb53 --- /dev/null +++ b/.pending/improvements/gaiacli/3841-Add-indent-to-J @@ -0,0 +1 @@ +#3841 Add indent to JSON of `gaiacli keys [add|show|list]` diff --git a/.pending/improvements/gaiacli/3859-Add-newline-to-echo-of-gaiacli-keys b/.pending/improvements/gaiacli/3859-Add-newline-to-echo-of-gaiacli-keys new file mode 100644 index 000000000..c8baf2c00 --- /dev/null +++ b/.pending/improvements/gaiacli/3859-Add-newline-to-echo-of-gaiacli-keys @@ -0,0 +1 @@ +#3859 Add newline to echo of `gaiacli keys ...` diff --git a/.pending/improvements/sdk/3801-baseapp-safety-improvements b/.pending/improvements/sdk/3801-baseapp-safety-improvements new file mode 100644 index 000000000..c971a2e16 --- /dev/null +++ b/.pending/improvements/sdk/3801-baseapp-safety-improvements @@ -0,0 +1 @@ +#3801 `baseapp` safety improvements diff --git a/.pending/improvements/sdk/3820-Make-Coins-IsAllGT-more-robust-and-consistent b/.pending/improvements/sdk/3820-Make-Coins-IsAllGT-more-robust-and-consistent new file mode 100644 index 000000000..b2ee3f547 --- /dev/null +++ b/.pending/improvements/sdk/3820-Make-Coins-IsAllGT-more-robust-and-consistent @@ -0,0 +1 @@ +#3820 Make Coins.IsAllGT() more robust and consistent. diff --git a/.pending/improvements/sdk/3828-New-sdkch-tool-to-maintain-changelogs b/.pending/improvements/sdk/3828-New-sdkch-tool-to-maintain-changelogs new file mode 100644 index 000000000..46903cfa1 --- /dev/null +++ b/.pending/improvements/sdk/3828-New-sdkch-tool-to-maintain-changelogs @@ -0,0 +1 @@ +#3828 New sdkch tool to maintain changelogs diff --git a/.pending/improvements/sdk/3864-Make-Coins-IsAllGTE-more-consistent b/.pending/improvements/sdk/3864-Make-Coins-IsAllGTE-more-consistent new file mode 100644 index 000000000..2b1a3796f --- /dev/null +++ b/.pending/improvements/sdk/3864-Make-Coins-IsAllGTE-more-consistent @@ -0,0 +1 @@ +#3864 Make Coins.IsAllGTE() more consistent. diff --git a/Makefile b/Makefile index 04803aaf2..e93bcc3a8 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/PENDING.md b/PENDING.md deleted file mode 100644 index b6021332f..000000000 --- a/PENDING.md +++ /dev/null @@ -1,77 +0,0 @@ -# PENDING CHANGELOG - - - -## BREAKING CHANGES - -### Gaia REST API - -### Gaia CLI - -### Gaia - -### SDK - -### Tendermint - - - -## 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 - -### 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 - -### Gaia REST API - -### Gaia CLI - -### Gaia - -### SDK - -* [\#3837] Fix `WithdrawValidatorCommission` to properly set the validator's -remaining commission. - -### Tendermint diff --git a/cmd/sdkch/README.md b/cmd/sdkch/README.md new file mode 100644 index 000000000..204b67f55 --- /dev/null +++ b/cmd/sdkch/README.md @@ -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. diff --git a/cmd/sdkch/main.go b/cmd/sdkch/main.go new file mode 100644 index 000000000..b87129299 --- /dev/null +++ b/cmd/sdkch/main.go @@ -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