Skip to content

Add commit support for changelog subcommand #1014

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 89 additions & 31 deletions cmd/krel/cmd/changelog.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,25 +43,26 @@ import (
// changelogCmd represents the subcommand for `krel changelog`
var changelogCmd = &cobra.Command{
Use: "changelog",
Short: "changelog maintains the lifecycle of CHANGELOG-x.y.md files",
Short: "changelog maintains the lifecycle of CHANGELOG-x.y.{md,html} files",
Long: `krel changelog

The 'changelog' subcommand of 'krel' does the following things by utilizing
the golang based 'release-notes' tool:

1. Generate the release notes for either the patch or the new minor release
a) Createa a new CHANGELOG-x.y.md file if we're working on a minor release
1. Generate the release notes for either a patch or a new minor release. Minor
releases can be alpha, beta or rc’s, too.
a) Create a new CHANGELOG-x.y.md file if not existing.
b) Correctly prepend the generated notes to the existing CHANGELOG-x.y.md
file if it’s a patch release. This also includes the table of contents.
file if already existing. This also includes the modification of the
table of contents.

2. Push the modified CHANGELOG-x.y.md into the master branch of
kubernetes/kubernetes
a) Push the release notes to the 'release-x.y' branch as well if it’s a
patch release
2. Convert the markdown release notes into a HTML equivalent on purpose of
sending it by mail to the announce list. The HTML file will be dropped into
the current working directly as 'CHANGELOG-x.y.html'. Sending the
announcement is done by another subcommand of 'krel', not "changelog'.

3. Convert the markdown release notes into a HTML equivalent on purpose of
sending it by mail to the announce list. Sending the announcement is done
by another subcommand of 'krel', not "changelog'.
3. Push the modified CHANGELOG-x.y.md into the master branch as well as the
corresponding release-branch of kubernetes/kubernetes.
`,
SilenceUsage: true,
SilenceErrors: true,
Expand All @@ -72,11 +73,10 @@ the golang based 'release-notes' tool:
}

type changelogOptions struct {
tag string
bucket string
tars string
token string
outputDir string
tag string
bucket string
tars string
token string
}

var changelogOpts = &changelogOptions{}
Expand All @@ -98,7 +98,6 @@ func init() {
changelogCmd.PersistentFlags().StringVar(&changelogOpts.tag, tagFlag, "", "The version tag of the release, for example v1.17.0-rc.1")
changelogCmd.PersistentFlags().StringVar(&changelogOpts.tars, tarsFlag, "", "Directory of tars to sha512 sum for display")
changelogCmd.PersistentFlags().StringVarP(&changelogOpts.token, tokenFlag, "t", "", "GitHub token for release notes retrieval")
changelogCmd.PersistentFlags().StringVarP(&changelogOpts.outputDir, "output", "o", os.TempDir(), "Output directory for the generated files")

if err := changelogCmd.MarkPersistentFlagRequired(tagFlag); err != nil {
logrus.Fatal(err)
Expand All @@ -118,13 +117,17 @@ func runChangelog() (err error) {
branch := fmt.Sprintf("release-%d.%d", tag.Major, tag.Minor)
logrus.Infof("Using release branch %s", branch)

logrus.Infof("Using local repository path %s", rootOpts.repoPath)
repo, err := git.CloneOrOpenDefaultGitHubRepoSSH(rootOpts.repoPath, git.DefaultGithubOrg)
if err != nil {
return err
}
if !rootOpts.nomock {
logrus.Info("Using dry mode, which does not modify any remote content")
repo.SetDry()
}

var markdown string

if tag.Patch == 0 {
if len(tag.Pre) == 0 {
// New final minor versions should have remote release notes
Expand All @@ -150,21 +153,25 @@ func runChangelog() (err error) {
return err
}

logrus.Info("Generating TOC")
toc, err := notes.GenerateTOC(markdown)
if err != nil {
return err
}

if err := writeMarkdown(toc, markdown, tag); err != nil {
if err := repo.CheckoutBranch(git.Master); err != nil {
return errors.Wrap(err, "checking out master branch")
}

if err := writeMarkdown(repo, toc, markdown, tag); err != nil {
return err
}

if err := writeHTML(tag, markdown); err != nil {
return err
}

// TODO: Push changes into repo
return nil
return pushChanges(repo, branch, tag)
}

func generateReleaseNotes(branch, startRev, endRev string) (string, error) {
Expand Down Expand Up @@ -225,8 +232,8 @@ func generateReleaseNotes(branch, startRev, endRev string) (string, error) {
return markdown, nil
}

func writeMarkdown(toc, markdown string, tag semver.Version) error {
changelogPath := changelogFilename(tag, "md")
func writeMarkdown(repo *git.Repo, toc, markdown string, tag semver.Version) error {
changelogPath := markdownChangelogFilename(repo, tag)
writeFile := func(t, m string) error {
return ioutil.WriteFile(
changelogPath, []byte(strings.Join(
Expand All @@ -242,7 +249,7 @@ func writeMarkdown(toc, markdown string, tag semver.Version) error {
}

// Changelog seems to exist, prepend the notes and re-generate the TOC
logrus.Infof("Adding new content to changelog file %q ", changelogPath)
logrus.Infof("Adding new content to changelog file %s ", changelogPath)
content, err := ioutil.ReadFile(changelogPath)
if err != nil {
return err
Expand All @@ -268,11 +275,16 @@ func writeMarkdown(toc, markdown string, tag semver.Version) error {
return writeFile(mergedTOC, mergedMarkdown)
}

func htmlChangelogFilename(tag semver.Version) string {
return changelogFilename(tag, "html")
}

func markdownChangelogFilename(repo *git.Repo, tag semver.Version) string {
return filepath.Join(repo.Dir(), changelogFilename(tag, "md"))
}

func changelogFilename(tag semver.Version, ext string) string {
return filepath.Join(
changelogOpts.outputDir,
fmt.Sprintf("CHANGELOG-%d.%d.%s", tag.Major, tag.Minor, ext),
)
return fmt.Sprintf("CHANGELOG-%d.%d.%s", tag.Major, tag.Minor, ext)
}

func addTocMarkers(toc string) string {
Expand Down Expand Up @@ -316,9 +328,12 @@ func writeHTML(tag semver.Version, markdown string) error {
return err
}

outputPath := changelogFilename(tag, "html")
logrus.Infof("Writing single HTML to %s", outputPath)
return ioutil.WriteFile(outputPath, output.Bytes(), 0o644)
absOutputPath, err := filepath.Abs(htmlChangelogFilename(tag))
if err != nil {
return err
}
logrus.Infof("Writing single HTML to %s", absOutputPath)
return ioutil.WriteFile(absOutputPath, output.Bytes(), 0o644)
}

func lookupRemoteReleaseNotes(branch string) (string, error) {
Expand Down Expand Up @@ -350,3 +365,46 @@ func lookupRemoteReleaseNotes(branch string) (string, error) {

return string(content), nil
}

func pushChanges(repo *git.Repo, branch string, tag semver.Version) error {
// Master branch modifications
filename := filepath.Base(markdownChangelogFilename(repo, tag))
logrus.Infof("Adding %s to repository", filename)
if err := repo.Add(filename); err != nil {
return errors.Wrapf(err, "trying to add file %s to repository", filename)
}

logrus.Info("Committing changes to master branch in repository")
if err := repo.Commit(fmt.Sprintf(
"Add %s for %s", filename, util.AddTagPrefix(tag.String()),
)); err != nil {
return errors.Wrap(err, "committing changes into repository")
}

if err := repo.Push(git.Master); err != nil {
return errors.Wrap(err, "pushing changes to the remote master")
}

// Release branch modifications
if err := repo.CheckoutBranch(branch); err != nil {
return errors.Wrapf(err, "checking out release branch %s", branch)
}

logrus.Info("Checking out changelog from master branch")
if err := repo.Checkout(git.Master, filename); err != nil {
return errors.Wrap(err, "checking out master branch changelog")
}

logrus.Info("Committing changes to release branch in repository")
if err := repo.Commit(fmt.Sprintf(
"Update %s for %s", filename, util.AddTagPrefix(tag.String()),
)); err != nil {
return errors.Wrap(err, "committing changes into repository")
}

if err := repo.Push(branch); err != nil {
return errors.Wrap(err, "pushing changes to the remote release branch")
}

return nil
}
41 changes: 41 additions & 0 deletions pkg/git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"path/filepath"
"regexp"
"strings"
"time"

"github.com/blang/semver"
"github.com/pkg/errors"
Expand Down Expand Up @@ -386,6 +387,14 @@ func (r *Repo) CheckoutBranch(name string) error {
})
}

// Checkout can be used to checkout any revision inside the repository
func (r *Repo) Checkout(rev string, args ...string) error {
cmdArgs := append([]string{"checkout", rev}, args...)
return command.
NewWithWorkDir(r.Dir(), gitExecutable, cmdArgs...).
RunSilentSuccess()
}

func IsReleaseBranch(branch string) bool {
re := regexp.MustCompile(branchRE)
if !re.MatchString(branch) {
Expand Down Expand Up @@ -598,3 +607,35 @@ func (r *Repo) tagsForBranch(branch string) ([]string, error) {

return strings.Fields(status.Output()), nil
}

// Add adds a file to the staging area of the repo
func (r *Repo) Add(filename string) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it would be useful to add tests for Add and Commit? Per usual, I would be fine with them coming in a follow-up PR.

Copy link
Member Author

@saschagrunert saschagrunert Jan 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes sure, I added three more unit tests on top. I was not completely sure how to test a failing commit (should usually succeed), but I think this is not a critical path.

worktree, err := r.inner.Worktree()
if err != nil {
return err
}
_, err = worktree.Add(filename)
if err != nil {
return err
}
return nil
}

// Commit commits the current repository state
func (r *Repo) Commit(msg string) error {
worktree, err := r.inner.Worktree()
if err != nil {
return err
}
_, err = worktree.Commit(msg, &git.CommitOptions{
Author: &object.Signature{
Name: "Anago GCB",
Email: "[email protected]",
When: time.Now(),
},
})
if err != nil {
return err
}
return nil
}
81 changes: 81 additions & 0 deletions pkg/git/git_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ type testRepo struct {
secondTagName string
thirdTagCommit string
thirdTagName string
testFileName string
}

// newTestRepo creates a test repo with the following structure:
Expand Down Expand Up @@ -198,6 +199,7 @@ func newTestRepo(t *testing.T) *testRepo {
secondTagCommit: secondTagRef.Hash().String(),
thirdTagName: thirdTagName,
thirdTagCommit: thirdTagRef.Hash().String(),
testFileName: testFileName,
}
}

Expand Down Expand Up @@ -496,3 +498,82 @@ func TestTagsForBranchFailureWrongBranch(t *testing.T) {
require.NotNil(t, err)
require.Nil(t, result)
}

func TestCheckoutSuccess(t *testing.T) {
testRepo := newTestRepo(t)
defer testRepo.cleanup(t)

require.Nil(t, ioutil.WriteFile(
filepath.Join(testRepo.sut.Dir(), testRepo.testFileName),
[]byte("hello world"),
0o644,
))
res, err := command.NewWithWorkDir(
testRepo.sut.Dir(), "git", "diff", "--name-only").Run()
require.Nil(t, err)
require.True(t, res.Success())
require.Contains(t, res.Output(), testRepo.testFileName)

err = testRepo.sut.Checkout(Master, testRepo.testFileName)
require.Nil(t, err)

res, err = command.NewWithWorkDir(
testRepo.sut.Dir(), "git", "diff", "--name-only").Run()
require.Nil(t, err)
require.True(t, res.Success())
require.Empty(t, res.Output())
}

func TestCheckoutFailureWrongRevision(t *testing.T) {
testRepo := newTestRepo(t)
defer testRepo.cleanup(t)

err := testRepo.sut.Checkout("wrong")
require.NotNil(t, err)
require.Contains(t, err.Error(), "checkout wrong did not succeed")
}

func TestAddSuccess(t *testing.T) {
testRepo := newTestRepo(t)
defer testRepo.cleanup(t)

f, err := ioutil.TempFile(testRepo.sut.Dir(), "test")
require.Nil(t, err)
require.Nil(t, f.Close())

filename := filepath.Base(f.Name())
err = testRepo.sut.Add(filename)
require.Nil(t, err)

res, err := command.NewWithWorkDir(
testRepo.sut.Dir(), "git", "diff", "--cached", "--name-only").Run()
require.Nil(t, err)
require.True(t, res.Success())
require.Contains(t, res.Output(), filename)
}

func TestAddFailureWrongPath(t *testing.T) {
testRepo := newTestRepo(t)
defer testRepo.cleanup(t)

err := testRepo.sut.Add("wrong")
require.NotNil(t, err)
require.Contains(t, err.Error(), "entry not found")
}

func TestCommitSuccess(t *testing.T) {
testRepo := newTestRepo(t)
defer testRepo.cleanup(t)

commitMessage := "My commit message for this test"
err := testRepo.sut.Commit(commitMessage)
require.Nil(t, err)

res, err := command.NewWithWorkDir(
testRepo.sut.Dir(), "git", "log", "-1",
).Run()
require.Nil(t, err)
require.True(t, res.Success())
require.Contains(t, res.Output(), "Author: Anago GCB <[email protected]>")
require.Contains(t, res.Output(), commitMessage)
}