cobra_commander 0.5.0 → 0.8.0
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/.travis.yml +3 -2
- data/CHANGELOG.md +26 -0
- data/README.md +102 -11
- data/cobra_commander.gemspec +5 -2
- data/exe/cobra +1 -1
- data/lib/cobra_commander.rb +15 -8
- data/lib/cobra_commander/affected.rb +37 -30
- 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 +8 -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 +42 -10
- data/lib/cobra_commander/component_tree.rb +0 -168
- data/lib/cobra_commander/graph.rb +0 -46
@@ -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,15 @@
|
|
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
|
-
output, = run_in_component(component, command)
|
14
|
-
printer.puts output
|
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
|
+
components.each do |component|
|
8
|
+
component.root_paths.each do |path|
|
9
|
+
printer.puts "===> #{component.name} (#{path})"
|
10
|
+
output, = Open3.capture2e(command, chdir: path, unsetenv_others: true)
|
11
|
+
printer.puts output
|
12
|
+
end
|
23
13
|
end
|
24
14
|
end
|
25
|
-
|
26
|
-
def env_vars(component)
|
27
|
-
{ "CURRENT_COMPONENT" => component.name, "CURRENT_COMPONENT_PATH" => component.path }
|
28
|
-
end
|
29
15
|
end
|
30
16
|
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"
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CobraCommander
|
4
|
+
module Output
|
5
|
+
# Prints the tree in a nice tree form
|
6
|
+
class AsciiTree
|
7
|
+
SPACE = " "
|
8
|
+
BAR = "│ "
|
9
|
+
TEE = "├── "
|
10
|
+
CORNER = "└── "
|
11
|
+
|
12
|
+
def initialize(component)
|
13
|
+
@component = component
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
StringIO.new.tap do |io|
|
18
|
+
io.puts @component.name
|
19
|
+
list_dependencies(io, @component)
|
20
|
+
end.string
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def list_dependencies(io, component, outdents = [])
|
26
|
+
component.dependencies.each do |dep|
|
27
|
+
decide_on_line(io, component, dep, outdents)
|
28
|
+
end
|
29
|
+
nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def decide_on_line(io, parent, dep, outdents)
|
33
|
+
if parent.dependencies.last != dep
|
34
|
+
add_tee(io, outdents, dep)
|
35
|
+
else
|
36
|
+
add_corner(io, outdents, dep)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def add_tee(io, outdents, dep)
|
41
|
+
io.puts line(outdents, TEE, dep.name)
|
42
|
+
list_dependencies(io, dep, (outdents + [BAR]))
|
43
|
+
end
|
44
|
+
|
45
|
+
def add_corner(io, outdents, dep)
|
46
|
+
io.puts line(outdents, CORNER, dep.name)
|
47
|
+
list_dependencies(io, dep, (outdents + [SPACE]))
|
48
|
+
end
|
49
|
+
|
50
|
+
def line(outdents, sym, name)
|
51
|
+
(outdents + [sym] + [name]).join
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CobraCommander
|
4
|
+
module Output
|
5
|
+
# Prints a list of components' names sorted alphabetically
|
6
|
+
class FlatList
|
7
|
+
def initialize(components)
|
8
|
+
@components = components
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
@components.map(&:name).sort
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "graphviz"
|
4
|
+
|
5
|
+
module CobraCommander
|
6
|
+
module Output
|
7
|
+
# Generates graphs of components
|
8
|
+
module GraphViz
|
9
|
+
def self.generate(component, output)
|
10
|
+
g = ::GraphViz.new(:G, type: :digraph, concentrate: true)
|
11
|
+
([component] + component.deep_dependencies).each do |comp|
|
12
|
+
g.add_nodes comp.name
|
13
|
+
g.add_edges comp.name, comp.dependencies.map(&:name)
|
14
|
+
end
|
15
|
+
|
16
|
+
g.output(extract_format(output) => output)
|
17
|
+
end
|
18
|
+
|
19
|
+
private_class_method def self.extract_format(output)
|
20
|
+
format = output[-3..-1]
|
21
|
+
return format if format == "png" || format == "dot"
|
22
|
+
|
23
|
+
raise ArgumentError, "output format must be 'png' or 'dot'"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CobraCommander
|
4
|
+
# An umbrella application
|
5
|
+
class Umbrella
|
6
|
+
attr_reader :name, :path
|
7
|
+
|
8
|
+
def initialize(name, path)
|
9
|
+
@root_component = Component.new(self, name)
|
10
|
+
@path = path
|
11
|
+
@components = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def find(name)
|
15
|
+
@components[name]
|
16
|
+
end
|
17
|
+
|
18
|
+
def root
|
19
|
+
@root_component
|
20
|
+
end
|
21
|
+
|
22
|
+
def resolve(component_root_path)
|
23
|
+
return root if root.root_paths.include?(component_root_path)
|
24
|
+
components.find do |component|
|
25
|
+
component.root_paths.include?(component_root_path)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def add_source(key, source)
|
30
|
+
@root_component.add_source key, source.path, source.dependencies
|
31
|
+
source.components.each do |path:, name:, dependencies:|
|
32
|
+
@components[name] ||= Component.new(self, name)
|
33
|
+
@components[name].add_source key, path, dependencies
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def components
|
38
|
+
@components.values
|
39
|
+
end
|
40
|
+
|
41
|
+
def dependents_of(component)
|
42
|
+
find(component)&.deep_dependents
|
43
|
+
&.sort_by(&:name)
|
44
|
+
end
|
45
|
+
|
46
|
+
def dependencies_of(name)
|
47
|
+
find(name)&.deep_dependencies
|
48
|
+
&.sort_by(&:name)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|