cobra_commander 0.6.0 → 0.9.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/.github/workflows/ci.yml +38 -0
- data/.travis.yml +3 -2
- data/CHANGELOG.md +24 -0
- data/README.md +109 -10
- data/cobra_commander.gemspec +6 -1
- data/exe/cobra +1 -1
- data/lib/cobra_commander.rb +10 -8
- data/lib/cobra_commander/affected.rb +43 -28
- data/lib/cobra_commander/change.rb +6 -6
- data/lib/cobra_commander/cli.rb +61 -73
- 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 +12 -23
- data/lib/cobra_commander/executor/component_exec.rb +23 -0
- data/lib/cobra_commander/executor/multi_exec.rb +45 -0
- 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 +71 -15
- data/lib/cobra_commander/cached_component_tree.rb +0 -24
- data/lib/cobra_commander/calculated_component_tree.rb +0 -146
- data/lib/cobra_commander/component_tree.rb +0 -69
- data/lib/cobra_commander/graph.rb +0 -45
data/lib/cobra_commander/cli.rb
CHANGED
@@ -3,103 +3,91 @@
|
|
3
3
|
require "thor"
|
4
4
|
require "fileutils"
|
5
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"
|
11
|
+
|
6
12
|
module CobraCommander
|
7
13
|
# Implements the tool's CLI
|
8
14
|
class CLI < Thor
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
COMMON_OPTIONS = "[--app=pwd] [--format=FORMAT] [--cache=nil]"
|
13
|
-
|
14
|
-
desc "do [command] [--app=pwd] [--cache=nil]", "Executes the command in the context of each component in [app]"
|
15
|
-
method_option :app, default: Dir.pwd, aliases: "-a", desc: "App path (default: CWD)"
|
16
|
-
method_option :cache, default: nil, aliases: "-c", desc: CACHE_DESCRIPTION
|
17
|
-
def do(command)
|
18
|
-
tree = maybe_cached_tree(options.app, options.cache)
|
19
|
-
executor = Executor.new(tree)
|
20
|
-
executor.exec(command)
|
21
|
-
end
|
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"
|
22
18
|
|
23
|
-
desc "
|
24
|
-
|
25
|
-
|
26
|
-
method_option :cache, default: nil, aliases: "-c", desc: CACHE_DESCRIPTION
|
27
|
-
def ls(app_path = nil)
|
28
|
-
tree = maybe_cached_tree(app_path || options.app, options.cache)
|
29
|
-
Output.print(
|
30
|
-
tree,
|
31
|
-
options.format
|
32
|
-
)
|
19
|
+
desc "version", "Prints version"
|
20
|
+
def version
|
21
|
+
puts CobraCommander::VERSION
|
33
22
|
end
|
34
23
|
|
35
|
-
desc "
|
36
|
-
method_option :
|
37
|
-
|
38
|
-
method_option :
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
43
33
|
end
|
44
34
|
|
45
|
-
desc "
|
46
|
-
|
47
|
-
method_option :
|
48
|
-
method_option :
|
49
|
-
def
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
options.format
|
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
|
54
43
|
)
|
55
44
|
end
|
56
45
|
|
57
|
-
desc "
|
58
|
-
def
|
59
|
-
|
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
|
60
50
|
end
|
61
51
|
|
62
|
-
desc "graph
|
63
|
-
method_option :
|
64
|
-
|
65
|
-
def graph(
|
66
|
-
|
67
|
-
|
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
|
68
63
|
end
|
69
64
|
|
70
|
-
desc "changes
|
65
|
+
desc "changes [--results=RESULTS] [--branch=BRANCH]", "Prints list of changed files"
|
71
66
|
method_option :results, default: "test", aliases: "-r", desc: "Accepts test, full, name or json"
|
72
67
|
method_option :branch, default: "master", aliases: "-b", desc: "Specified target to calculate against"
|
73
|
-
|
74
|
-
|
75
|
-
tree = maybe_cached_tree(app_path, options.cache)
|
76
|
-
Change.new(tree, options.results, options.branch).run!
|
77
|
-
end
|
78
|
-
|
79
|
-
desc "cache APP_PATH CACHE_PATH", "Caches a representation of the component structure of the app"
|
80
|
-
def cache(app_path, cache_path)
|
81
|
-
tree = CobraCommander.umbrella_tree(app_path)
|
82
|
-
write_tree_cache(tree, cache_path)
|
83
|
-
puts "Created cache of component tree at #{cache_path}"
|
68
|
+
def changes
|
69
|
+
Change.new(umbrella, options.results, options.branch).run!
|
84
70
|
end
|
85
71
|
|
86
72
|
private
|
87
73
|
|
88
|
-
def
|
89
|
-
|
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
|
90
80
|
|
91
|
-
|
92
|
-
CobraCommander.tree_from_cache(cache_path)
|
93
|
-
else
|
94
|
-
tree = CobraCommander.umbrella_tree(app_path)
|
95
|
-
write_tree_cache(tree, cache_path)
|
96
|
-
tree
|
97
|
-
end
|
81
|
+
umbrella.find(name) || error("Component #{name} not found, try one of `cobra ls`") || exit(1)
|
98
82
|
end
|
99
83
|
|
100
|
-
def
|
101
|
-
|
102
|
-
|
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]
|
103
91
|
end
|
104
92
|
end
|
105
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
|
@@ -1,30 +1,19 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "executor/component_exec"
|
4
|
+
require_relative "executor/multi_exec"
|
5
|
+
|
3
6
|
module CobraCommander
|
4
7
|
# Execute commands on all components of a ComponentTree
|
5
|
-
|
6
|
-
def
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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)
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
def env_vars(component)
|
27
|
-
{ "CURRENT_COMPONENT" => component.name, "CURRENT_COMPONENT_PATH" => component.path }
|
8
|
+
module Executor
|
9
|
+
def self.exec(components, command, output = $stdout, status_output = $stderr)
|
10
|
+
components = Array(components)
|
11
|
+
exec = if components.size == 1
|
12
|
+
ComponentExec.new(components.first)
|
13
|
+
else
|
14
|
+
MultiExec.new(components)
|
15
|
+
end
|
16
|
+
exec.run(command, output: output, spin_output: status_output)
|
28
17
|
end
|
29
18
|
end
|
30
19
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tty-command"
|
4
|
+
|
5
|
+
module CobraCommander
|
6
|
+
module Executor
|
7
|
+
# Execute a command on a single component
|
8
|
+
class ComponentExec
|
9
|
+
def initialize(component)
|
10
|
+
@component = component
|
11
|
+
end
|
12
|
+
|
13
|
+
def run(command, output: $stdout, **cmd_options)
|
14
|
+
tty = TTY::Command.new(pty: true, printer: :quiet, output: output)
|
15
|
+
Bundler.with_original_env do
|
16
|
+
@component.root_paths.all? do |path|
|
17
|
+
tty.run!(command, chdir: path, **cmd_options).success?
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|