eng/tools/apidiff/cmd/packages.go (156 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 (
"encoding/json"
"fmt"
"io"
"os"
"strings"
"github.com/Azure/azure-sdk-for-go/eng/tools/internal/exports"
"github.com/Azure/azure-sdk-for-go/eng/tools/internal/repo"
"github.com/Azure/azure-sdk-for-go/eng/tools/internal/report"
"github.com/spf13/cobra"
)
var packagesCmd = &cobra.Command{
Use: "packages <package search dir> (<base commit> <target commit(s)>) | (<commit sequence>)",
Short: "Generates a report for all packages under the specified directory containing the delta between commits.",
Long: `The packages command generates a report for all of the packages under the directory specified in <package dir>.
Commits can be specified as either a base and one or more target commits or a sequence of commits.
For a base/target pair each target commit is compared against the base commit.
For a commit sequence each commit N in the sequence is compared against commit N+1.
Commit sequences must be comma-delimited.`,
RunE: func(cmd *cobra.Command, args []string) error {
rpt, err := thePackagesCmd(args)
if err != nil {
return err
}
err = PrintReport(rpt)
if err != nil {
return err
}
evalReportStatus(rpt)
return nil
},
}
func init() {
rootCmd.AddCommand(packagesCmd)
}
// ExecPackagesCmd is the programmatic interface for the packages command.
func ExecPackagesCmd(pkgDir string, commitSeq string, flags CommandFlags) (report.CommitPkgsReport, error) {
flags.apply()
return thePackagesCmd([]string{pkgDir, commitSeq})
}
// split into its own func as we can't call os.Exit from it (the defer won't get executed)
func thePackagesCmd(args []string) (rpt report.CommitPkgsReport, err error) {
cloneRepo, err := processArgsAndClone(args)
if err != nil {
return
}
rpt.CommitsReports = map[string]report.PkgsReport{}
worker := func(rootDir string, cloneRepo repo.WorkingTree, baseCommit, targetCommit string) error {
vprintf("generating diff between %s and %s\n", baseCommit, targetCommit)
// get for lhs
dprintf("checking out base commit %s and gathering exports\n", baseCommit)
lhs, err := getRepoContentForCommit(&cloneRepo, rootDir, baseCommit)
if err != nil {
return err
}
// get for rhs
dprintf("checking out target commit %s and gathering exports\n", targetCommit)
rhs, err := getRepoContentForCommit(&cloneRepo, rootDir, targetCommit)
if err != nil {
return err
}
r := getPkgsReport(lhs, rhs)
rpt.UpdateAffectedPackages(targetCommit, r)
if r.HasBreakingChanges() {
rpt.BreakingChanges = append(rpt.BreakingChanges, targetCommit)
}
rpt.CommitsReports[fmt.Sprintf("%s:%s", baseCommit, targetCommit)] = r
return nil
}
err = generateReports(args, cloneRepo, worker)
if err != nil {
return
}
return
}
func getRepoContentForCommit(wt *repo.WorkingTree, dir, commit string) (r RepoContent, err error) {
err = wt.Checkout(commit)
if err != nil {
err = fmt.Errorf("failed to check out commit '%s': %s", commit, err)
return
}
return getRepoContent(wt, dir)
}
func getRepoContent(wt *repo.WorkingTree, dir string) (RepoContent, error) {
pkgDirs, err := report.GetPackages(dir)
if err != nil {
return nil, err
}
if debugFlag {
fmt.Println("found the following package directories")
for _, d := range pkgDirs {
fmt.Printf("\t%s\n", d)
}
}
r, err := getExportsForPackages(wt.Root(), pkgDirs)
if err != nil {
return nil, err
}
return r, nil
}
// RepoContent contains repo content, it's structured as "package path":content
type RepoContent map[string]exports.Content
// returns RepoContent based on the provided slice of package directories
func getExportsForPackages(root string, pkgDirs []string) (RepoContent, error) {
exps := RepoContent{}
for _, pkgDir := range pkgDirs {
dprintf("getting exports for %s\n", pkgDir)
// pkgDir = "C:\Users\somebody\AppData\Local\Temp\apidiff-1529437978\services\addons\mgmt\2017-05-15\addons"
// convert to package path "github.com/Azure/azure-sdk-for-go/services/analysisservices/mgmt/2016-05-16/analysisservices"
pkgPath := strings.Replace(pkgDir, root, "github.com/Azure/azure-sdk-for-go", -1)
pkgPath = strings.Replace(pkgPath, string(os.PathSeparator), "/", -1)
if _, ok := exps[pkgPath]; ok {
return nil, fmt.Errorf("duplicate package: %s", pkgPath)
}
exp, err := exports.Get(pkgDir)
if err != nil {
return nil, err
}
exps[pkgPath] = exp
}
return exps, nil
}
// Print prints the RepoContent to a Writer as JSON string
func (r *RepoContent) Print(o io.Writer) error {
b, err := json.MarshalIndent(r, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal report: %v", err)
}
_, err = o.Write(b)
return err
}
// generates a PkgsReport based on the delta between lhs and rhs
func getPkgsReport(lhs, rhs RepoContent) report.PkgsReport {
rpt := report.PkgsReport{}
if !onlyBreakingChangesFlag {
rpt.AddedPackages = getDiffPkgs(lhs, rhs)
}
if !onlyAdditiveChangesFlag {
rpt.RemovedPackages = getDiffPkgs(rhs, lhs)
}
// diff packages
for rhsPkg, rhsCnt := range rhs {
if _, ok := lhs[rhsPkg]; !ok {
continue
}
if r := report.Generate(lhs[rhsPkg], rhsCnt, &report.GenerationOption{
OnlyBreakingChanges: onlyBreakingChangesFlag,
OnlyAdditiveChanges: onlyAdditiveChangesFlag,
}); !r.IsEmpty() {
// only add an entry if the report contains data
if rpt.ModifiedPackages == nil {
rpt.ModifiedPackages = report.ModifiedPackages{}
}
rpt.ModifiedPackages[rhsPkg] = r
}
}
return rpt
}
// returns a list of packages in rhs that aren't in lhs
func getDiffPkgs(lhs, rhs RepoContent) report.PkgsList {
list := report.PkgsList{}
for rhsPkg := range rhs {
if _, ok := lhs[rhsPkg]; !ok {
list = append(list, rhsPkg)
}
}
return list
}