eng/tools/apidiff/cmd/changelog.go (185 lines of code) (raw):

// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. package cmd import ( "errors" "fmt" "sort" "strings" "github.com/Azure/azure-sdk-for-go/eng/tools/internal/markdown" "github.com/Azure/azure-sdk-for-go/eng/tools/internal/report" "github.com/spf13/cobra" ) var changelogCmd = &cobra.Command{ Use: "changelog <package search dir> <base commit> <target commit> <release tag version>", Short: "Generates a CHANGELOG report in markdown format for the packages under the specified directory.", Long: `The changelog command generates a CHANGELOG for all of the packages under the directory specified in <package dir>. A table for added, removed, updated, and breaking changes will be created as required.`, Args: func(cmd *cobra.Command, args []string) error { // there should be exactly four args, a directory, two commit hashes and the release tag version if err := cobra.ExactArgs(4)(cmd, args); err != nil { return err } if strings.Index(args[2], ",") > -1 { return errors.New("sequence of target commits is not supported") } return nil }, RunE: func(cmd *cobra.Command, args []string) error { return theChangelogCmd(args) }, } func init() { rootCmd.AddCommand(changelogCmd) } func theChangelogCmd(args []string) error { // TODO: refactor so that we don't depend on the packages command rpt, err := thePackagesCmd(args[:3]) if err != nil { return err } if rpt.IsEmpty() { return nil } // there should only be one report, the delta between the base and target commits if len(rpt.CommitsReports) > 1 { panic("expected only one report") } for _, cr := range rpt.CommitsReports { changelog, err := writePackageChangelog(cr, args[3]) if err != nil { return err } fmt.Println(changelog) } return nil } func writePackageChangelog(pr report.PkgsReport, version string) (string, error) { md := &markdown.Writer{} // write out the changelog's title and the release tag header before populating with other changes. md.WriteTitle("Release History") md.WriteTopLevelHeader(fmt.Sprintf("%s (Released)", version)) if err := reportAddedPkgs(pr, md); err != nil { return "", fmt.Errorf("failed to write table for added packages: %+v", err) } if err := reportUpdatedPkgs(pr, md); err != nil { return "", fmt.Errorf("failed to write table for updated packages: %+v", err) } if err := reportBreakingPkgs(pr, md); err != nil { return "", fmt.Errorf("failed to write table for breaking change packages: %+v", err) } if err := reportRemovedPkgs(pr, md); err != nil { return "", fmt.Errorf("failed to write table for removed packages: %+v", err) } return md.String(), nil } func reportAddedPkgs(pr report.PkgsReport, md *markdown.Writer) error { if len(pr.AddedPackages) == 0 { return nil } t, err := createPackageTable(pr.AddedPackages) if err != nil { return err } if t.Rows() > 0 { md.WriteSubheader("New Packages") md.WriteTable(*t) } return nil } func reportUpdatedPkgs(pr report.PkgsReport, md *markdown.Writer) error { if pr.ModifiedPackages == nil || !pr.ModifiedPackages.HasAdditiveChanges() { return nil } var updated []string for pkgName, pkgRpt := range pr.ModifiedPackages { if pkgRpt.HasAdditiveChanges() && !pkgRpt.HasBreakingChanges() { updated = append(updated, pkgName) } } t, err := createPackageTable(updated) if err != nil { return err } if t.Rows() > 0 { md.WriteSubheader("Updated Packages") md.WriteTable(*t) } return nil } func reportBreakingPkgs(pr report.PkgsReport, md *markdown.Writer) error { if pr.ModifiedPackages == nil || !pr.ModifiedPackages.HasBreakingChanges() { return nil } var breaking []string for pkgName, pkgRpt := range pr.ModifiedPackages { if pkgRpt.HasBreakingChanges() { breaking = append(breaking, pkgName) } } t, err := createPackageTable(breaking) if err != nil { return err } if t.Rows() > 0 { md.WriteSubheader("Breaking Changes") md.WriteTable(*t) } return nil } func reportRemovedPkgs(pr report.PkgsReport, md *markdown.Writer) error { if len(pr.RemovedPackages) == 0 { return nil } t, err := createPackageTable(pr.RemovedPackages) if err != nil { return err } if t.Rows() > 0 { md.WriteSubheader("Removed Packages") md.WriteTable(*t) } return nil } type tableRow struct { pkgName string apiVersions []string } func convertFullPackagePathToPackageNameAndAPIVersion(packageName string) (string, string, error) { // packageName is a string like "github.com/Azure/azure-sdk-for-go/services/consumption/mgmt/2018-08-31/consumption" segments := strings.Split(packageName, "/") if len(segments) < 2 { return "", "", fmt.Errorf("expecting package name '%s' to have at least two segments", packageName) } return segments[len(segments)-1], segments[len(segments)-2], nil } func createPackageTable(pkgs []string) (*markdown.Table, error) { t := markdown.NewTable("rc", "Package Name", "API Version") rows, err := categorizePackageAPIVersions(pkgs) if err != nil { return nil, err } for _, row := range rows { t.AddRow(row.pkgName, strings.Join(row.apiVersions, "<br/>")) } return t, nil } func categorizePackageAPIVersions(pkgs []string) ([]tableRow, error) { entries := make(map[string][]string) for _, pkg := range pkgs { pkgName, apiVer, err := convertFullPackagePathToPackageNameAndAPIVersion(pkg) if err != nil { return nil, err } if apis, ok := entries[pkgName]; ok { entries[pkgName] = append(apis, apiVer) } else { entries[pkgName] = []string{apiVer} } } // convert the map to a slice of tableRows var rows []tableRow for pkgName, apiVers := range entries { sort.Strings(apiVers) rows = append(rows, tableRow{ pkgName: pkgName, apiVersions: apiVers, }) } sort.Slice(rows, func(i, j int) bool { return rows[i].pkgName < rows[j].pkgName }) return rows, nil }