Skip to content

Commit

Permalink
Merge pull request #14 from charypar/clean-up-cli
Browse files Browse the repository at this point in the history
Clean up CLI code
  • Loading branch information
charypar authored Nov 19, 2018
2 parents 1ea5c38 + 8b7a386 commit 104deb9
Show file tree
Hide file tree
Showing 6 changed files with 256 additions and 122 deletions.
175 changes: 86 additions & 89 deletions cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"github.com/charypar/monobuild/diff"
"github.com/charypar/monobuild/graph"
"github.com/charypar/monobuild/manifests"
"github.com/charypar/monobuild/set"
)

func joinErrors(message string, errors []error) error {
Expand All @@ -20,138 +19,136 @@ func joinErrors(message string, errors []error) error {
return fmt.Errorf("%s\n%s", message, strings.Join(errstrings, "\n"))
}

func Format(dependencies graph.Graph, schedule graph.Graph, impacted []string, dotFormat bool, printDependencies bool) string {
if dotFormat && printDependencies {
return dependencies.Dot(impacted)
func loadManifests(globPattern string) ([]string, graph.Graph, graph.Graph, error) {
manifestFiles, err := doublestar.Glob(globPattern)
if err != nil {
return []string{}, graph.Graph{}, graph.Graph{}, fmt.Errorf("error finding dependency manifests: %s", err)
}

if dotFormat {
return schedule.DotSchedule(impacted)
// Find components and dependencies
components, deps, errs := manifests.Read(manifestFiles, false)
if errs != nil {
return []string{}, graph.Graph{}, graph.Graph{}, fmt.Errorf("%s", joinErrors("cannot load dependencies:", errs))
}

if printDependencies {
return dependencies.Text(impacted)
}
dependencies := deps.AsGraph()
buildSchedule := dependencies.FilterEdges([]int{graph.Strong})

return schedule.Text(impacted)
return components, dependencies, buildSchedule, nil
}

// Print is 'monobuild print'
func Print(dependencyFilesGlob string, scope string, topLevel bool) (graph.Graph, graph.Graph, []string, error) {
paths, err := doublestar.Glob(dependencyFilesGlob)
if err != nil {
return graph.Graph{}, graph.Graph{}, []string{}, fmt.Errorf("Error finding dependency manifests: %s", err)
}
// Scope of selection
type Scope struct {
Scope string
TopLevel bool
}

components, deps, errs := manifests.Read(paths, false)
if errs != nil {
return graph.Graph{}, graph.Graph{}, []string{}, fmt.Errorf("%s", joinErrors("cannot load dependencies:", errs))
}
// OutputFormat hold the format of text output
type OutputFormat int

dependencies := deps.AsGraph()
buildSchedule := dependencies.FilterEdges([]int{graph.Strong})
// Text is the standard text format.
// Each line follows this pattern:
// <component>: dependency, dependency, dependency...
var Text OutputFormat = 1

selection := dependencies.Vertices()
// Dot is the DOT graph language, see https://graphviz.gitlab.io/_pages/doc/info/lang.html
var Dot OutputFormat = 2

if scope != "" {
var scoped []string
// OutputType holds the kind of output to show
type OutputType int

// ensure valid scope
for _, c := range components {
if c == scope {
scoped = []string{scope}
}
}
// Schedule is a build schedule output showing build steps and their dependencies
var Schedule OutputType = 1

if len(scoped) < 1 {
return graph.Graph{}, graph.Graph{}, []string{}, fmt.Errorf("Cannot scope to '%s', not a component", scope)
}
// Dependencies is the dependency graph showing components and their dependencies
var Dependencies OutputType = 2

// OutputOptions hold all the options that change how the result of a command is shown
// on the command line.
// The options are not always independent, e.g. the Dot format has different output
// for Schedule type and Dependencies type.
type OutputOptions struct {
Format OutputFormat // Output text format
Type OutputType // Type of output shown
}

// Format output for the command line, filtering nodes only to those in the 'filter' slice.
// Output options can be set using 'opts'
func Format(dependencies graph.Graph, schedule graph.Graph, filter []string, opts OutputOptions) string {
if opts.Format == Dot && opts.Type == Dependencies {
return dependencies.Dot(filter)
}

if opts.Format == Dot {
return schedule.DotSchedule(filter)
}

selection = append(dependencies.Descendants(scoped), scoped...)
if opts.Type == Dependencies {
return dependencies.Text(filter)
}

if topLevel {
reverse := dependencies.Reverse()
vertices := dependencies.Vertices()
return schedule.Text(filter)
}

// Print is 'monobuild print'
func Print(dependencyFilesGlob string, scope Scope) (graph.Graph, graph.Graph, []string, error) {
components, dependencies, buildSchedule, err := loadManifests(dependencyFilesGlob)
if err != nil {
return graph.Graph{}, graph.Graph{}, []string{}, err
}

selection := newFilter(components, []string{})

topLevel := make([]string, 0, len(vertices))
for i := range vertices {
if len(reverse.Children(vertices[i:i+1])) < 1 {
topLevel = append(topLevel, vertices[i])
}
if scope.Scope != "" {
err = selection.scopeTo(scope.Scope, dependencies)
if err != nil {
return graph.Graph{}, graph.Graph{}, []string{}, err
}
}

selection = set.New(selection).Intersect(set.New(topLevel)).AsStrings()
if scope.TopLevel {
selection.onlyTop(dependencies)
}

return dependencies, buildSchedule, selection, nil
return dependencies, buildSchedule, selection.AsStrings(), nil
}

// Diff is 'monobuild diff'
func Diff(dependencyFilesGlob string, mainBranch bool, baseBranch string, baseCommit string, includeStrong bool, scope string, topLevel bool) (graph.Graph, graph.Graph, []string, error) {
manifestFiles, err := doublestar.Glob(dependencyFilesGlob)
func Diff(dependencyFilesGlob string, mode diff.Mode, scope Scope, includeStrong bool) (graph.Graph, graph.Graph, []string, error) {
components, dependencies, buildSchedule, err := loadManifests(dependencyFilesGlob)
if err != nil {
return graph.Graph{}, graph.Graph{}, []string{}, fmt.Errorf("error finding dependency manifests: %s", err)
}

// Find components and dependency manifests
components, deps, errs := manifests.Read(manifestFiles, false)
if errs != nil {
return graph.Graph{}, graph.Graph{}, []string{}, fmt.Errorf("%s", joinErrors("cannot load dependencies:", errs))
return graph.Graph{}, graph.Graph{}, []string{}, err
}

// Get changed files
changes, err := diff.ChangedFiles(mainBranch, baseBranch, baseCommit)
changes, err := diff.ChangedFiles(mode)
if err != nil {
return graph.Graph{}, graph.Graph{}, []string{}, fmt.Errorf("cannot find changes: %s", err)
}

// Reduce changed files to components
changedComponents := manifests.FilterComponents(components, changes)

// Find impacted components
dependencies := deps.AsGraph()
buildSchedule := dependencies.FilterEdges([]int{graph.Strong})

changedComponents := manifests.FilterComponents(components, changes)
impacted := diff.Impacted(changedComponents, dependencies)

if scope != "" {
var scoped []string
// Select what to show

// ensure valid scope
for _, c := range components {
if c == scope {
scoped = []string{scope}
}
}
selection := newFilter(components, impacted)

if len(scoped) < 1 {
return graph.Graph{}, graph.Graph{}, []string{}, fmt.Errorf("Cannot scope to '%s', not a component", scope)
if scope.Scope != "" {
err = selection.scopeTo(scope.Scope, dependencies)
if err != nil {
return graph.Graph{}, graph.Graph{}, []string{}, err
}

scopedAndDeps := append(dependencies.Descendants(scoped), scoped...)
impacted = set.New(impacted).Intersect(set.New(scopedAndDeps)).AsStrings()
}

if topLevel {
reverse := dependencies.Reverse()
vertices := dependencies.Vertices()

topLevel := make([]string, 0, len(vertices))
for i := range vertices {
if len(reverse.Children(vertices[i:i+1])) < 1 {
topLevel = append(topLevel, vertices[i])
}
}

impacted = set.New(impacted).Intersect(set.New(topLevel)).AsStrings()
if scope.TopLevel {
selection.onlyTop(dependencies)
}

// needs to come _after_ topLevel!
if includeStrong {
strong := buildSchedule.Descendants(impacted)
impacted = append(impacted, strong...)
selection.addStrong(buildSchedule)
}

return dependencies, buildSchedule, impacted, nil
return dependencies, buildSchedule, selection.AsStrings(), nil
}
61 changes: 61 additions & 0 deletions cli/filter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package cli

import (
"fmt"

"github.com/charypar/monobuild/graph"
"github.com/charypar/monobuild/set"
)

type filter struct {
components set.Set
filtered set.Set
}

func newFilter(components []string, filtered []string) filter {
componentsSet := set.New(components)
var filteredSet set.Set
if len(filtered) < 1 {
filteredSet = componentsSet
} else {
filteredSet = set.New(filtered)
}

return filter{components: componentsSet, filtered: filteredSet}
}

func (f *filter) scopeTo(component string, dependencies graph.Graph) error {
if !f.components.Has(component) {
return fmt.Errorf("cannot scope to '%s', not a component", component)
}

scoped := set.New(append(dependencies.Descendants([]string{component}), component))
f.filtered = f.filtered.Intersect(scoped)

return nil
}

func (f *filter) onlyTop(dependencies graph.Graph) {
// FIXME this algorithm probably belongs to graph
reverse := dependencies.Reverse()
vertices := dependencies.Vertices()

topLevel := make([]string, 0, len(vertices))
for i := range vertices {
if len(reverse.Children(vertices[i:i+1])) < 1 {
topLevel = append(topLevel, vertices[i])
}
}

f.filtered = f.filtered.Intersect(set.New(topLevel))
}

func (f *filter) addStrong(buildSchedule graph.Graph) {
strong := buildSchedule.Descendants(f.filtered.AsStrings())

f.filtered = f.filtered.Union(set.New(strong))
}

func (f *filter) AsStrings() []string {
return f.filtered.AsStrings()
}
61 changes: 48 additions & 13 deletions cmd/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,19 @@ import (
"log"

"github.com/charypar/monobuild/cli"
"github.com/charypar/monobuild/diff"
"github.com/spf13/cobra"
)

var baseBranch string
var baseCommit string
var mainBranch bool
var rebuildStrong bool
var dotHighlight bool
type diffOptions struct {
baseBranch string
baseCommit string
mainBranch bool
rebuildStrong bool
dotHighlight bool
}

var diffOpts diffOptions

var diffCmd = &cobra.Command{
Use: "diff",
Expand All @@ -31,19 +36,49 @@ the original dependeny graph (using all dependencies).`,
func init() {
rootCmd.AddCommand(diffCmd)

diffCmd.Flags().StringVar(&baseBranch, "base-branch", "master", "Base branch to use for comparison")
diffCmd.Flags().StringVar(&baseCommit, "base-commit", "HEAD^1", "Base commit to compare with (useful in main-brahnch mode when using rebase merging)")
diffCmd.Flags().BoolVar(&mainBranch, "main-branch", false, "Run in main branch mode (i.e. only compare with parent commit)")
diffCmd.Flags().BoolVar(&rebuildStrong, "rebuild-strong", false, "Include all strong dependencies of affected components")
diffCmd.Flags().BoolVar(&printDependencies, "dependencies", false, "Ouput the dependencies, not the build schedule")
diffCmd.Flags().BoolVar(&dotFormat, "dot", false, "Print in DOT format for GraphViz")
diffCmd.Flags().StringVar(&diffOpts.baseBranch, "base-branch", "master", "Base branch to use for comparison")
diffCmd.Flags().StringVar(&diffOpts.baseCommit, "base-commit", "HEAD^1", "Base commit to compare with (useful in main-brahnch mode when using rebase merging)")
diffCmd.Flags().BoolVar(&diffOpts.mainBranch, "main-branch", false, "Run in main branch mode (i.e. only compare with parent commit)")
diffCmd.Flags().BoolVar(&diffOpts.rebuildStrong, "rebuild-strong", false, "Include all strong dependencies of affected components")
diffCmd.Flags().BoolVar(&commonOpts.printDependencies, "dependencies", false, "Ouput the dependencies, not the build schedule")
diffCmd.Flags().BoolVar(&commonOpts.dotFormat, "dot", false, "Print in DOT format for GraphViz")
}

func diffFn(cmd *cobra.Command, args []string) {
dependencies, schedule, impacted, err := cli.Diff(dependencyFilesGlob, mainBranch, baseBranch, baseCommit, rebuildStrong, scope, topLevel)
// first we tediously process the CLI flags

var branchMode diff.BranchMode
if diffOpts.mainBranch {
branchMode = diff.Main
} else {
branchMode = diff.Feature
}

var format cli.OutputFormat
if commonOpts.dotFormat {
format = cli.Dot
} else {
format = cli.Text
}

diffMode := diff.Mode{Mode: branchMode, BaseBranch: diffOpts.baseBranch, BaseCommit: diffOpts.baseCommit}
scope := cli.Scope{Scope: commonOpts.scope, TopLevel: commonOpts.topLevel}

var outType cli.OutputType
if commonOpts.printDependencies {
outType = cli.Dependencies
} else {
outType = cli.Schedule
}

outputOpts := cli.OutputOptions{Format: format, Type: outType}

// run the CLI command

dependencies, schedule, impacted, err := cli.Diff(commonOpts.dependencyFilesGlob, diffMode, scope, diffOpts.rebuildStrong)
if err != nil {
log.Fatal(err)
}

fmt.Print(cli.Format(dependencies, schedule, impacted, dotFormat, printDependencies))
fmt.Print(cli.Format(dependencies, schedule, impacted, outputOpts))
}
Loading

0 comments on commit 104deb9

Please # to comment.