diff options
author | Simon Hausmann <simon.hausmann@theqtcompany.com> | 2016-02-25 16:54:02 +0100 |
---|---|---|
committer | Simon Hausmann <simon.hausmann@theqtcompany.com> | 2016-02-26 11:56:05 +0000 |
commit | 66f33fecc3b4a2896a4f33a3a7f06fb5cdd36dc8 (patch) | |
tree | 60457667db3619f6f278e0c52ad51a7cb8d6ad69 /src | |
parent | 9be50ab01acf71f98d183a93667953303633c1cc (diff) | |
download | qtqa-66f33fecc3b4a2896a4f33a3a7f06fb5cdd36dc8.tar.gz |
Added little helper tool to create an initial changelog template for a module
Simply run the binary in the module directory with the correct branch checked
out. The tool will peek at .qmake.conf to figure out the current version and
run git tag -l to see what the previous version is.
The resulting change log requires manual editing, but it is a start.
Change-Id: I975c0d7a74fee8cab2ae6f400972c5dbc73ff367
Reviewed-by: Robin Burchell <robin.burchell@viroteck.net>
Diffstat (limited to 'src')
-rw-r--r-- | src/createchangelog/main.go | 468 | ||||
-rw-r--r-- | src/createchangelog/main_test.go | 44 |
2 files changed, 512 insertions, 0 deletions
diff --git a/src/createchangelog/main.go b/src/createchangelog/main.go new file mode 100644 index 0000000..ea523c5 --- /dev/null +++ b/src/createchangelog/main.go @@ -0,0 +1,468 @@ +package main + +import ( + "bufio" + "bytes" + "fmt" + "go/doc" + "html/template" + "os" + "os/exec" + "path/filepath" + "regexp" + "sort" + "strings" + + "github.com/hashicorp/go-version" +) + +type commandWithCapturedOutput struct { + cmd *exec.Cmd + stdout bytes.Buffer + stderr bytes.Buffer +} + +func newCommandWithCapturedOutput(cmd *exec.Cmd) *commandWithCapturedOutput { + result := &commandWithCapturedOutput{} + result.cmd = cmd + result.cmd.Stdout = &result.stdout + result.cmd.Stderr = &result.stderr + return result +} + +func (c *commandWithCapturedOutput) run() (string, error) { + err := c.cmd.Run() + if err != nil { + return "", fmt.Errorf("Error running %s: %s\nStdout: %s\nStderr: %s", strings.Join(c.cmd.Args, " "), err, c.stdout.String(), c.stderr.String()) + } + return c.stdout.String(), err +} + +func (c *commandWithCapturedOutput) runWithSpaceTrimmed() (string, error) { + output, err := c.run() + return strings.TrimSpace(output), err +} + +func (c *commandWithCapturedOutput) runWithLines() ([]string, error) { + output, err := c.run() + if err != nil { + return nil, err + } + var lines []string + + scanner := bufio.NewScanner(bytes.NewBufferString(output)) + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("Error parsing line output: %s", err) + } + + return lines, err +} + +type repository string + +func (repo repository) gitCommand(command string, parameters ...string) *commandWithCapturedOutput { + parameters = append([]string{"--git-dir=" + string(repo), command}, parameters...) + return newCommandWithCapturedOutput(exec.Command("git", parameters...)) +} + +func (repo repository) tags() ([]string, error) { + return repo.gitCommand("tag", "-l").runWithLines() +} + +func (repo repository) revList(revisionRange string) ([]string, error) { + return repo.gitCommand("rev-list", revisionRange).runWithLines() +} + +func (repo repository) catFile(objectType string, sha1 string) (string, error) { + return repo.gitCommand("cat-file", objectType, sha1).run() +} + +func detectCurrentQtVersionFromQMakeConf() (*version.Version, error) { + qmakeConfFile, err := os.Open(".qmake.conf") + if err != nil { + defer qmakeConfFile.Close() + return nil, fmt.Errorf("Error opening .qmake.conf - are you in the right directory?") + } + + moduleVersionRegExp := regexp.MustCompile(`\s*MODULE_VERSION\s*=\s*(` + version.VersionRegexpRaw + `).*`) + + scanner := bufio.NewScanner(qmakeConfFile) + for scanner.Scan() { + line := scanner.Text() + match := moduleVersionRegExp.FindStringSubmatch(line) + if len(match) < 2 { + continue + } + return version.NewVersion(match[1]) + } + + return nil, fmt.Errorf("Could not find MODULE_VERSION line in .qmake.conf to detect the current module version") +} + +func determinePastQtVersions(repo repository) ([]*version.Version, error) { + tags, err := repo.tags() + if err != nil { + return nil, fmt.Errorf("Error determining tags: %s", err) + } + + var parsedVersions []*version.Version + + for _, tag := range tags { + v, err := version.NewVersion(tag) + if err != nil { + continue + } + parsedVersions = append(parsedVersions, v) + } + + return parsedVersions, nil +} + +func filterOutPreReleases(versions []*version.Version) []*version.Version { + var withoutPreRelease []*version.Version + for _, v := range versions { + if v.Prerelease() != "" { + continue + } + withoutPreRelease = append(withoutPreRelease, v) + } + return withoutPreRelease +} + +type sortedVersions []*version.Version + +func (slice sortedVersions) Len() int { + return len(slice) +} + +func (slice sortedVersions) Less(i, j int) bool { + return slice[i].LessThan(slice[j]) +} + +func (slice sortedVersions) Swap(i, j int) { + slice[i], slice[j] = slice[j], slice[i] +} + +type changeLogEntry struct { + module string + class string + text string +} + +func extractChangeLog(commitMessage string) (entry changeLogEntry) { + scanner := bufio.NewScanner(bytes.NewBufferString(commitMessage)) + + for scanner.Scan() { + trimmedLine := strings.TrimSpace(scanner.Text()) + + if entry.text == "" { + withoutChangeLog := strings.TrimPrefix(trimmedLine, "[ChangeLog]") + if withoutChangeLog == trimmedLine { + continue + } + + remainder := withoutChangeLog + for { + withoutBracket := strings.TrimPrefix(remainder, "[") + if withoutBracket == remainder { + break + } + var group string + remainder = strings.TrimLeftFunc(withoutBracket, func(ch rune) bool { + endOfGroup := ch == ']' + if !endOfGroup { + group += string(ch) + } + return !endOfGroup + }) + remainder = strings.TrimPrefix(remainder, "]") + if entry.module == "" { + entry.module = group + } else if entry.class == "" { + entry.class = group + } else { + remainder = "[" + withoutBracket + break + } + } + + entry.text = strings.TrimSpace(remainder) + } else if trimmedLine == "" { + break + } else { + entry.text = entry.text + " " + trimmedLine + } + } + return +} + +func determinePreviousMinorVersion(versions []*version.Version) *version.Version { + lastVersion := len(versions) - 1 + previousMinor := lastVersion + for previousMinor > 0 { + if versions[previousMinor].Segments()[1] != versions[lastVersion].Segments()[1] { + break + } + previousMinor-- + } + return versions[previousMinor] +} + +type templateVersion struct { + Major int + Minor int + Patch int +} + +func newTemplateVersion(v *version.Version) templateVersion { + var result templateVersion + result.Major = v.Segments()[0] + result.Minor = v.Segments()[1] + if len(v.Segments()) == 3 { + result.Patch = v.Segments()[2] + } + return result +} + +const minorVersionHeader = `Qt {{.CurrentVersion.Major}}.{{.CurrentVersion.Minor}} introduces many new features and improvements as well as bugfixes +over the {{.LastVersion.Major}}.{{.LastVersion.Minor}}.x series. For more details, refer to the online documentation +included in this distribution. The documentation is also available online: + + http://qt-project.org/doc/qt-{{.CurrentVersion.Major}} + +The Qt version {{.CurrentVersion.Major}}.{{.CurrentVersion.Minor}} series is binary compatible with the {{.LastVersion.Major}}.{{.LastVersion.Minor}}.x series. +Applications compiled for {{.LastVersion.Major}}.{{.LastVersion.Minor}} will continue to run with {{.CurrentVersion.Major}}.{{.CurrentVersion.Minor}}. + +Some of the changes listed in this file include issue tracking numbers +corresponding to tasks in the Qt Bug Tracker: + + http://bugreports.qt-project.org/ + +Each of these identifiers can be entered in the bug tracker to obtain more +information about a particular change. + +**************************************************************************** +* Important Behavior Changes * +**************************************************************************** + +**************************************************************************** +* Library * +**************************************************************************** + +` + +const patchVersionHeader = `Qt {{.CurrentVersion.Major}}.{{.CurrentVersion.Minor}}.{{.CurrentVersion.Patch}} is a bug-fix release. It maintains both forward and backward +compatibility (source and binary) with Qt {{.LastVersion.Major}}.{{.LastVersion.Minor}}.{{.LastVersion.Patch}}. + +For more details, refer to the online documentation included in this +distribution. The documentation is also available online: + + http://qt-project.org/doc/qt-{{.CurrentVersion.Major}}.{{.CurrentVersion.Minor}} + +The Qt version {{.CurrentVersion.Major}}.{{.CurrentVersion.Minor}} series is binary compatible with the {{.PreviousMinorVersion.Major}}.{{.PreviousMinorVersion.Minor}}.x series. +Applications compiled for {{.PreviousMinorVersion.Major}}.{{.PreviousMinorVersion.Minor}} will continue to run with {{.CurrentVersion.Major}}.{{.CurrentVersion.Minor}}. + +Some of the changes listed in this file include issue tracking numbers +corresponding to tasks in the Qt Bug Tracker: + + http://bugreports.qt-project.org/ + +Each of these identifiers can be entered in the bug tracker to obtain more +information about a particular change. + +**************************************************************************** +* Important Behavior Changes * +**************************************************************************** + +**************************************************************************** +* Library * +**************************************************************************** + +` + +func printParagraph(indent string, bulletCharacter byte, text string) { + var buf bytes.Buffer + doc.ToText(&buf, text, indent, indent, 79-len(indent)) + output := buf.Bytes() + if len(output) > len(indent) && len(indent) > 2 { + bulletPos := len(indent) - 2 + output[bulletPos] = bulletCharacter + } + os.Stdout.Write(output) +} + +type classChange struct { + className string + changes []string +} + +type moduleChanges struct { + moduleName string + classIndependentChanges []string + changesPerClass []*classChange +} + +func (m *moduleChanges) append(entry changeLogEntry) { + if entry.class == "" { + m.classIndependentChanges = append(m.classIndependentChanges, entry.text) + return + } + i := sort.Search(len(m.changesPerClass), func(i int) bool { return m.changesPerClass[i].className >= entry.class }) + if i < len(m.changesPerClass) && m.changesPerClass[i].className == entry.class { + m.changesPerClass[i].changes = append(m.changesPerClass[i].changes, entry.text) + } else { + newClassChange := &classChange{ + className: entry.class, + changes: []string{entry.text}, + } + m.changesPerClass = append(m.changesPerClass[:i], append([]*classChange{newClassChange}, m.changesPerClass[i:]...)...) + } +} + +func (m *moduleChanges) print() { + fmt.Println(m.moduleName) + fmt.Println(strings.Repeat("-", len(m.moduleName))) + + for _, change := range m.classIndependentChanges { + fmt.Println() + printParagraph(" ", '-', change) + } + + for _, classChange := range m.changesPerClass { + fmt.Println() + fmt.Printf(" - %s:\n", classChange.className) + for _, ch := range classChange.changes { + printParagraph(" ", '*', ch) + } + } + + fmt.Println() +} + +type topLevelChanges struct { + globalChanges []string + changesPerModule []*moduleChanges +} + +func (t *topLevelChanges) append(entry changeLogEntry) { + if entry.module == "" { + t.globalChanges = append(t.globalChanges, entry.text) + return + } + i := sort.Search(len(t.changesPerModule), func(i int) bool { return t.changesPerModule[i].moduleName >= entry.module }) + if i < len(t.changesPerModule) && t.changesPerModule[i].moduleName == entry.module { + t.changesPerModule[i].append(entry) + } else { + newModule := &moduleChanges{ + moduleName: entry.module, + } + newModule.append(entry) + t.changesPerModule = append(t.changesPerModule[:i], append([]*moduleChanges{newModule}, t.changesPerModule[i:]...)...) + } +} + +func (t *topLevelChanges) print() { + for _, change := range t.globalChanges { + fmt.Println() + printParagraph(" ", ' ', change) + fmt.Println() + } + + for _, module := range t.changesPerModule { + module.print() + } +} + +func appMain() error { + currentVersion, err := detectCurrentQtVersionFromQMakeConf() + if err != nil { + return err + } + + workingDir, err := os.Getwd() + if err != nil { + return fmt.Errorf("Error determining current working directory: %s", err) + } + + repo := repository(filepath.Join(workingDir, ".git")) + pastVersions, err := determinePastQtVersions(repo) + if err != nil { + return err + } + + pastVersions = filterOutPreReleases(pastVersions) + + sort.Sort(sortedVersions(pastVersions)) + + lastVersion := pastVersions[len(pastVersions)-1] + previousMinorVersion := determinePreviousMinorVersion(pastVersions) + + if !lastVersion.LessThan(currentVersion) { + return fmt.Errorf("Detected current version %s is not newer than last tagged version %s - no changelog to create", currentVersion, lastVersion) + } + + if len(lastVersion.Segments()) < 2 { + return fmt.Errorf("Invalid last version: %s", lastVersion) + } + if len(currentVersion.Segments()) < 2 { + return fmt.Errorf("Invalid current version: %s", currentVersion) + } + + headerVariables := struct { + LastVersion templateVersion + CurrentVersion templateVersion + PreviousMinorVersion templateVersion + }{ + newTemplateVersion(lastVersion), + newTemplateVersion(currentVersion), + newTemplateVersion(previousMinorVersion), + } + + var headerTemplate *template.Template + + // Same minor version -> then it's a patch release + if currentVersion.Segments()[1] == lastVersion.Segments()[1] { + headerTemplate = template.Must(template.New("patchVersionTemplate").Parse(patchVersionHeader)) + } else { + headerTemplate = template.Must(template.New("minorVersionTemplate").Parse(minorVersionHeader)) + } + + if err := headerTemplate.Execute(os.Stdout, &headerVariables); err != nil { + return err + } + + commits, err := repo.revList("v" + lastVersion.String() + "..HEAD") + if err != nil { + return fmt.Errorf("Error determining commits for change log: %s", err) + } + + var changes topLevelChanges + + for _, commitSha1 := range commits { + commit, err := repo.catFile("commit", commitSha1) + if err != nil { + return fmt.Errorf("Error reading commit %s: %s", commitSha1, err) + } + entry := extractChangeLog(commit) + if entry.text == "" { + continue + } + //log.Println(commitSha1, entry.groups, entry.text) + changes.append(entry) + } + + changes.print() + return nil +} + +func main() { + err := appMain() + if err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/src/createchangelog/main_test.go b/src/createchangelog/main_test.go new file mode 100644 index 0000000..1872e2a --- /dev/null +++ b/src/createchangelog/main_test.go @@ -0,0 +1,44 @@ +package main + +import ( + "testing" +) + +func TestChangeLogParser(t *testing.T) { + commitText := ` +Some text here + + [ChangeLog] Multi line text + here + +more text here +` + + entry := extractChangeLog(commitText) + if entry.module != "" || entry.class != "" { + t.Fatal("Unexpected groups parsed") + } + + if entry.text != "Multi line text here" { + t.Fatalf("Unexpected text extracted: %s", entry.text) + } + + commitText = ` +Some text here + + [ChangeLog][Some][Group] Multi line text + here + +more text here +` + + entry = extractChangeLog(commitText) + if entry.module != "Some" && entry.class != "Group" { + t.Fatalf("Groups incorrectly parsed: %s %s", entry.class, entry.module) + } + + if entry.text != "Multi line text here" { + t.Fatalf("Unexpected text extracted: %s", entry.text) + } + +} |