cobra_commander 0.5.1 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.editorconfig +11 -0
- data/.github/workflows/ci.yml +38 -0
- data/.travis.yml +3 -2
- data/CHANGELOG.md +25 -0
- data/README.md +110 -11
- data/cobra_commander.gemspec +4 -1
- data/exe/cobra +1 -1
- data/lib/cobra_commander.rb +15 -8
- data/lib/cobra_commander/affected.rb +43 -28
- data/lib/cobra_commander/change.rb +6 -7
- data/lib/cobra_commander/cli.rb +70 -39
- data/lib/cobra_commander/component.rb +52 -0
- data/lib/cobra_commander/dependencies.rb +4 -0
- data/lib/cobra_commander/dependencies/bundler.rb +38 -0
- data/lib/cobra_commander/dependencies/yarn/package.rb +37 -0
- data/lib/cobra_commander/dependencies/yarn/package_repo.rb +31 -0
- data/lib/cobra_commander/dependencies/yarn_workspace.rb +55 -0
- data/lib/cobra_commander/executor.rb +10 -22
- data/lib/cobra_commander/output.rb +3 -72
- data/lib/cobra_commander/output/ascii_tree.rb +55 -0
- data/lib/cobra_commander/output/flat_list.rb +16 -0
- data/lib/cobra_commander/output/graph_viz.rb +27 -0
- data/lib/cobra_commander/umbrella.rb +51 -0
- data/lib/cobra_commander/version.rb +1 -1
- data/renovate.json +8 -0
- metadata +33 -6
- data/lib/cobra_commander/component_tree.rb +0 -170
- data/lib/cobra_commander/graph.rb +0 -46
data/lib/cobra_commander/cli.rb
CHANGED
@@ -1,62 +1,93 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "thor"
|
4
|
+
require "fileutils"
|
5
|
+
|
6
|
+
require "cobra_commander"
|
7
|
+
require "cobra_commander/affected"
|
8
|
+
require "cobra_commander/change"
|
9
|
+
require "cobra_commander/executor"
|
10
|
+
require "cobra_commander/output"
|
4
11
|
|
5
12
|
module CobraCommander
|
6
13
|
# Implements the tool's CLI
|
7
14
|
class CLI < Thor
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
desc "ls [app_path]", "Prints tree of components for an app"
|
17
|
-
method_option :app, default: Dir.pwd, aliases: "-a", desc: "App path (default: CWD)"
|
18
|
-
method_option :format, default: "tree", aliases: "-f", desc: "Format (list or tree, default: list)"
|
19
|
-
def ls(app_path = nil)
|
20
|
-
Output.print(
|
21
|
-
CobraCommander.umbrella_tree(app_path || options.app),
|
22
|
-
options.format
|
23
|
-
)
|
15
|
+
class_option :app, default: Dir.pwd, aliases: "-a", type: :string
|
16
|
+
class_option :js, default: false, type: :boolean, desc: "Consider only the JS dependency graph"
|
17
|
+
class_option :ruby, default: false, type: :boolean, desc: "Consider only the Ruby dependency graph"
|
18
|
+
|
19
|
+
desc "version", "Prints version"
|
20
|
+
def version
|
21
|
+
puts CobraCommander::VERSION
|
24
22
|
end
|
25
23
|
|
26
|
-
desc "
|
27
|
-
method_option :
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
24
|
+
desc "ls [component]", "Lists the components in the context of a given component or umbrella"
|
25
|
+
method_option :dependencies, type: :boolean, aliases: "-d",
|
26
|
+
desc: "Run the command on each dependency of a given component"
|
27
|
+
method_option :dependents, type: :boolean, aliases: "-D",
|
28
|
+
desc: "Run the command on each dependency of a given component"
|
29
|
+
method_option :total, type: :boolean, aliases: "-t", desc: "Prints the total count of components"
|
30
|
+
def ls(component = nil)
|
31
|
+
components = components_filtered(component)
|
32
|
+
puts options.total ? components.size : CobraCommander::Output::FlatList.new(components).to_s
|
32
33
|
end
|
33
34
|
|
34
|
-
desc "
|
35
|
-
|
36
|
-
method_option :
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
35
|
+
desc "exec [component] <command>", "Executes the command in the context of a given component or set thereof. " \
|
36
|
+
"Defaults to all components."
|
37
|
+
method_option :dependencies, type: :boolean, desc: "Run the command on each dependency of a given component"
|
38
|
+
method_option :dependents, type: :boolean, desc: "Run the command on each dependency of a given component"
|
39
|
+
def exec(command_or_component, command = nil)
|
40
|
+
CobraCommander::Executor.exec(
|
41
|
+
components_filtered(command && command_or_component),
|
42
|
+
command ? command : command_or_component
|
41
43
|
)
|
42
44
|
end
|
43
45
|
|
44
|
-
desc "
|
45
|
-
def
|
46
|
-
|
46
|
+
desc "tree [component]", "Prints the dependency tree of a given component or umbrella"
|
47
|
+
def tree(component = nil)
|
48
|
+
component = find_component(component)
|
49
|
+
puts CobraCommander::Output::AsciiTree.new(component).to_s
|
47
50
|
end
|
48
51
|
|
49
|
-
desc "graph
|
50
|
-
method_option :
|
51
|
-
|
52
|
-
|
52
|
+
desc "graph [component]", "Outputs a graph of a given component or umbrella"
|
53
|
+
method_option :output, default: File.join(Dir.pwd, "output.png"), aliases: "-o",
|
54
|
+
desc: "Output file, accepts .png or .dot"
|
55
|
+
def graph(component = nil)
|
56
|
+
CobraCommander::Output::GraphViz.generate(
|
57
|
+
find_component(component),
|
58
|
+
options.output
|
59
|
+
)
|
60
|
+
puts "Graph generated at #{options.output}"
|
61
|
+
rescue ArgumentError => error
|
62
|
+
error error.message
|
53
63
|
end
|
54
64
|
|
55
|
-
desc "changes
|
65
|
+
desc "changes [--results=RESULTS] [--branch=BRANCH]", "Prints list of changed files"
|
56
66
|
method_option :results, default: "test", aliases: "-r", desc: "Accepts test, full, name or json"
|
57
67
|
method_option :branch, default: "master", aliases: "-b", desc: "Specified target to calculate against"
|
58
|
-
def changes
|
59
|
-
Change.new(
|
68
|
+
def changes
|
69
|
+
Change.new(umbrella, options.results, options.branch).run!
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def umbrella
|
75
|
+
@umbrella ||= CobraCommander.umbrella(options.app, yarn: options.js, bundler: options.ruby)
|
76
|
+
end
|
77
|
+
|
78
|
+
def find_component(name)
|
79
|
+
return umbrella.root unless name
|
80
|
+
|
81
|
+
umbrella.find(name) || error("Component #{name} not found, try one of `cobra ls`") || exit(1)
|
82
|
+
end
|
83
|
+
|
84
|
+
def components_filtered(component_name)
|
85
|
+
return umbrella.components unless component_name
|
86
|
+
component = find_component(component_name)
|
87
|
+
|
88
|
+
return component.deep_dependencies if options.dependencies
|
89
|
+
return component.deep_dependents if options.dependents
|
90
|
+
[component]
|
60
91
|
end
|
61
92
|
end
|
62
93
|
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CobraCommander
|
4
|
+
# Represents a component withing an Umbrella
|
5
|
+
class Component
|
6
|
+
attr_reader :name, :dependencies, :sources
|
7
|
+
|
8
|
+
def initialize(umbrella, name)
|
9
|
+
@umbrella = umbrella
|
10
|
+
@name = name
|
11
|
+
@dependency_names = []
|
12
|
+
@sources = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_source(key, path, dependency_names)
|
16
|
+
@sources[key] = path
|
17
|
+
@dependency_names |= dependency_names
|
18
|
+
end
|
19
|
+
|
20
|
+
def root_paths
|
21
|
+
@sources.values.map(&File.method(:dirname)).uniq
|
22
|
+
end
|
23
|
+
|
24
|
+
def inspect
|
25
|
+
"#<CobraCommander::Component:#{object_id} #{name} dependencies=#{dependencies.map(&:name)} packages=#{sources}>"
|
26
|
+
end
|
27
|
+
|
28
|
+
def deep_dependents
|
29
|
+
@deep_dependents ||= @umbrella.components.find_all do |dep|
|
30
|
+
dep.deep_dependencies.include?(self)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def deep_dependencies
|
35
|
+
@deep_dependencies ||= dependencies.reduce(dependencies) do |deps, dep|
|
36
|
+
deps | dep.deep_dependencies
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def dependents
|
41
|
+
@dependents ||= @umbrella.components.find_all do |dep|
|
42
|
+
dep.dependencies.include?(self)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def dependencies
|
47
|
+
@dependencies ||= @dependency_names.sort
|
48
|
+
.map(&@umbrella.method(:find))
|
49
|
+
.compact
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CobraCommander
|
4
|
+
module Dependencies
|
5
|
+
# Calculates ruby bundler dependencies
|
6
|
+
class Bundler
|
7
|
+
def initialize(root)
|
8
|
+
@definition = ::Bundler::Definition.build(
|
9
|
+
Pathname.new(File.join(root, "Gemfile")).realpath,
|
10
|
+
Pathname.new(File.join(root, "Gemfile.lock")).realpath,
|
11
|
+
false
|
12
|
+
)
|
13
|
+
end
|
14
|
+
|
15
|
+
def path
|
16
|
+
@definition.lockfile
|
17
|
+
end
|
18
|
+
|
19
|
+
def dependencies
|
20
|
+
@definition.dependencies.map(&:name)
|
21
|
+
end
|
22
|
+
|
23
|
+
def components
|
24
|
+
components_source.specs.map do |spec|
|
25
|
+
{ path: spec.loaded_from, name: spec.name, dependencies: spec.dependencies.map(&:name) }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def components_source
|
32
|
+
@components_source ||= @definition.send(:sources).path_sources.find do |source|
|
33
|
+
source.path.to_s.eql?("components")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module CobraCommander
|
6
|
+
module Dependencies
|
7
|
+
module Yarn
|
8
|
+
# Represents an Yarn package.json file
|
9
|
+
class Package
|
10
|
+
attr_reader :path
|
11
|
+
|
12
|
+
def initialize(path)
|
13
|
+
@path = Pathname.new(File.join(path, "package.json")).realpath
|
14
|
+
end
|
15
|
+
|
16
|
+
def project_tag
|
17
|
+
name.match(%r{^@[\w-]+\/}).to_s
|
18
|
+
end
|
19
|
+
|
20
|
+
def name
|
21
|
+
json["name"]
|
22
|
+
end
|
23
|
+
|
24
|
+
def dependencies
|
25
|
+
json.fetch("dependencies", {})
|
26
|
+
.merge(json.fetch("devDependencies", {}))
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def json
|
32
|
+
@json ||= JSON.parse(File.read(@path))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CobraCommander
|
4
|
+
module Dependencies
|
5
|
+
module Yarn
|
6
|
+
# Yarn package repository to load and cache package.json files
|
7
|
+
class PackageRepo
|
8
|
+
def initialize
|
9
|
+
@specs ||= {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def specs
|
13
|
+
@specs.values
|
14
|
+
end
|
15
|
+
|
16
|
+
def load_linked_specs(package)
|
17
|
+
package.dependencies.values.each do |spec|
|
18
|
+
next unless spec =~ /link:(.+)/
|
19
|
+
load_spec(File.join(package.path, "..", Regexp.last_match(1)))
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def load_spec(path)
|
24
|
+
@specs[path] ||= Package.new(path).tap do |package|
|
25
|
+
load_linked_specs(package)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "open3"
|
4
|
+
|
5
|
+
require_relative "yarn/package"
|
6
|
+
require_relative "yarn/package_repo"
|
7
|
+
|
8
|
+
module CobraCommander
|
9
|
+
module Dependencies
|
10
|
+
# Yarn workspace components source for an umbrella
|
11
|
+
class YarnWorkspace
|
12
|
+
attr_reader :packages
|
13
|
+
|
14
|
+
def initialize(root_path)
|
15
|
+
@repo = Yarn::PackageRepo.new
|
16
|
+
@root_package = Yarn::Package.new(root_path)
|
17
|
+
@repo.load_linked_specs(@root_package)
|
18
|
+
load_workspace_packages
|
19
|
+
end
|
20
|
+
|
21
|
+
def path
|
22
|
+
@root_package.path
|
23
|
+
end
|
24
|
+
|
25
|
+
def dependencies
|
26
|
+
(workspace_spec.keys | @root_package.dependencies.keys).map(&method(:untag))
|
27
|
+
end
|
28
|
+
|
29
|
+
def components
|
30
|
+
@repo.specs.map do |spec|
|
31
|
+
{ path: spec.path, name: untag(spec.name), dependencies: spec.dependencies.keys.map(&method(:untag)) }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def load_workspace_packages
|
38
|
+
workspace_spec.map do |_name, spec|
|
39
|
+
@repo.load_spec File.expand_path(File.join(@root_package.path, "..", spec["location"]))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def workspace_spec
|
44
|
+
@workspace_spec ||= begin
|
45
|
+
output, = Open3.capture2("yarn workspaces --json info", chdir: File.dirname(@root_package.path))
|
46
|
+
JSON.parse(JSON.parse(output)["data"])
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def untag(name)
|
51
|
+
name.gsub(@root_package.project_tag, "")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -2,29 +2,17 @@
|
|
2
2
|
|
3
3
|
module CobraCommander
|
4
4
|
# Execute commands on all components of a ComponentTree
|
5
|
-
|
6
|
-
def
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
private
|
19
|
-
|
20
|
-
def run_in_component(component, command)
|
21
|
-
Dir.chdir(component.path) do
|
22
|
-
Open3.capture2e(env_vars(component), command)
|
5
|
+
module Executor
|
6
|
+
def self.exec(components, command, printer = $stdout)
|
7
|
+
Bundler.with_original_env do
|
8
|
+
components.each do |component|
|
9
|
+
component.root_paths.each do |path|
|
10
|
+
printer.puts "===> #{component.name} (#{path})"
|
11
|
+
output, = Open3.capture2e(command, chdir: path)
|
12
|
+
printer.puts output
|
13
|
+
end
|
14
|
+
end
|
23
15
|
end
|
24
16
|
end
|
25
|
-
|
26
|
-
def env_vars(component)
|
27
|
-
{ "CURRENT_COMPONENT" => component.name, "CURRENT_COMPONENT_PATH" => component.path }
|
28
|
-
end
|
29
17
|
end
|
30
18
|
end
|
@@ -1,74 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
def self.print(tree, format)
|
7
|
-
output = format == "list" ? Output::FlatList.new(tree) : Output::Tree.new(tree)
|
8
|
-
puts output.to_s
|
9
|
-
end
|
10
|
-
|
11
|
-
# Flattens a tree and prints unique items
|
12
|
-
class FlatList
|
13
|
-
def initialize(tree)
|
14
|
-
@tree = tree
|
15
|
-
end
|
16
|
-
|
17
|
-
def to_s
|
18
|
-
@tree.flatten.map(&:name)
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
# Prints the tree in a nice tree form
|
23
|
-
class Tree
|
24
|
-
attr_accessor :tree
|
25
|
-
|
26
|
-
SPACE = " "
|
27
|
-
BAR = "│ "
|
28
|
-
TEE = "├── "
|
29
|
-
CORNER = "└── "
|
30
|
-
|
31
|
-
def initialize(tree)
|
32
|
-
@tree = tree
|
33
|
-
end
|
34
|
-
|
35
|
-
def to_s
|
36
|
-
StringIO.new.tap do |io|
|
37
|
-
io.puts @tree.name
|
38
|
-
list_dependencies(io, @tree)
|
39
|
-
end.string
|
40
|
-
end
|
41
|
-
|
42
|
-
private
|
43
|
-
|
44
|
-
def list_dependencies(io, deps, outdents = [])
|
45
|
-
deps.dependencies.each do |dep|
|
46
|
-
decide_on_line(io, deps, dep, outdents)
|
47
|
-
end
|
48
|
-
nil
|
49
|
-
end
|
50
|
-
|
51
|
-
def decide_on_line(io, parent, dep, outdents)
|
52
|
-
if parent.dependencies.last != dep
|
53
|
-
add_tee(io, outdents, dep)
|
54
|
-
else
|
55
|
-
add_corner(io, outdents, dep)
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
def add_tee(io, outdents, dep)
|
60
|
-
io.puts line(outdents, TEE, dep.name)
|
61
|
-
list_dependencies(io, dep, (outdents + [BAR]))
|
62
|
-
end
|
63
|
-
|
64
|
-
def add_corner(io, outdents, dep)
|
65
|
-
io.puts line(outdents, CORNER, dep.name)
|
66
|
-
list_dependencies(io, dep, (outdents + [SPACE]))
|
67
|
-
end
|
68
|
-
|
69
|
-
def line(outdents, sym, name)
|
70
|
-
(outdents + [sym] + [name]).join
|
71
|
-
end
|
72
|
-
end
|
73
|
-
end
|
74
|
-
end
|
3
|
+
require_relative "output/flat_list"
|
4
|
+
require_relative "output/ascii_tree"
|
5
|
+
require_relative "output/graph_viz"
|