cobra_commander 0.6.1 → 0.9.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/.rubocop.yml +8 -38
- data/.travis.yml +2 -3
- data/CHANGELOG.md +20 -0
- data/README.md +109 -10
- data/cobra_commander.gemspec +14 -10
- 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 +10 -10
- data/lib/cobra_commander/cli.rb +63 -73
- data/lib/cobra_commander/component.rb +52 -0
- data/lib/cobra_commander/dependencies.rb +4 -0
- data/lib/cobra_commander/dependencies/bundler.rb +42 -0
- data/lib/cobra_commander/dependencies/yarn/package.rb +38 -0
- data/lib/cobra_commander/dependencies/yarn/package_repo.rb +32 -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 +33 -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 +52 -0
- data/lib/cobra_commander/version.rb +1 -1
- data/renovate.json +8 -0
- metadata +94 -38
- data/lib/cobra_commander/cached_component_tree.rb +0 -24
- data/lib/cobra_commander/calculated_component_tree.rb +0 -141
- data/lib/cobra_commander/component_tree.rb +0 -69
- data/lib/cobra_commander/graph.rb +0 -45
@@ -8,12 +8,12 @@ module CobraCommander
|
|
8
8
|
class Change
|
9
9
|
InvalidSelectionError = Class.new(StandardError)
|
10
10
|
|
11
|
-
def initialize(
|
12
|
-
@root_dir = Dir.chdir(
|
11
|
+
def initialize(umbrella, results, branch)
|
12
|
+
@root_dir = Dir.chdir(umbrella.path) { `git rev-parse --show-toplevel`.chomp }
|
13
13
|
@results = results
|
14
14
|
@branch = branch
|
15
|
-
@
|
16
|
-
@affected = Affected.new(@
|
15
|
+
@umbrella = umbrella
|
16
|
+
@affected = Affected.new(@umbrella, changes)
|
17
17
|
end
|
18
18
|
|
19
19
|
def run!
|
@@ -42,16 +42,16 @@ module CobraCommander
|
|
42
42
|
Open3.capture3("git", "diff", "--name-only", @branch)
|
43
43
|
end
|
44
44
|
|
45
|
-
if result.exitstatus == 128
|
46
|
-
raise InvalidSelectionError, "Specified --branch could not be found"
|
47
|
-
end
|
45
|
+
raise InvalidSelectionError, "Specified --branch could not be found" if result.exitstatus == 128
|
48
46
|
|
49
47
|
diff.split("\n").map { |f| File.join(@root_dir, f) }
|
50
48
|
end
|
51
49
|
end
|
52
50
|
|
53
51
|
def assert_valid_result_choice
|
54
|
-
|
52
|
+
return if %w[test full name json].include?(@results)
|
53
|
+
|
54
|
+
raise InvalidSelectionError, "--results must be 'test', 'full', 'name' or 'json'"
|
55
55
|
end
|
56
56
|
|
57
57
|
def selected_result?(result)
|
@@ -85,8 +85,8 @@ module CobraCommander
|
|
85
85
|
end
|
86
86
|
end
|
87
87
|
|
88
|
-
def display(
|
89
|
-
"#{
|
88
|
+
def display(name:, type:, **)
|
89
|
+
"#{name} - #{type}"
|
90
90
|
end
|
91
91
|
|
92
92
|
def blank_line
|
data/lib/cobra_commander/cli.rb
CHANGED
@@ -3,103 +3,93 @@
|
|
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_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 => e
|
62
|
+
error e.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
|
+
|
87
|
+
component = find_component(component_name)
|
88
|
+
|
89
|
+
return component.deep_dependencies if options.dependencies
|
90
|
+
return component.deep_dependents if options.dependents
|
91
|
+
|
92
|
+
[component]
|
103
93
|
end
|
104
94
|
end
|
105
95
|
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, :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,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler"
|
4
|
+
require "bundler/lockfile_parser"
|
5
|
+
require "pathname"
|
6
|
+
|
7
|
+
module CobraCommander
|
8
|
+
module Dependencies
|
9
|
+
# Calculates ruby bundler dependencies
|
10
|
+
class Bundler
|
11
|
+
attr_reader :path
|
12
|
+
|
13
|
+
def initialize(root)
|
14
|
+
@root = root
|
15
|
+
@path = Pathname.new(File.join(root, "Gemfile.lock")).realpath
|
16
|
+
end
|
17
|
+
|
18
|
+
def dependencies
|
19
|
+
lockfile.dependencies.values.map(&:name)
|
20
|
+
end
|
21
|
+
|
22
|
+
def components
|
23
|
+
components_source.specs.map do |spec|
|
24
|
+
{ path: spec.loaded_from, name: spec.name, dependencies: spec.dependencies.map(&:name) }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def lockfile
|
31
|
+
@lockfile ||= ::Bundler::LockfileParser.new(::Bundler.read_file(path))
|
32
|
+
end
|
33
|
+
|
34
|
+
def components_source
|
35
|
+
@components_source ||= begin
|
36
|
+
source = @lockfile.sources.find { |s| s.path.to_s.eql?("components") }
|
37
|
+
::Bundler::Source::Path.new(source.options.merge("root_path" => @root))
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "pathname"
|
5
|
+
|
6
|
+
module CobraCommander
|
7
|
+
module Dependencies
|
8
|
+
module Yarn
|
9
|
+
# Represents an Yarn package.json file
|
10
|
+
class Package
|
11
|
+
attr_reader :path
|
12
|
+
|
13
|
+
def initialize(path)
|
14
|
+
@path = ::Pathname.new(File.join(path, "package.json")).realpath
|
15
|
+
end
|
16
|
+
|
17
|
+
def project_tag
|
18
|
+
name.match(%r{^@[\w-]+/}).to_s
|
19
|
+
end
|
20
|
+
|
21
|
+
def name
|
22
|
+
json["name"]
|
23
|
+
end
|
24
|
+
|
25
|
+
def dependencies
|
26
|
+
json.fetch("dependencies", {})
|
27
|
+
.merge(json.fetch("devDependencies", {}))
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def json
|
33
|
+
@json ||= JSON.parse(File.read(@path))
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,32 @@
|
|
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.each_value do |spec|
|
18
|
+
next unless spec =~ /link:(.+)/
|
19
|
+
|
20
|
+
load_spec(File.join(package.path, "..", Regexp.last_match(1)))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def load_spec(path)
|
25
|
+
@specs[path] ||= Package.new(path).tap do |package|
|
26
|
+
load_linked_specs(package)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
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
|