mirror of
https://github.com/git-chglog/git-chglog.git
synced 2026-01-23 02:15:12 +00:00
commit
29102c1d91
27 changed files with 1201 additions and 480 deletions
11
Makefile
11
Makefile
|
|
@ -11,6 +11,7 @@ clean:
|
|||
rm -rf ./dist/
|
||||
rm -rf ./git-chglog
|
||||
rm -rf $(GOPATH)/bin/git-chglog
|
||||
rm -rf cover.out
|
||||
|
||||
.PHONY: bulid
|
||||
build:
|
||||
|
|
@ -20,10 +21,12 @@ build:
|
|||
test:
|
||||
go test -v `go list ./... | grep -v /vendor/`
|
||||
|
||||
.PHONY: coverage
|
||||
coverage:
|
||||
goverage -coverprofile=cover.out `go list ./... | grep -v /vendor/`
|
||||
go tool cover -func=cover.out
|
||||
@rm -rf cover.out
|
||||
|
||||
.PHONY: install
|
||||
install:
|
||||
go install ./cmd/git-chglog
|
||||
|
||||
.PHONY: chglog
|
||||
chglog:
|
||||
git-chglog -c ./.chglog/config.yml
|
||||
|
|
|
|||
6
cmd/git-chglog/builder.go
Normal file
6
cmd/git-chglog/builder.go
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
package main
|
||||
|
||||
// Builder ...
|
||||
type Builder interface {
|
||||
Build(*Answer) (string, error)
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ import (
|
|||
|
||||
// CLI ...
|
||||
type CLI struct {
|
||||
ctx *Context
|
||||
ctx *CLIContext
|
||||
fs FileSystem
|
||||
logger *Logger
|
||||
configLoader ConfigLoader
|
||||
|
|
@ -22,8 +22,7 @@ type CLI struct {
|
|||
|
||||
// NewCLI ...
|
||||
func NewCLI(
|
||||
ctx *Context,
|
||||
fs FileSystem,
|
||||
ctx *CLIContext, fs FileSystem,
|
||||
configLoader ConfigLoader,
|
||||
generator Generator,
|
||||
) *CLI {
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ func TestCLIForStdout(t *testing.T) {
|
|||
}
|
||||
|
||||
c := NewCLI(
|
||||
&Context{
|
||||
&CLIContext{
|
||||
WorkingDir: "/",
|
||||
ConfigPath: "/.chglog/config.yml",
|
||||
OutputPath: "",
|
||||
|
|
@ -108,7 +108,7 @@ func TestCLIForFile(t *testing.T) {
|
|||
}
|
||||
|
||||
c := NewCLI(
|
||||
&Context{
|
||||
&CLIContext{
|
||||
WorkingDir: "/",
|
||||
ConfigPath: "/.chglog/config.yml",
|
||||
OutputPath: "/dir/to/CHANGELOG.tpl",
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ type Config struct {
|
|||
}
|
||||
|
||||
// Normalize ...
|
||||
func (config *Config) Normalize(ctx *Context) error {
|
||||
func (config *Config) Normalize(ctx *CLIContext) error {
|
||||
err := mergo.Merge(config, &Config{
|
||||
Bin: "git",
|
||||
Template: "CHANGELOG.tpl.md",
|
||||
|
|
@ -148,7 +148,7 @@ func (config *Config) normalizeStyleOfGitHub() {
|
|||
}
|
||||
|
||||
// Convert ...
|
||||
func (config *Config) Convert(ctx *Context) *chglog.Config {
|
||||
func (config *Config) Convert(ctx *CLIContext) *chglog.Config {
|
||||
info := config.Info
|
||||
opts := config.Options
|
||||
|
||||
|
|
|
|||
73
cmd/git-chglog/config_builder.go
Normal file
73
cmd/git-chglog/config_builder.go
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ConfigBuilder ...
|
||||
type ConfigBuilder interface {
|
||||
Builder
|
||||
}
|
||||
|
||||
type configBuilderImpl struct{}
|
||||
|
||||
// NewConfigBuilder ...
|
||||
func NewConfigBuilder() ConfigBuilder {
|
||||
return &configBuilderImpl{}
|
||||
}
|
||||
|
||||
// Build ...
|
||||
func (*configBuilderImpl) Build(ans *Answer) (string, error) {
|
||||
var msgFormat *CommitMessageFormat
|
||||
|
||||
for _, f := range formats {
|
||||
if f.Display == ans.CommitMessageFormat {
|
||||
msgFormat = f
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if msgFormat == nil {
|
||||
return "", fmt.Errorf("\"%s\" is an invalid commit message format", ans.CommitMessageFormat)
|
||||
}
|
||||
|
||||
repoURL := strings.TrimRight(ans.RepositoryURL, "/")
|
||||
if repoURL == "" {
|
||||
repoURL = "\"\""
|
||||
}
|
||||
|
||||
config := fmt.Sprintf(`style: %s
|
||||
template: %s
|
||||
info:
|
||||
title: CHANGELOG
|
||||
repository_url: %s
|
||||
options:
|
||||
commits:
|
||||
# filters:
|
||||
# Type:
|
||||
# - feat
|
||||
# - fix
|
||||
# - perf
|
||||
# - refactor
|
||||
commit_groups:
|
||||
# title_maps:
|
||||
# feat: Features
|
||||
# fix: Bug Fixes
|
||||
# perf: Performance Improvements
|
||||
# refactor: Code Refactoring
|
||||
header:
|
||||
pattern: "%s"
|
||||
pattern_maps:%s
|
||||
notes:
|
||||
keywords:
|
||||
- BREAKING CHANGE`,
|
||||
ans.Style,
|
||||
defaultTemplateFilename,
|
||||
repoURL,
|
||||
msgFormat.Pattern,
|
||||
msgFormat.PatternMapString(),
|
||||
)
|
||||
|
||||
return config, nil
|
||||
}
|
||||
9
cmd/git-chglog/config_builder_mock.go
Normal file
9
cmd/git-chglog/config_builder_mock.go
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package main
|
||||
|
||||
type mockConfigBuilderImpl struct {
|
||||
ReturnBuild func(*Answer) (string, error)
|
||||
}
|
||||
|
||||
func (m *mockConfigBuilderImpl) Build(ans *Answer) (string, error) {
|
||||
return m.ReturnBuild(ans)
|
||||
}
|
||||
62
cmd/git-chglog/config_builder_test.go
Normal file
62
cmd/git-chglog/config_builder_test.go
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConfigBulider(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
builder := NewConfigBuilder()
|
||||
|
||||
out, err := builder.Build(&Answer{
|
||||
RepositoryURL: "https://github.com/git-chglog/git-chglog/git-chglog/",
|
||||
Style: styleNone,
|
||||
CommitMessageFormat: fmtGitBasic.Display,
|
||||
Template: tplStandard,
|
||||
})
|
||||
|
||||
assert.Nil(err)
|
||||
assert.Contains(out, "style: none")
|
||||
assert.Contains(out, "template: CHANGELOG.tpl.md")
|
||||
assert.Contains(out, " repository_url: https://github.com/git-chglog/git-chglog/git-chglog")
|
||||
assert.Contains(out, fmt.Sprintf(" pattern: \"%s\"", fmtGitBasic.Pattern))
|
||||
assert.Contains(out, fmt.Sprintf(
|
||||
` pattern_maps:
|
||||
- %s
|
||||
- %s`,
|
||||
fmtGitBasic.PatternMaps[0],
|
||||
fmtGitBasic.PatternMaps[1],
|
||||
))
|
||||
}
|
||||
|
||||
func TestConfigBuliderEmptyRepoURL(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
builder := NewConfigBuilder()
|
||||
|
||||
out, err := builder.Build(&Answer{
|
||||
RepositoryURL: "",
|
||||
Style: styleNone,
|
||||
CommitMessageFormat: fmtGitBasic.Display,
|
||||
Template: tplStandard,
|
||||
})
|
||||
|
||||
assert.Nil(err)
|
||||
assert.Contains(out, " repository_url: \"\"")
|
||||
}
|
||||
|
||||
func TestConfigBuliderInvalidFormat(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
builder := NewConfigBuilder()
|
||||
|
||||
_, err := builder.Build(&Answer{
|
||||
RepositoryURL: "",
|
||||
Style: styleNone,
|
||||
CommitMessageFormat: "",
|
||||
Template: tplStandard,
|
||||
})
|
||||
|
||||
assert.Contains(err.Error(), "invalid commit message format")
|
||||
}
|
||||
|
|
@ -16,7 +16,7 @@ func TestConfigNormalize(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
err := config.Normalize(&Context{
|
||||
err := config.Normalize(&CLIContext{
|
||||
ConfigPath: "/test/config.yml",
|
||||
})
|
||||
|
||||
|
|
@ -30,7 +30,7 @@ func TestConfigNormalize(t *testing.T) {
|
|||
Template: "/CHANGELOG.tpl.md",
|
||||
}
|
||||
|
||||
err = config.Normalize(&Context{
|
||||
err = config.Normalize(&CLIContext{
|
||||
ConfigPath: "/test/config.yml",
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import (
|
|||
"io"
|
||||
)
|
||||
|
||||
// Context ...
|
||||
type Context struct {
|
||||
// CLIContext ...
|
||||
type CLIContext struct {
|
||||
WorkingDir string
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
|
|
@ -16,3 +16,10 @@ type Context struct {
|
|||
NoEmoji bool
|
||||
Query string
|
||||
}
|
||||
|
||||
// InitContext ...
|
||||
type InitContext struct {
|
||||
WorkingDir string
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,16 @@ package main
|
|||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
// FileSystem ...
|
||||
type FileSystem interface {
|
||||
Exists(path string) bool
|
||||
MkdirP(path string) error
|
||||
Create(name string) (File, error)
|
||||
WriteFile(path string, content []byte) error
|
||||
}
|
||||
|
||||
// File ...
|
||||
|
|
@ -25,6 +28,11 @@ var fs = &osFileSystem{}
|
|||
|
||||
type osFileSystem struct{}
|
||||
|
||||
func (*osFileSystem) Exists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (*osFileSystem) MkdirP(path string) error {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return os.MkdirAll(path, os.ModePerm)
|
||||
|
|
@ -35,3 +43,7 @@ func (*osFileSystem) MkdirP(path string) error {
|
|||
func (*osFileSystem) Create(name string) (File, error) {
|
||||
return os.Create(name)
|
||||
}
|
||||
|
||||
func (*osFileSystem) WriteFile(path string, content []byte) error {
|
||||
return ioutil.WriteFile(path, content, os.ModePerm)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
package main
|
||||
|
||||
type mockFileSystem struct {
|
||||
ReturnMkdirP func(string) error
|
||||
ReturnCreate func(string) (File, error)
|
||||
ReturnExists func(string) bool
|
||||
ReturnMkdirP func(string) error
|
||||
ReturnCreate func(string) (File, error)
|
||||
ReturnWriteFile func(string, []byte) error
|
||||
}
|
||||
|
||||
func (m *mockFileSystem) Exists(path string) bool {
|
||||
return m.ReturnExists(path)
|
||||
}
|
||||
|
||||
func (m *mockFileSystem) MkdirP(path string) error {
|
||||
|
|
@ -13,6 +19,10 @@ func (m *mockFileSystem) Create(name string) (File, error) {
|
|||
return m.ReturnCreate(name)
|
||||
}
|
||||
|
||||
func (m *mockFileSystem) WriteFile(path string, content []byte) error {
|
||||
return m.ReturnWriteFile(path, content)
|
||||
}
|
||||
|
||||
type mockFile struct {
|
||||
File
|
||||
ReturnWrite func([]byte) (int, error)
|
||||
|
|
|
|||
|
|
@ -1,419 +1,95 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/fatih/color"
|
||||
gitcmd "github.com/tsuyoshiwada/go-gitcmd"
|
||||
survey "gopkg.in/AlecAivazis/survey.v1"
|
||||
emoji "gopkg.in/kyokomi/emoji.v1"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultConfigFilename = "config.yml"
|
||||
defaultTemplateFilename = "CHANGELOG.tpl.md"
|
||||
|
||||
styleGitHub = "github"
|
||||
styleNone = "none"
|
||||
changelogStyles = []string{
|
||||
styleGitHub,
|
||||
styleNone,
|
||||
}
|
||||
|
||||
fmtTypeScopeSubject = "<type>(<scope>): <subject> -- feat(core) Add new feature"
|
||||
fmtTypeSubject = "<type>: <subject> -- feat: Add new feature"
|
||||
fmtGitBasic = "<<type> subject> -- Add new feature"
|
||||
fmtSubject = "<subject> -- Add new feature (Not detect `type` field)"
|
||||
commitMessageFormats = []string{
|
||||
fmtTypeScopeSubject,
|
||||
fmtTypeSubject,
|
||||
fmtGitBasic,
|
||||
fmtSubject,
|
||||
}
|
||||
|
||||
tplStandard = "standard"
|
||||
tplCool = "cool"
|
||||
templateStyles = []string{
|
||||
tplStandard,
|
||||
tplCool,
|
||||
}
|
||||
)
|
||||
|
||||
// Answer ...
|
||||
type Answer struct {
|
||||
RepositoryURL string `survey:"repository_url"`
|
||||
Style string `survey:"style"`
|
||||
CommitMessageFormat string `survey:"commit_message_format"`
|
||||
Template string `survey:"template"`
|
||||
IncludeMerges bool `survey:"include_merges"`
|
||||
IncludeReverts bool `survey:"include_reverts"`
|
||||
ConfigDir string `survey:"config_dir"`
|
||||
}
|
||||
|
||||
// Initializer ...
|
||||
type Initializer struct {
|
||||
client gitcmd.Client
|
||||
ctx *InitContext
|
||||
client gitcmd.Client
|
||||
fs FileSystem
|
||||
logger *Logger
|
||||
questioner Questioner
|
||||
configBuilder ConfigBuilder
|
||||
templateBuilder TemplateBuilder
|
||||
}
|
||||
|
||||
// NewInitializer ...
|
||||
func NewInitializer() *Initializer {
|
||||
func NewInitializer(
|
||||
ctx *InitContext,
|
||||
fs FileSystem,
|
||||
questioner Questioner,
|
||||
configBuilder ConfigBuilder,
|
||||
templateBuilder TemplateBuilder,
|
||||
) *Initializer {
|
||||
return &Initializer{
|
||||
client: gitcmd.New(&gitcmd.Config{
|
||||
Bin: "git",
|
||||
}),
|
||||
ctx: ctx,
|
||||
fs: fs,
|
||||
logger: NewLogger(ctx.Stdout, ctx.Stderr, false, false),
|
||||
questioner: questioner,
|
||||
configBuilder: configBuilder,
|
||||
templateBuilder: templateBuilder,
|
||||
}
|
||||
}
|
||||
|
||||
// Run ...
|
||||
func (init *Initializer) Run() int {
|
||||
answer, err := init.ask()
|
||||
ans, err := init.questioner.Ask()
|
||||
if err != nil {
|
||||
init.logger.Error(err.Error())
|
||||
return ExitCodeError
|
||||
}
|
||||
|
||||
err = init.generateConfigure(answer)
|
||||
if err != nil {
|
||||
if err = init.fs.MkdirP(filepath.Join(init.ctx.WorkingDir, ans.ConfigDir)); err != nil {
|
||||
init.logger.Error(err.Error())
|
||||
return ExitCodeError
|
||||
}
|
||||
|
||||
if err = init.generateConfig(ans); err != nil {
|
||||
init.logger.Error(err.Error())
|
||||
return ExitCodeError
|
||||
}
|
||||
|
||||
if err = init.generateTemplate(ans); err != nil {
|
||||
init.logger.Error(err.Error())
|
||||
return ExitCodeError
|
||||
}
|
||||
|
||||
success := color.CyanString("✔")
|
||||
emoji.Fprintf(os.Stdout, `
|
||||
init.logger.Log(fmt.Sprintf(`
|
||||
:sparkles: %s
|
||||
%s %s
|
||||
%s %s
|
||||
|
||||
`,
|
||||
color.GreenString("Configuration file and template generation completed!"),
|
||||
success,
|
||||
filepath.Join(answer.ConfigDir, defaultConfigFilename),
|
||||
filepath.Join(ans.ConfigDir, defaultConfigFilename),
|
||||
success,
|
||||
filepath.Join(answer.ConfigDir, defaultTemplateFilename),
|
||||
)
|
||||
filepath.Join(ans.ConfigDir, defaultTemplateFilename),
|
||||
))
|
||||
|
||||
return ExitCodeOK
|
||||
}
|
||||
|
||||
func (init *Initializer) ask() (*Answer, error) {
|
||||
answer := &Answer{}
|
||||
qs := init.createQuestions()
|
||||
err := survey.Ask(qs, answer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return answer, nil
|
||||
}
|
||||
|
||||
func (init *Initializer) createQuestions() []*survey.Question {
|
||||
originURL := init.getRepositoryURL()
|
||||
|
||||
return []*survey.Question{
|
||||
{
|
||||
Name: "repository_url",
|
||||
Prompt: &survey.Input{
|
||||
Message: "What is the URL of your repository?",
|
||||
Default: originURL,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "style",
|
||||
Prompt: &survey.Select{
|
||||
Message: "What is your favorite style?",
|
||||
Options: changelogStyles,
|
||||
Default: changelogStyles[0],
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "commit_message_format",
|
||||
Prompt: &survey.Select{
|
||||
Message: "Choose the format of your favorite commit message",
|
||||
Options: commitMessageFormats,
|
||||
Default: commitMessageFormats[0],
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "template",
|
||||
Prompt: &survey.Select{
|
||||
Message: "What is your favorite template style?",
|
||||
Options: templateStyles,
|
||||
Default: templateStyles[0],
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "include_merges",
|
||||
Prompt: &survey.Confirm{
|
||||
Message: "Do you include Merge Commit in CHANGELOG?",
|
||||
Default: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "include_reverts",
|
||||
Prompt: &survey.Confirm{
|
||||
Message: "Do you include Revert Commit in CHANGELOG?",
|
||||
Default: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "config_dir",
|
||||
Prompt: &survey.Input{
|
||||
Message: "In which directory do you output configuration files and templates?",
|
||||
Default: ".chglog",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (init *Initializer) getRepositoryURL() string {
|
||||
if init.client.CanExec() != nil || init.client.InsideWorkTree() != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
rawurl, err := init.client.Exec("config", "--get", "remote.origin.url")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return remoteOriginURLToHTTP(rawurl)
|
||||
}
|
||||
|
||||
func (init *Initializer) generateConfigure(answer *Answer) error {
|
||||
var err error
|
||||
|
||||
err = fs.MkdirP(answer.ConfigDir)
|
||||
func (init *Initializer) generateConfig(ans *Answer) error {
|
||||
s, err := init.configBuilder.Build(ans)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config := init.createConfigYamlContent(answer)
|
||||
tpl := init.createTemplate(answer)
|
||||
return init.fs.WriteFile(filepath.Join(init.ctx.WorkingDir, ans.ConfigDir, defaultConfigFilename), []byte(s))
|
||||
}
|
||||
|
||||
configPath := filepath.Join(answer.ConfigDir, defaultConfigFilename)
|
||||
templatePath := filepath.Join(answer.ConfigDir, defaultTemplateFilename)
|
||||
|
||||
err = init.createFileWithConfirm(configPath, config)
|
||||
func (init *Initializer) generateTemplate(ans *Answer) error {
|
||||
s, err := init.templateBuilder.Build(ans)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = init.createFileWithConfirm(templatePath, tpl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*Initializer) createFileWithConfirm(path, content string) error {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
answer := struct {
|
||||
OK bool
|
||||
}{}
|
||||
|
||||
err := survey.Ask([]*survey.Question{
|
||||
{
|
||||
Name: "ok",
|
||||
Prompt: &survey.Confirm{
|
||||
Message: fmt.Sprintf("\"%s\" already exists. Do you want to overwrite?", path),
|
||||
Default: true,
|
||||
},
|
||||
},
|
||||
}, &answer)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !answer.OK {
|
||||
return errors.New("creation of the file was interrupted")
|
||||
}
|
||||
}
|
||||
|
||||
err := ioutil.WriteFile(path, []byte(content), os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (init *Initializer) createConfigYamlContent(answer *Answer) string {
|
||||
var (
|
||||
style = answer.Style
|
||||
template = defaultTemplateFilename
|
||||
repositoryURL = answer.RepositoryURL
|
||||
headerPattern string
|
||||
headerPatternMaps string
|
||||
)
|
||||
|
||||
switch answer.CommitMessageFormat {
|
||||
case fmtTypeScopeSubject:
|
||||
headerPattern = `^(\\w*)(?:\\(([\\w\\$\\.\\-\\*\\s]*)\\))?\\:\\s(.*)$`
|
||||
headerPatternMaps = `
|
||||
- Type
|
||||
- Scope
|
||||
- Subject`
|
||||
case fmtTypeSubject:
|
||||
headerPattern = `^(\\w*)\\:\\s(.*)$`
|
||||
headerPatternMaps = `
|
||||
- Type
|
||||
- Subject`
|
||||
case fmtGitBasic:
|
||||
headerPattern = `^((\\w+)\\s.*)$`
|
||||
headerPatternMaps = `
|
||||
- Subject
|
||||
- Type`
|
||||
case fmtSubject:
|
||||
headerPattern = `^(.*)$`
|
||||
headerPatternMaps = `
|
||||
- Subject`
|
||||
}
|
||||
|
||||
config := fmt.Sprintf(`style: %s
|
||||
template: %s
|
||||
info:
|
||||
title: CHANGELOG
|
||||
repository_url: %s
|
||||
options:
|
||||
commits:
|
||||
# filters:
|
||||
# Type:
|
||||
# - feat
|
||||
# - fix
|
||||
# - perf
|
||||
# - refactor
|
||||
commit_groups:
|
||||
# title_maps:
|
||||
# feat: Features
|
||||
# fix: Bug Fixes
|
||||
# perf: Performance Improvements
|
||||
# refactor: Code Refactoring
|
||||
header:
|
||||
pattern: "%s"
|
||||
pattern_maps:%s
|
||||
notes:
|
||||
keywords:
|
||||
- BREAKING CHANGE`,
|
||||
style,
|
||||
template,
|
||||
repositoryURL,
|
||||
headerPattern,
|
||||
headerPatternMaps,
|
||||
)
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
func (init *Initializer) createTemplate(answer *Answer) string {
|
||||
tpl := "{{range .Versions}}\n"
|
||||
|
||||
// versions
|
||||
tpl += init.versionHeader(answer.Style, answer.Template)
|
||||
|
||||
// commits
|
||||
tpl += init.commits(answer.Style, answer.Template, answer.CommitMessageFormat)
|
||||
|
||||
// revert
|
||||
if answer.IncludeReverts {
|
||||
tpl += `{{if .RevertCommits}}
|
||||
### Reverts
|
||||
{{range .RevertCommits}}
|
||||
* {{.Revert.Header}}{{end}}
|
||||
{{end}}`
|
||||
}
|
||||
|
||||
// merge
|
||||
if answer.IncludeMerges {
|
||||
tpl += fmt.Sprintf(`{{if .MergeCommits}}
|
||||
### %s
|
||||
{{range .MergeCommits}}
|
||||
* {{.Header}}{{end}}
|
||||
{{end}}`, init.mergeTitle(answer.Style))
|
||||
}
|
||||
|
||||
tpl += `{{range .NoteGroups}}
|
||||
### {{.Title}}
|
||||
{{range .Notes}}
|
||||
{{.Body}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}`
|
||||
|
||||
return tpl
|
||||
}
|
||||
|
||||
func (*Initializer) versionHeader(style, template string) string {
|
||||
var (
|
||||
tpl string
|
||||
tagName string
|
||||
date = "{{datetime \"2006-01-02\" .Tag.Date}}"
|
||||
)
|
||||
|
||||
// parts
|
||||
switch style {
|
||||
case styleGitHub:
|
||||
tpl = "<a name=\"{{.Tag.Name}}\"></a>\n"
|
||||
tagName = "{{if .Tag.Previous}}[{{.Tag.Name}}]({{$.Info.RepositoryURL}}/compare/{{.Tag.Previous.Name}}...{{.Tag.Name}}){{else}}{{.Tag.Name}}{{end}}"
|
||||
default:
|
||||
tagName = "{{.Tag.Name}}"
|
||||
}
|
||||
|
||||
// format
|
||||
switch template {
|
||||
case tplStandard:
|
||||
tpl = fmt.Sprintf("%s## %s (%s)\n",
|
||||
tpl,
|
||||
tagName,
|
||||
date,
|
||||
)
|
||||
case tplCool:
|
||||
tpl = fmt.Sprintf("%s## %s\n\n> %s\n",
|
||||
tpl,
|
||||
tagName,
|
||||
date,
|
||||
)
|
||||
}
|
||||
|
||||
return tpl
|
||||
}
|
||||
|
||||
func (*Initializer) commits(style, template, format string) string {
|
||||
var (
|
||||
header string
|
||||
body string
|
||||
)
|
||||
|
||||
switch format {
|
||||
case fmtSubject:
|
||||
body = `{{range .Commits}}
|
||||
* {{.Header}}{{end}}
|
||||
`
|
||||
|
||||
default:
|
||||
if format == fmtTypeScopeSubject {
|
||||
header = "{{if ne .Scope \"\"}}**{{.Scope}}:** {{end}}{{.Subject}}"
|
||||
} else {
|
||||
header = "{{.Subject}}"
|
||||
}
|
||||
|
||||
body = fmt.Sprintf(`### {{.Title}}
|
||||
{{range .Commits}}
|
||||
* %s{{end}}
|
||||
`, header)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`{{range .CommitGroups}}
|
||||
%s{{end}}`, body)
|
||||
}
|
||||
|
||||
func (*Initializer) mergeTitle(style string) string {
|
||||
switch style {
|
||||
case styleGitHub:
|
||||
return "Pull Requests"
|
||||
default:
|
||||
return "Merges"
|
||||
}
|
||||
return init.fs.WriteFile(filepath.Join(init.ctx.WorkingDir, ans.ConfigDir, defaultTemplateFilename), []byte(s))
|
||||
}
|
||||
|
|
|
|||
73
cmd/git-chglog/initializer_test.go
Normal file
73
cmd/git-chglog/initializer_test.go
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestInitializer(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
stdout := &bytes.Buffer{}
|
||||
stderr := &bytes.Buffer{}
|
||||
|
||||
mockFs := &mockFileSystem{
|
||||
ReturnMkdirP: func(path string) error {
|
||||
if path == "/test/config" {
|
||||
return nil
|
||||
}
|
||||
return errors.New("")
|
||||
},
|
||||
ReturnWriteFile: func(path string, content []byte) error {
|
||||
if path == "/test/config/config.yml" || path == "/test/config/CHANGELOG.tpl.md" {
|
||||
return nil
|
||||
}
|
||||
return errors.New("")
|
||||
},
|
||||
}
|
||||
|
||||
questioner := &mockQuestionerImpl{
|
||||
ReturnAsk: func() (*Answer, error) {
|
||||
return &Answer{
|
||||
ConfigDir: "config",
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
configBuilder := &mockConfigBuilderImpl{
|
||||
ReturnBuild: func(ans *Answer) (string, error) {
|
||||
if ans.ConfigDir == "config" {
|
||||
return "config", nil
|
||||
}
|
||||
return "", errors.New("")
|
||||
},
|
||||
}
|
||||
|
||||
tplBuilder := &mockTemplateBuilderImpl{
|
||||
ReturnBuild: func(ans *Answer) (string, error) {
|
||||
if ans.ConfigDir == "config" {
|
||||
return "template", nil
|
||||
}
|
||||
return "", errors.New("")
|
||||
},
|
||||
}
|
||||
|
||||
init := NewInitializer(
|
||||
&InitContext{
|
||||
WorkingDir: "/test",
|
||||
Stdout: stdout,
|
||||
Stderr: stderr,
|
||||
},
|
||||
mockFs,
|
||||
questioner,
|
||||
configBuilder,
|
||||
tplBuilder,
|
||||
)
|
||||
|
||||
assert.Equal(ExitCodeOK, init.Run())
|
||||
assert.Equal("", stderr.String())
|
||||
assert.Contains(stdout.String(), "Configuration file and template")
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"os"
|
||||
|
||||
"github.com/fatih/color"
|
||||
gitcmd "github.com/tsuyoshiwada/go-gitcmd"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
|
|
@ -111,29 +112,44 @@ func main() {
|
|||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "failed to get working directory", err)
|
||||
os.Exit(1)
|
||||
os.Exit(ExitCodeError)
|
||||
}
|
||||
|
||||
// initializer
|
||||
if c.Bool("init") {
|
||||
os.Exit(NewInitializer().Run())
|
||||
initializer := NewInitializer(
|
||||
&InitContext{
|
||||
WorkingDir: wd,
|
||||
Stdout: os.Stdout,
|
||||
Stderr: os.Stderr,
|
||||
},
|
||||
fs,
|
||||
NewQuestioner(
|
||||
gitcmd.New(&gitcmd.Config{
|
||||
Bin: "git",
|
||||
}),
|
||||
fs,
|
||||
),
|
||||
NewConfigBuilder(),
|
||||
NewTemplateBuilder(),
|
||||
)
|
||||
|
||||
os.Exit(initializer.Run())
|
||||
}
|
||||
|
||||
// chglog
|
||||
ctx := &Context{
|
||||
WorkingDir: wd,
|
||||
Stdout: os.Stdout,
|
||||
Stderr: os.Stderr,
|
||||
ConfigPath: c.String("config"),
|
||||
OutputPath: c.String("output"),
|
||||
Silent: c.Bool("silent"),
|
||||
NoColor: c.Bool("no-color"),
|
||||
NoEmoji: c.Bool("no-emoji"),
|
||||
Query: c.Args().First(),
|
||||
}
|
||||
|
||||
chglogCLI := NewCLI(
|
||||
ctx,
|
||||
&CLIContext{
|
||||
WorkingDir: wd,
|
||||
Stdout: os.Stdout,
|
||||
Stderr: os.Stderr,
|
||||
ConfigPath: c.String("config"),
|
||||
OutputPath: c.String("output"),
|
||||
Silent: c.Bool("silent"),
|
||||
NoColor: c.Bool("no-color"),
|
||||
NoEmoji: c.Bool("no-emoji"),
|
||||
Query: c.Args().First(),
|
||||
},
|
||||
fs,
|
||||
NewConfigLoader(),
|
||||
NewGenerator(),
|
||||
|
|
|
|||
188
cmd/git-chglog/questioner.go
Normal file
188
cmd/git-chglog/questioner.go
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
gitcmd "github.com/tsuyoshiwada/go-gitcmd"
|
||||
survey "gopkg.in/AlecAivazis/survey.v1"
|
||||
)
|
||||
|
||||
// Answer ...
|
||||
type Answer struct {
|
||||
RepositoryURL string `survey:"repository_url"`
|
||||
Style string `survey:"style"`
|
||||
CommitMessageFormat string `survey:"commit_message_format"`
|
||||
Template string `survey:"template"`
|
||||
IncludeMerges bool `survey:"include_merges"`
|
||||
IncludeReverts bool `survey:"include_reverts"`
|
||||
ConfigDir string `survey:"config_dir"`
|
||||
}
|
||||
|
||||
// Questioner ...
|
||||
type Questioner interface {
|
||||
Ask() (*Answer, error)
|
||||
}
|
||||
|
||||
type questionerImpl struct {
|
||||
client gitcmd.Client
|
||||
fs FileSystem
|
||||
}
|
||||
|
||||
// NewQuestioner ...
|
||||
func NewQuestioner(client gitcmd.Client, fs FileSystem) Questioner {
|
||||
return &questionerImpl{
|
||||
client: client,
|
||||
fs: fs,
|
||||
}
|
||||
}
|
||||
|
||||
// Ask ...
|
||||
func (q *questionerImpl) Ask() (*Answer, error) {
|
||||
ans, err := q.ask()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config := filepath.Join(ans.ConfigDir, defaultConfigFilename)
|
||||
tpl := filepath.Join(ans.ConfigDir, defaultTemplateFilename)
|
||||
c := q.fs.Exists(config)
|
||||
t := q.fs.Exists(tpl)
|
||||
msg := ""
|
||||
|
||||
if c && t {
|
||||
msg = fmt.Sprintf("\"%s\" and \"%s\" already exists. Do you want to overwrite?", config, tpl)
|
||||
} else if c {
|
||||
msg = fmt.Sprintf("\"%s\" already exists. Do you want to overwrite?", config)
|
||||
} else if t {
|
||||
msg = fmt.Sprintf("\"%s\" already exists. Do you want to overwrite?", tpl)
|
||||
}
|
||||
|
||||
if msg != "" {
|
||||
overwrite := false
|
||||
err = survey.AskOne(&survey.Confirm{
|
||||
Message: msg,
|
||||
Default: true,
|
||||
}, &overwrite, nil)
|
||||
|
||||
if err != nil || !overwrite {
|
||||
return nil, errors.New("creation of the file was interrupted")
|
||||
}
|
||||
}
|
||||
|
||||
return ans, nil
|
||||
}
|
||||
|
||||
func (q *questionerImpl) ask() (*Answer, error) {
|
||||
ans := &Answer{}
|
||||
fmts := q.getFormats()
|
||||
|
||||
questions := []*survey.Question{
|
||||
{
|
||||
Name: "repository_url",
|
||||
Prompt: &survey.Input{
|
||||
Message: "What is the URL of your repository?",
|
||||
Default: q.getRepositoryURL(),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "style",
|
||||
Prompt: &survey.Select{
|
||||
Message: "What is your favorite style?",
|
||||
Options: styles,
|
||||
Default: styles[0],
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "commit_message_format",
|
||||
Prompt: &survey.Select{
|
||||
Message: "Choose the format of your favorite commit message",
|
||||
Options: fmts,
|
||||
Default: fmts[0],
|
||||
},
|
||||
Transform: func(ans interface{}) (newAns interface{}) {
|
||||
if s, ok := ans.(string); ok {
|
||||
newAns = q.parseFormat(s)
|
||||
}
|
||||
return
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "template",
|
||||
Prompt: &survey.Select{
|
||||
Message: "What is your favorite template style?",
|
||||
Options: templates,
|
||||
Default: templates[0],
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "include_merges",
|
||||
Prompt: &survey.Confirm{
|
||||
Message: "Do you include Merge Commit in CHANGELOG?",
|
||||
Default: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "include_reverts",
|
||||
Prompt: &survey.Confirm{
|
||||
Message: "Do you include Revert Commit in CHANGELOG?",
|
||||
Default: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "config_dir",
|
||||
Prompt: &survey.Input{
|
||||
Message: "In which directory do you output configuration files and templates?",
|
||||
Default: defaultConfigDir,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := survey.Ask(questions, ans)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ans, nil
|
||||
}
|
||||
|
||||
func (*questionerImpl) getFormats() []string {
|
||||
arr := make([]string, len(formats))
|
||||
max := 0
|
||||
|
||||
for _, f := range formats {
|
||||
l := len(f.Display)
|
||||
if max < l {
|
||||
max = l
|
||||
}
|
||||
}
|
||||
|
||||
for i, f := range formats {
|
||||
arr[i] = fmt.Sprintf(
|
||||
"%s -- %s",
|
||||
f.Display+strings.Repeat(" ", max-len(f.Display)),
|
||||
f.Preview,
|
||||
)
|
||||
}
|
||||
|
||||
return arr
|
||||
}
|
||||
|
||||
func (*questionerImpl) parseFormat(input string) string {
|
||||
return strings.TrimSpace(strings.Split(input, "--")[0])
|
||||
}
|
||||
|
||||
func (q *questionerImpl) getRepositoryURL() string {
|
||||
if q.client.CanExec() != nil || q.client.InsideWorkTree() != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
rawurl, err := q.client.Exec("config", "--get", "remote.origin.url")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return remoteOriginURLToHTTP(rawurl)
|
||||
}
|
||||
9
cmd/git-chglog/questioner_mock.go
Normal file
9
cmd/git-chglog/questioner_mock.go
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package main
|
||||
|
||||
type mockQuestionerImpl struct {
|
||||
ReturnAsk func() (*Answer, error)
|
||||
}
|
||||
|
||||
func (m *mockQuestionerImpl) Ask() (*Answer, error) {
|
||||
return m.ReturnAsk()
|
||||
}
|
||||
143
cmd/git-chglog/template_builder.go
Normal file
143
cmd/git-chglog/template_builder.go
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
// TemplateBuilder ...
|
||||
type TemplateBuilder interface {
|
||||
Builder
|
||||
}
|
||||
|
||||
type templateBuilderImpl struct{}
|
||||
|
||||
// NewTemplateBuilder ...
|
||||
func NewTemplateBuilder() TemplateBuilder {
|
||||
return &templateBuilderImpl{}
|
||||
}
|
||||
|
||||
// Build ...
|
||||
func (t *templateBuilderImpl) Build(ans *Answer) (string, error) {
|
||||
// versions
|
||||
tpl := "{{range .Versions}}\n"
|
||||
|
||||
// version header
|
||||
tpl += t.versionHeader(ans.Style, ans.Template)
|
||||
|
||||
// commits
|
||||
tpl += t.commits(ans.Template, ans.CommitMessageFormat)
|
||||
|
||||
// revert
|
||||
if ans.IncludeReverts {
|
||||
tpl += t.reverts()
|
||||
}
|
||||
|
||||
// merges
|
||||
if ans.IncludeMerges {
|
||||
tpl += t.merges(ans.Style)
|
||||
}
|
||||
|
||||
// notes
|
||||
tpl += t.notes()
|
||||
|
||||
// versions end
|
||||
tpl += "\n{{end}}"
|
||||
|
||||
return tpl, nil
|
||||
}
|
||||
|
||||
func (*templateBuilderImpl) versionHeader(style, template string) string {
|
||||
var (
|
||||
tpl string
|
||||
tagName string
|
||||
date = "{{datetime \"2006-01-02\" .Tag.Date}}"
|
||||
)
|
||||
|
||||
// parts
|
||||
switch style {
|
||||
case styleGitHub:
|
||||
tpl = "<a name=\"{{.Tag.Name}}\"></a>\n"
|
||||
tagName = "{{if .Tag.Previous}}[{{.Tag.Name}}]({{$.Info.RepositoryURL}}/compare/{{.Tag.Previous.Name}}...{{.Tag.Name}}){{else}}{{.Tag.Name}}{{end}}"
|
||||
default:
|
||||
tagName = "{{.Tag.Name}}"
|
||||
}
|
||||
|
||||
// format
|
||||
switch template {
|
||||
case tplStandard:
|
||||
tpl = fmt.Sprintf("%s## %s (%s)\n",
|
||||
tpl,
|
||||
tagName,
|
||||
date,
|
||||
)
|
||||
case tplCool:
|
||||
tpl = fmt.Sprintf("%s## %s\n\n> %s\n",
|
||||
tpl,
|
||||
tagName,
|
||||
date,
|
||||
)
|
||||
}
|
||||
|
||||
return tpl
|
||||
}
|
||||
|
||||
func (*templateBuilderImpl) commits(template, format string) string {
|
||||
var (
|
||||
header string
|
||||
body string
|
||||
)
|
||||
|
||||
switch format {
|
||||
case fmtSubject.Display:
|
||||
body = `{{range .Commits}}
|
||||
* {{.Header}}{{end}}
|
||||
`
|
||||
|
||||
default:
|
||||
if format == fmtTypeScopeSubject.Display {
|
||||
header = "{{if ne .Scope \"\"}}**{{.Scope}}:** {{end}}{{.Subject}}"
|
||||
} else {
|
||||
header = "{{.Subject}}"
|
||||
}
|
||||
|
||||
body = fmt.Sprintf(`### {{.Title}}
|
||||
{{range .Commits}}
|
||||
* %s{{end}}
|
||||
`, header)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`{{range .CommitGroups}}
|
||||
%s{{end}}`, body)
|
||||
}
|
||||
|
||||
func (*templateBuilderImpl) reverts() string {
|
||||
return `{{if .RevertCommits}}
|
||||
### Reverts
|
||||
{{range .RevertCommits}}
|
||||
* {{.Revert.Header}}{{end}}
|
||||
{{end}}`
|
||||
}
|
||||
|
||||
func (t *templateBuilderImpl) merges(style string) string {
|
||||
var title string
|
||||
|
||||
switch style {
|
||||
case styleGitHub:
|
||||
title = "Pull Requests"
|
||||
default:
|
||||
title = "Merges"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`{{if .MergeCommits}}
|
||||
### %s
|
||||
{{range .MergeCommits}}
|
||||
* {{.Header}}{{end}}
|
||||
{{end}}`, title)
|
||||
}
|
||||
|
||||
func (*templateBuilderImpl) notes() string {
|
||||
return `{{range .NoteGroups}}
|
||||
### {{.Title}}
|
||||
{{range .Notes}}
|
||||
{{.Body}}
|
||||
{{end}}
|
||||
{{end}}`
|
||||
}
|
||||
9
cmd/git-chglog/template_builder_mock.go
Normal file
9
cmd/git-chglog/template_builder_mock.go
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package main
|
||||
|
||||
type mockTemplateBuilderImpl struct {
|
||||
ReturnBuild func(*Answer) (string, error)
|
||||
}
|
||||
|
||||
func (m *mockTemplateBuilderImpl) Build(ans *Answer) (string, error) {
|
||||
return m.ReturnBuild(ans)
|
||||
}
|
||||
253
cmd/git-chglog/template_builder_test.go
Normal file
253
cmd/git-chglog/template_builder_test.go
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTemplateBuilderDefault(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
builder := NewTemplateBuilder()
|
||||
|
||||
out, err := builder.Build(&Answer{
|
||||
Style: styleGitHub,
|
||||
CommitMessageFormat: fmtTypeScopeSubject.Display,
|
||||
Template: tplStandard,
|
||||
IncludeMerges: true,
|
||||
IncludeReverts: true,
|
||||
})
|
||||
|
||||
assert.Nil(err)
|
||||
assert.Equal(`{{range .Versions}}
|
||||
<a name="{{.Tag.Name}}"></a>
|
||||
## {{if .Tag.Previous}}[{{.Tag.Name}}]({{$.Info.RepositoryURL}}/compare/{{.Tag.Previous.Name}}...{{.Tag.Name}}){{else}}{{.Tag.Name}}{{end}} ({{datetime "2006-01-02" .Tag.Date}})
|
||||
{{range .CommitGroups}}
|
||||
### {{.Title}}
|
||||
{{range .Commits}}
|
||||
* {{if ne .Scope ""}}**{{.Scope}}:** {{end}}{{.Subject}}{{end}}
|
||||
{{end}}{{if .RevertCommits}}
|
||||
### Reverts
|
||||
{{range .RevertCommits}}
|
||||
* {{.Revert.Header}}{{end}}
|
||||
{{end}}{{if .MergeCommits}}
|
||||
### Pull Requests
|
||||
{{range .MergeCommits}}
|
||||
* {{.Header}}{{end}}
|
||||
{{end}}{{range .NoteGroups}}
|
||||
### {{.Title}}
|
||||
{{range .Notes}}
|
||||
{{.Body}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}`, out)
|
||||
}
|
||||
|
||||
func TestTemplateBuilderNone(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
builder := NewTemplateBuilder()
|
||||
|
||||
out, err := builder.Build(&Answer{
|
||||
Style: styleNone,
|
||||
CommitMessageFormat: fmtTypeScopeSubject.Display,
|
||||
Template: tplStandard,
|
||||
IncludeMerges: true,
|
||||
IncludeReverts: true,
|
||||
})
|
||||
|
||||
assert.Nil(err)
|
||||
assert.Equal(`{{range .Versions}}
|
||||
## {{.Tag.Name}} ({{datetime "2006-01-02" .Tag.Date}})
|
||||
{{range .CommitGroups}}
|
||||
### {{.Title}}
|
||||
{{range .Commits}}
|
||||
* {{if ne .Scope ""}}**{{.Scope}}:** {{end}}{{.Subject}}{{end}}
|
||||
{{end}}{{if .RevertCommits}}
|
||||
### Reverts
|
||||
{{range .RevertCommits}}
|
||||
* {{.Revert.Header}}{{end}}
|
||||
{{end}}{{if .MergeCommits}}
|
||||
### Merges
|
||||
{{range .MergeCommits}}
|
||||
* {{.Header}}{{end}}
|
||||
{{end}}{{range .NoteGroups}}
|
||||
### {{.Title}}
|
||||
{{range .Notes}}
|
||||
{{.Body}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}`, out)
|
||||
}
|
||||
|
||||
func TestTemplateBuilderCool(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
builder := NewTemplateBuilder()
|
||||
|
||||
out, err := builder.Build(&Answer{
|
||||
Style: styleNone,
|
||||
CommitMessageFormat: fmtTypeScopeSubject.Display,
|
||||
Template: tplCool,
|
||||
IncludeMerges: true,
|
||||
IncludeReverts: true,
|
||||
})
|
||||
|
||||
assert.Nil(err)
|
||||
assert.Equal(`{{range .Versions}}
|
||||
## {{.Tag.Name}}
|
||||
|
||||
> {{datetime "2006-01-02" .Tag.Date}}
|
||||
{{range .CommitGroups}}
|
||||
### {{.Title}}
|
||||
{{range .Commits}}
|
||||
* {{if ne .Scope ""}}**{{.Scope}}:** {{end}}{{.Subject}}{{end}}
|
||||
{{end}}{{if .RevertCommits}}
|
||||
### Reverts
|
||||
{{range .RevertCommits}}
|
||||
* {{.Revert.Header}}{{end}}
|
||||
{{end}}{{if .MergeCommits}}
|
||||
### Merges
|
||||
{{range .MergeCommits}}
|
||||
* {{.Header}}{{end}}
|
||||
{{end}}{{range .NoteGroups}}
|
||||
### {{.Title}}
|
||||
{{range .Notes}}
|
||||
{{.Body}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}`, out)
|
||||
}
|
||||
|
||||
func TestTemplateBuilderSubjectOnly(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
builder := NewTemplateBuilder()
|
||||
|
||||
out, err := builder.Build(&Answer{
|
||||
Style: styleNone,
|
||||
CommitMessageFormat: fmtSubject.Display,
|
||||
Template: tplStandard,
|
||||
IncludeMerges: true,
|
||||
IncludeReverts: true,
|
||||
})
|
||||
|
||||
assert.Nil(err)
|
||||
assert.Equal(`{{range .Versions}}
|
||||
## {{.Tag.Name}} ({{datetime "2006-01-02" .Tag.Date}})
|
||||
{{range .CommitGroups}}
|
||||
{{range .Commits}}
|
||||
* {{.Header}}{{end}}
|
||||
{{end}}{{if .RevertCommits}}
|
||||
### Reverts
|
||||
{{range .RevertCommits}}
|
||||
* {{.Revert.Header}}{{end}}
|
||||
{{end}}{{if .MergeCommits}}
|
||||
### Merges
|
||||
{{range .MergeCommits}}
|
||||
* {{.Header}}{{end}}
|
||||
{{end}}{{range .NoteGroups}}
|
||||
### {{.Title}}
|
||||
{{range .Notes}}
|
||||
{{.Body}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}`, out)
|
||||
}
|
||||
|
||||
func TestTemplateBuilderSubject(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
builder := NewTemplateBuilder()
|
||||
|
||||
out, err := builder.Build(&Answer{
|
||||
Style: styleNone,
|
||||
CommitMessageFormat: fmtTypeSubject.Display,
|
||||
Template: tplStandard,
|
||||
IncludeMerges: true,
|
||||
IncludeReverts: true,
|
||||
})
|
||||
|
||||
assert.Nil(err)
|
||||
assert.Equal(`{{range .Versions}}
|
||||
## {{.Tag.Name}} ({{datetime "2006-01-02" .Tag.Date}})
|
||||
{{range .CommitGroups}}
|
||||
### {{.Title}}
|
||||
{{range .Commits}}
|
||||
* {{.Subject}}{{end}}
|
||||
{{end}}{{if .RevertCommits}}
|
||||
### Reverts
|
||||
{{range .RevertCommits}}
|
||||
* {{.Revert.Header}}{{end}}
|
||||
{{end}}{{if .MergeCommits}}
|
||||
### Merges
|
||||
{{range .MergeCommits}}
|
||||
* {{.Header}}{{end}}
|
||||
{{end}}{{range .NoteGroups}}
|
||||
### {{.Title}}
|
||||
{{range .Notes}}
|
||||
{{.Body}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}`, out)
|
||||
}
|
||||
|
||||
func TestTemplateBuilderIgnoreReverts(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
builder := NewTemplateBuilder()
|
||||
|
||||
out, err := builder.Build(&Answer{
|
||||
Style: styleNone,
|
||||
CommitMessageFormat: fmtTypeSubject.Display,
|
||||
Template: tplStandard,
|
||||
IncludeMerges: true,
|
||||
IncludeReverts: false,
|
||||
})
|
||||
|
||||
assert.Nil(err)
|
||||
assert.Equal(`{{range .Versions}}
|
||||
## {{.Tag.Name}} ({{datetime "2006-01-02" .Tag.Date}})
|
||||
{{range .CommitGroups}}
|
||||
### {{.Title}}
|
||||
{{range .Commits}}
|
||||
* {{.Subject}}{{end}}
|
||||
{{end}}{{if .MergeCommits}}
|
||||
### Merges
|
||||
{{range .MergeCommits}}
|
||||
* {{.Header}}{{end}}
|
||||
{{end}}{{range .NoteGroups}}
|
||||
### {{.Title}}
|
||||
{{range .Notes}}
|
||||
{{.Body}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}`, out)
|
||||
}
|
||||
|
||||
func TestTemplateBuilderIgnoreMerges(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
builder := NewTemplateBuilder()
|
||||
|
||||
out, err := builder.Build(&Answer{
|
||||
Style: styleNone,
|
||||
CommitMessageFormat: fmtTypeSubject.Display,
|
||||
Template: tplStandard,
|
||||
IncludeMerges: false,
|
||||
IncludeReverts: true,
|
||||
})
|
||||
|
||||
assert.Nil(err)
|
||||
assert.Equal(`{{range .Versions}}
|
||||
## {{.Tag.Name}} ({{datetime "2006-01-02" .Tag.Date}})
|
||||
{{range .CommitGroups}}
|
||||
### {{.Title}}
|
||||
{{range .Commits}}
|
||||
* {{.Subject}}{{end}}
|
||||
{{end}}{{if .RevertCommits}}
|
||||
### Reverts
|
||||
{{range .RevertCommits}}
|
||||
* {{.Revert.Header}}{{end}}
|
||||
{{end}}{{range .NoteGroups}}
|
||||
### {{.Title}}
|
||||
{{range .Notes}}
|
||||
{{.Body}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}`, out)
|
||||
}
|
||||
95
cmd/git-chglog/variables.go
Normal file
95
cmd/git-chglog/variables.go
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Defaults
|
||||
var (
|
||||
defaultConfigDir = ".chglog"
|
||||
defaultConfigFilename = "config.yml"
|
||||
defaultTemplateFilename = "CHANGELOG.tpl.md"
|
||||
)
|
||||
|
||||
// Styles
|
||||
var (
|
||||
styleGitHub = "github"
|
||||
styleNone = "none"
|
||||
styles = []string{
|
||||
styleGitHub,
|
||||
styleNone,
|
||||
}
|
||||
)
|
||||
|
||||
// CommitMessageFormat ...
|
||||
type CommitMessageFormat struct {
|
||||
Preview string
|
||||
Display string
|
||||
Pattern string
|
||||
PatternMaps []string
|
||||
}
|
||||
|
||||
// PatternMapString ...
|
||||
func (f *CommitMessageFormat) PatternMapString() string {
|
||||
s := " []"
|
||||
l := len(f.PatternMaps)
|
||||
if l == 0 {
|
||||
return s
|
||||
}
|
||||
|
||||
arr := make([]string, l)
|
||||
for i, p := range f.PatternMaps {
|
||||
arr[i] = fmt.Sprintf(
|
||||
"%s- %s",
|
||||
strings.Repeat(" ", 6),
|
||||
p,
|
||||
)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("\n%s", strings.Join(arr, "\n"))
|
||||
}
|
||||
|
||||
// Formats
|
||||
var (
|
||||
fmtTypeScopeSubject = &CommitMessageFormat{
|
||||
Preview: "feat(core) Add new feature",
|
||||
Display: "<type>(<scope>): <subject>",
|
||||
Pattern: `^(\\w*)(?:\\(([\\w\\$\\.\\-\\*\\s]*)\\))?\\:\\s(.*)$`,
|
||||
PatternMaps: []string{"Type", "Scope", "Subject"},
|
||||
}
|
||||
fmtTypeSubject = &CommitMessageFormat{
|
||||
Preview: "feat: Add new feature",
|
||||
Display: "<type>: <subject>",
|
||||
Pattern: `^(\\w*)\\:\\s(.*)$`,
|
||||
PatternMaps: []string{"Type", "Subject"},
|
||||
}
|
||||
fmtGitBasic = &CommitMessageFormat{
|
||||
Preview: "Add new feature",
|
||||
Display: "<<type> subject>",
|
||||
Pattern: `^((\\w+)\\s.*)$`,
|
||||
PatternMaps: []string{"Subject", "Type"},
|
||||
}
|
||||
fmtSubject = &CommitMessageFormat{
|
||||
Preview: "Add new feature (Not detect `type` field)",
|
||||
Display: "<subject>",
|
||||
Pattern: `^(.*)$`,
|
||||
PatternMaps: []string{"Subject"},
|
||||
}
|
||||
formats = []*CommitMessageFormat{
|
||||
fmtTypeScopeSubject,
|
||||
fmtTypeSubject,
|
||||
fmtGitBasic,
|
||||
fmtSubject,
|
||||
}
|
||||
)
|
||||
|
||||
// Templates
|
||||
var (
|
||||
tplStandard = "standard"
|
||||
tplCool = "cool"
|
||||
templates = []string{
|
||||
tplStandard,
|
||||
tplCool,
|
||||
}
|
||||
)
|
||||
30
cmd/git-chglog/variables_test.go
Normal file
30
cmd/git-chglog/variables_test.go
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCommitMessageFormatPatternMaps(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
f := &CommitMessageFormat{
|
||||
PatternMaps: []string{
|
||||
"Type",
|
||||
"Scope",
|
||||
"Subject",
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(`
|
||||
- Type
|
||||
- Scope
|
||||
- Subject`, f.PatternMapString())
|
||||
|
||||
f = &CommitMessageFormat{
|
||||
PatternMaps: []string{},
|
||||
}
|
||||
|
||||
assert.Equal(" []", f.PatternMapString())
|
||||
}
|
||||
|
|
@ -80,13 +80,15 @@ type CommitGroup struct {
|
|||
// If you give `Tag`, the reference hierarchy will be deepened.
|
||||
// This struct is used to minimize the hierarchy of references
|
||||
type RelateTag struct {
|
||||
Name string
|
||||
Date time.Time
|
||||
Name string
|
||||
Subject string
|
||||
Date time.Time
|
||||
}
|
||||
|
||||
// Tag is data of git-tag
|
||||
type Tag struct {
|
||||
Name string
|
||||
Subject string
|
||||
Date time.Time
|
||||
Next *RelateTag
|
||||
Previous *RelateTag
|
||||
|
|
|
|||
|
|
@ -42,9 +42,15 @@ func (p *GitHubProcessor) ProcessCommit(commit *Commit) *Commit {
|
|||
commit.Header = p.addLinks(commit.Header)
|
||||
commit.Subject = p.addLinks(commit.Subject)
|
||||
commit.Body = p.addLinks(commit.Body)
|
||||
|
||||
for _, note := range commit.Notes {
|
||||
note.Body = p.addLinks(note.Body)
|
||||
}
|
||||
|
||||
if commit.Revert != nil {
|
||||
commit.Revert.Header = p.addLinks(commit.Revert.Header)
|
||||
}
|
||||
|
||||
return commit
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -51,4 +51,19 @@ gh-56 hoge fuga`,
|
|||
},
|
||||
),
|
||||
)
|
||||
|
||||
assert.Equal(
|
||||
&Commit{
|
||||
Revert: &Revert{
|
||||
Header: "revert header [@mention](https://github.com/mention) [#123](https://example.com/issues/123)",
|
||||
},
|
||||
},
|
||||
processor.ProcessCommit(
|
||||
&Commit{
|
||||
Revert: &Revert{
|
||||
Header: "revert header @mention #123",
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@ package chglog
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -11,24 +10,24 @@ import (
|
|||
)
|
||||
|
||||
type tagReader struct {
|
||||
client gitcmd.Client
|
||||
format string
|
||||
reTag *regexp.Regexp
|
||||
client gitcmd.Client
|
||||
format string
|
||||
separator string
|
||||
}
|
||||
|
||||
func newTagReader(client gitcmd.Client) *tagReader {
|
||||
return &tagReader{
|
||||
client: client,
|
||||
reTag: regexp.MustCompile("tag: ([\\w\\.\\-_]+),?"),
|
||||
client: client,
|
||||
separator: "@@__CHGLOG__@@",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *tagReader) ReadAll() ([]*Tag, error) {
|
||||
out, err := r.client.Exec(
|
||||
"log",
|
||||
"--tags",
|
||||
"--simplify-by-decoration",
|
||||
"--pretty=%D\t%at",
|
||||
"for-each-ref",
|
||||
"--format",
|
||||
"%(refname)"+r.separator+"%(subject)"+r.separator+"%(taggerdate)"+r.separator+"%(authordate)",
|
||||
"refs/tags",
|
||||
)
|
||||
|
||||
tags := []*Tag{}
|
||||
|
|
@ -40,28 +39,49 @@ func (r *tagReader) ReadAll() ([]*Tag, error) {
|
|||
lines := strings.Split(out, "\n")
|
||||
|
||||
for _, line := range lines {
|
||||
if line == "" {
|
||||
tokens := strings.Split(line, r.separator)
|
||||
|
||||
if len(tokens) != 4 {
|
||||
continue
|
||||
}
|
||||
|
||||
tokens := strings.Split(line, "\t")
|
||||
|
||||
res := r.reTag.FindAllStringSubmatch(tokens[0], -1)
|
||||
if len(res) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
ts, err := strconv.Atoi(tokens[1])
|
||||
name := r.parseRefname(tokens[0])
|
||||
subject := r.parseSubject(tokens[1])
|
||||
date, err := r.parseDate(tokens[2])
|
||||
if err != nil {
|
||||
continue
|
||||
t, err2 := r.parseDate(tokens[3])
|
||||
if err2 != nil {
|
||||
return nil, err2
|
||||
}
|
||||
date = t
|
||||
}
|
||||
|
||||
tags = append(tags, &Tag{
|
||||
Name: res[0][1],
|
||||
Date: time.Unix(int64(ts), 0),
|
||||
Name: name,
|
||||
Subject: subject,
|
||||
Date: date,
|
||||
})
|
||||
}
|
||||
|
||||
r.sortTags(tags)
|
||||
r.assignPreviousAndNextTag(tags)
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func (*tagReader) parseRefname(input string) string {
|
||||
return strings.Replace(input, "refs/tags/", "", 1)
|
||||
}
|
||||
|
||||
func (*tagReader) parseSubject(input string) string {
|
||||
return strings.TrimSpace(input)
|
||||
}
|
||||
|
||||
func (*tagReader) parseDate(input string) (time.Time, error) {
|
||||
return time.Parse("Mon Jan 2 15:04:05 2006 -0700", input)
|
||||
}
|
||||
|
||||
func (*tagReader) assignPreviousAndNextTag(tags []*Tag) {
|
||||
total := len(tags)
|
||||
|
||||
for i, tag := range tags {
|
||||
|
|
@ -72,21 +92,27 @@ func (r *tagReader) ReadAll() ([]*Tag, error) {
|
|||
|
||||
if i > 0 {
|
||||
next = &RelateTag{
|
||||
Name: tags[i-1].Name,
|
||||
Date: tags[i-1].Date,
|
||||
Name: tags[i-1].Name,
|
||||
Subject: tags[i-1].Subject,
|
||||
Date: tags[i-1].Date,
|
||||
}
|
||||
}
|
||||
|
||||
if i+1 < total {
|
||||
prev = &RelateTag{
|
||||
Name: tags[i+1].Name,
|
||||
Date: tags[i+1].Date,
|
||||
Name: tags[i+1].Name,
|
||||
Subject: tags[i+1].Subject,
|
||||
Date: tags[i+1].Date,
|
||||
}
|
||||
}
|
||||
|
||||
tag.Next = next
|
||||
tag.Previous = prev
|
||||
}
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func (*tagReader) sortTags(tags []*Tag) {
|
||||
sort.Slice(tags, func(i, j int) bool {
|
||||
return !tags[i].Date.Before(tags[j].Date)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,19 +13,17 @@ func TestTagReader(t *testing.T) {
|
|||
assert := assert.New(t)
|
||||
client := &mockClient{
|
||||
ReturnExec: func(subcmd string, args ...string) (string, error) {
|
||||
if subcmd != "log" {
|
||||
if subcmd != "for-each-ref" {
|
||||
return "", errors.New("")
|
||||
}
|
||||
return strings.Join([]string{
|
||||
"",
|
||||
"tag: v5.2.0-beta.1, origin/labs/router\t1518023112",
|
||||
"tag: 2.0.0\t1517875200",
|
||||
"tag: v2.0.4-rc.1\t1517788800",
|
||||
"tag: 2.0.4-beta.1\t1517702400",
|
||||
"tag: hoge_fuga\t1517616000",
|
||||
"tag: 1.9.29-alpha.0\t1517529600",
|
||||
"hoge\t0",
|
||||
"foo\t0",
|
||||
"refs/tags/v2.0.4-beta.1@@__CHGLOG__@@Release v2.0.4-beta.1@@__CHGLOG__@@Thu Feb 1 00:00:00 2018 +0000@@__CHGLOG__@@",
|
||||
"refs/tags/4.4.3@@__CHGLOG__@@This is tag subject@@__CHGLOG__@@@@__CHGLOG__@@Fri Feb 2 00:00:00 2018 +0000",
|
||||
"refs/tags/4.4.4@@__CHGLOG__@@Release 4.4.4@@__CHGLOG__@@Fri Feb 2 10:00:40 2018 +0000@@__CHGLOG__@@",
|
||||
"refs/tags/5.0.0-rc.0@@__CHGLOG__@@Release 5.0.0-rc.0@@__CHGLOG__@@Sat Feb 3 12:30:10 2018 +0000@@__CHGLOG__@@",
|
||||
"refs/tags/hoge_fuga@@__CHGLOG__@@Invalid semver tag name@@__CHGLOG__@@Mon Mar 12 12:30:10 2018 +0000@@__CHGLOG__@@",
|
||||
"hoge@@__CHGLOG__@@",
|
||||
}, "\n"), nil
|
||||
},
|
||||
}
|
||||
|
|
@ -36,68 +34,69 @@ func TestTagReader(t *testing.T) {
|
|||
assert.Equal(
|
||||
[]*Tag{
|
||||
&Tag{
|
||||
Name: "v5.2.0-beta.1",
|
||||
Date: time.Unix(1518023112, 0),
|
||||
Next: nil,
|
||||
Name: "hoge_fuga",
|
||||
Subject: "Invalid semver tag name",
|
||||
Date: time.Date(2018, 3, 12, 12, 30, 10, 0, time.UTC),
|
||||
Next: nil,
|
||||
Previous: &RelateTag{
|
||||
Name: "2.0.0",
|
||||
Date: time.Unix(1517875200, 0),
|
||||
Name: "5.0.0-rc.0",
|
||||
Subject: "Release 5.0.0-rc.0",
|
||||
Date: time.Date(2018, 2, 3, 12, 30, 10, 0, time.UTC),
|
||||
},
|
||||
},
|
||||
&Tag{
|
||||
Name: "2.0.0",
|
||||
Date: time.Unix(1517875200, 0),
|
||||
Name: "5.0.0-rc.0",
|
||||
Subject: "Release 5.0.0-rc.0",
|
||||
Date: time.Date(2018, 2, 3, 12, 30, 10, 0, time.UTC),
|
||||
Next: &RelateTag{
|
||||
Name: "v5.2.0-beta.1",
|
||||
Date: time.Unix(1518023112, 0),
|
||||
Name: "hoge_fuga",
|
||||
Subject: "Invalid semver tag name",
|
||||
Date: time.Date(2018, 3, 12, 12, 30, 10, 0, time.UTC),
|
||||
},
|
||||
Previous: &RelateTag{
|
||||
Name: "v2.0.4-rc.1",
|
||||
Date: time.Unix(1517788800, 0),
|
||||
Name: "4.4.4",
|
||||
Subject: "Release 4.4.4",
|
||||
Date: time.Date(2018, 2, 2, 10, 0, 40, 0, time.UTC),
|
||||
},
|
||||
},
|
||||
&Tag{
|
||||
Name: "v2.0.4-rc.1",
|
||||
Date: time.Unix(1517788800, 0),
|
||||
Name: "4.4.4",
|
||||
Subject: "Release 4.4.4",
|
||||
Date: time.Date(2018, 2, 2, 10, 0, 40, 0, time.UTC),
|
||||
Next: &RelateTag{
|
||||
Name: "2.0.0",
|
||||
Date: time.Unix(1517875200, 0),
|
||||
Name: "5.0.0-rc.0",
|
||||
Subject: "Release 5.0.0-rc.0",
|
||||
Date: time.Date(2018, 2, 3, 12, 30, 10, 0, time.UTC),
|
||||
},
|
||||
Previous: &RelateTag{
|
||||
Name: "2.0.4-beta.1",
|
||||
Date: time.Unix(1517702400, 0),
|
||||
Name: "4.4.3",
|
||||
Subject: "This is tag subject",
|
||||
Date: time.Date(2018, 2, 2, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
},
|
||||
&Tag{
|
||||
Name: "2.0.4-beta.1",
|
||||
Date: time.Unix(1517702400, 0),
|
||||
Name: "4.4.3",
|
||||
Subject: "This is tag subject",
|
||||
Date: time.Date(2018, 2, 2, 0, 0, 0, 0, time.UTC),
|
||||
Next: &RelateTag{
|
||||
Name: "v2.0.4-rc.1",
|
||||
Date: time.Unix(1517788800, 0),
|
||||
Name: "4.4.4",
|
||||
Subject: "Release 4.4.4",
|
||||
Date: time.Date(2018, 2, 2, 10, 0, 40, 0, time.UTC),
|
||||
},
|
||||
Previous: &RelateTag{
|
||||
Name: "hoge_fuga",
|
||||
Date: time.Unix(1517616000, 0),
|
||||
Name: "v2.0.4-beta.1",
|
||||
Subject: "Release v2.0.4-beta.1",
|
||||
Date: time.Date(2018, 2, 1, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
},
|
||||
&Tag{
|
||||
Name: "hoge_fuga",
|
||||
Date: time.Unix(1517616000, 0),
|
||||
Name: "v2.0.4-beta.1",
|
||||
Subject: "Release v2.0.4-beta.1",
|
||||
Date: time.Date(2018, 2, 1, 0, 0, 0, 0, time.UTC),
|
||||
Next: &RelateTag{
|
||||
Name: "2.0.4-beta.1",
|
||||
Date: time.Unix(1517702400, 0),
|
||||
},
|
||||
Previous: &RelateTag{
|
||||
Name: "1.9.29-alpha.0",
|
||||
Date: time.Unix(1517529600, 0),
|
||||
},
|
||||
},
|
||||
&Tag{
|
||||
Name: "1.9.29-alpha.0",
|
||||
Date: time.Unix(1517529600, 0),
|
||||
Next: &RelateTag{
|
||||
Name: "hoge_fuga",
|
||||
Date: time.Unix(1517616000, 0),
|
||||
Name: "4.4.3",
|
||||
Subject: "This is tag subject",
|
||||
Date: time.Date(2018, 2, 2, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
Previous: nil,
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue