git-commander 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.rubocop.yml +38 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/.yardopts +1 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +39 -0
- data/LICENSE.txt +21 -0
- data/README.md +196 -0
- data/Rakefile +7 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/git-cmd +35 -0
- data/git-commander.gemspec +31 -0
- data/lib/git_commander.rb +17 -0
- data/lib/git_commander/cli.rb +126 -0
- data/lib/git_commander/command.rb +168 -0
- data/lib/git_commander/command/configurator.rb +26 -0
- data/lib/git_commander/command/loaders/file_loader.rb +35 -0
- data/lib/git_commander/command/loaders/raw.rb +43 -0
- data/lib/git_commander/command/option.rb +43 -0
- data/lib/git_commander/command/runner.rb +45 -0
- data/lib/git_commander/command_loader_options.rb +34 -0
- data/lib/git_commander/loader.rb +28 -0
- data/lib/git_commander/loader_result.rb +18 -0
- data/lib/git_commander/logger.rb +39 -0
- data/lib/git_commander/plugin.rb +50 -0
- data/lib/git_commander/plugin/executor.rb +10 -0
- data/lib/git_commander/plugin/loader.rb +77 -0
- data/lib/git_commander/plugins/git.rb +31 -0
- data/lib/git_commander/plugins/github.rb +35 -0
- data/lib/git_commander/plugins/prompt.rb +8 -0
- data/lib/git_commander/plugins/system.rb +3 -0
- data/lib/git_commander/registry.rb +91 -0
- data/lib/git_commander/rspec.rb +3 -0
- data/lib/git_commander/rspec/plugin_helpers.rb +82 -0
- data/lib/git_commander/system.rb +76 -0
- data/lib/git_commander/version.rb +5 -0
- metadata +159 -0
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/git_commander/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "git-commander"
|
7
|
+
spec.version = GitCommander::VERSION
|
8
|
+
spec.authors = ["Valentino Stoll"]
|
9
|
+
spec.email = ["v@codenamev.com"]
|
10
|
+
|
11
|
+
spec.summary = "Make your own git commands"
|
12
|
+
spec.description = "Build custom flexible git workflows with Ruby!"
|
13
|
+
spec.homepage = "https://github.com/codenamev/git-commander"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
# Specify which files should be added to the gem when it is released.
|
17
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
18
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
19
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
20
|
+
end
|
21
|
+
spec.bindir = "exe"
|
22
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
23
|
+
spec.require_paths = ["lib"]
|
24
|
+
|
25
|
+
spec.add_development_dependency "byebug", "~> 11"
|
26
|
+
spec.add_development_dependency "rake", "~> 12.3"
|
27
|
+
spec.add_development_dependency "rdoc", "~> 6.2"
|
28
|
+
spec.add_development_dependency "rspec", "< 4.0"
|
29
|
+
|
30
|
+
spec.add_dependency "bundler", "~> 2.1", ">= 1.10.0"
|
31
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "git_commander/command"
|
4
|
+
require "git_commander/logger"
|
5
|
+
require "git_commander/plugin"
|
6
|
+
require "git_commander/registry"
|
7
|
+
require "git_commander/system"
|
8
|
+
require "git_commander/version"
|
9
|
+
|
10
|
+
# GitCommander is the global module used to house the global logger
|
11
|
+
module GitCommander
|
12
|
+
module_function
|
13
|
+
|
14
|
+
def logger(*args)
|
15
|
+
@logger ||= GitCommander::Logger.new(*args)
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "registry"
|
4
|
+
require_relative "version"
|
5
|
+
require "optparse"
|
6
|
+
|
7
|
+
module GitCommander
|
8
|
+
# Manages mapping commands and their arguments that are run from the command-line (via `git-cmd`)
|
9
|
+
# to their corresponding git-commander registered commands.
|
10
|
+
#
|
11
|
+
# @example Run a registered "start" command with a single argument
|
12
|
+
# GitCommander::CLI.new.run ["start", "new-feature""]
|
13
|
+
#
|
14
|
+
class CLI
|
15
|
+
attr_reader :output, :registry
|
16
|
+
|
17
|
+
# @param registry [GitCommander::Registry] (GitCommander::Registry.new) the
|
18
|
+
# command registry to use for matching available commands
|
19
|
+
# @param output [IO] (STDOUT) the IO object you want to use to send output to when running commands
|
20
|
+
def initialize(registry: GitCommander::Registry.new, output: STDOUT)
|
21
|
+
@registry = registry
|
22
|
+
@output = output
|
23
|
+
end
|
24
|
+
|
25
|
+
# Runs a GitCommander command
|
26
|
+
#
|
27
|
+
# @param args [Array] (ARGV) a list of arguments to pass to the registered command
|
28
|
+
def run(args = ARGV)
|
29
|
+
arguments = Array(args)
|
30
|
+
command = registry.find arguments.shift
|
31
|
+
options = parse_command_options!(command, arguments)
|
32
|
+
command.run options
|
33
|
+
rescue Registry::CommandNotFound
|
34
|
+
log_command_not_found(command)
|
35
|
+
|
36
|
+
help
|
37
|
+
rescue StandardError => e
|
38
|
+
say e.message
|
39
|
+
say e.backtrace
|
40
|
+
end
|
41
|
+
|
42
|
+
def say(message)
|
43
|
+
output.puts message
|
44
|
+
end
|
45
|
+
|
46
|
+
# Parses an array of values (as ARGV would provide) for the provided git-cmd command name.
|
47
|
+
# The +arguments+ are run through Ruby's [OptionParser] for validation and
|
48
|
+
# then filtered through the +command+ to extract it's options with any
|
49
|
+
# default values.
|
50
|
+
#
|
51
|
+
# @param command [Command] the git-cmd command to parse the arguments for
|
52
|
+
# @param arguments [Array] the command line arguments
|
53
|
+
# @return [Array<GitCommander::Command::Option>] the available options with values
|
54
|
+
def parse_command_options!(command, arguments)
|
55
|
+
parser = configure_option_parser_for_command(command)
|
56
|
+
parser.parse!(arguments)
|
57
|
+
|
58
|
+
# Add arguments to options to pass to defined commands
|
59
|
+
command.arguments.each do |argument|
|
60
|
+
argument.value = arguments.shift
|
61
|
+
end
|
62
|
+
|
63
|
+
command.options
|
64
|
+
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
|
65
|
+
command.help
|
66
|
+
GitCommander.logger.debug "[CLI] Failed to parse command line options – #{e.inspect}"
|
67
|
+
exit 1
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def help
|
73
|
+
say "NAME"
|
74
|
+
say " git-cmd – Git Commander allows running custom git commands from a centralized location."
|
75
|
+
say "VERSION"
|
76
|
+
say " #{GitCommander::VERSION}"
|
77
|
+
say "USAGE"
|
78
|
+
say " git-cmd command [command options] [arguments...]"
|
79
|
+
say "COMMANDS"
|
80
|
+
say registry.commands.keys.join(", ")
|
81
|
+
end
|
82
|
+
|
83
|
+
def log_command_not_found(command)
|
84
|
+
GitCommander.logger.error <<~ERROR_LOG
|
85
|
+
#{command} not found in registry. Available commands: #{registry.commands.keys.inspect}
|
86
|
+
ERROR_LOG
|
87
|
+
end
|
88
|
+
|
89
|
+
def configure_option_parser_for_command(command)
|
90
|
+
valid_arguments_for_command = command.arguments.map { |arg| "[#{arg.name}]" }.join(" ")
|
91
|
+
|
92
|
+
OptionParser.new do |opts|
|
93
|
+
opts.banner = "USAGE:\n git-cmd #{command.name} [command options] #{valid_arguments_for_command}"
|
94
|
+
opts.separator ""
|
95
|
+
opts.separator "COMMAND OPTIONS:" if command.flags.any? || command.switches.any?
|
96
|
+
|
97
|
+
configure_flags_for_option_parser_and_command(opts, command)
|
98
|
+
configure_switches_for_option_parser_and_command(opts, command)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def configure_flags_for_option_parser_and_command(option_parser, command)
|
103
|
+
command.flags.each do |flag|
|
104
|
+
option_parser.on("-#{flag.name[0]}", command_line_flag_formatted_name(flag), flag.description.to_s) do |f|
|
105
|
+
flag.value = f
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def configure_switches_for_option_parser_and_command(option_parser, command)
|
111
|
+
command.switches.each do |switch|
|
112
|
+
option_parser.on("-#{switch.name[0]}", "--[no-]#{switch.name}", switch.description) do |s|
|
113
|
+
switch.value = s
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def command_line_flag_formatted_name(flag)
|
119
|
+
"--#{underscore_to_kebab(flag.name)} #{flag.name.upcase}"
|
120
|
+
end
|
121
|
+
|
122
|
+
def underscore_to_kebab(sym_or_string)
|
123
|
+
sym_or_string.to_s.gsub("_", "-").to_sym
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
require_relative "command/configurator"
|
5
|
+
require_relative "command/option"
|
6
|
+
require_relative "command/runner"
|
7
|
+
require_relative "command_loader_options"
|
8
|
+
|
9
|
+
module GitCommander
|
10
|
+
# Wraps domain logic for executing git-cmd Commands
|
11
|
+
class Command
|
12
|
+
include GitCommander::CommandLoaderOptions
|
13
|
+
|
14
|
+
attr_reader :arguments, :flags, :switches, :block, :name, :registry
|
15
|
+
attr_accessor :output
|
16
|
+
|
17
|
+
# @param name [String, Symbol] the name of the command
|
18
|
+
# @param registry [GitCommander::Registry] (GitCommander::Registry.new) the
|
19
|
+
# command registry to use lookups
|
20
|
+
# @param options [Hash] the options to create the command with
|
21
|
+
#
|
22
|
+
# @option options [String] :description (nil) a short description to use in the
|
23
|
+
# single line version of the command's help output
|
24
|
+
# @option options [String] :summary (nil) the long-form description of the command
|
25
|
+
# to use in the command's help output
|
26
|
+
# @option options [IO] :output (STDOUT) the IO object you want to use to
|
27
|
+
# send outut from the command to
|
28
|
+
# @option options [Array] :arguments an array of hashes describing the
|
29
|
+
# argument names and default values that can be supplied to the command
|
30
|
+
# @option options [Array] :flags an array of hashes describing the
|
31
|
+
# flags and default values that can be supplied to the command
|
32
|
+
# @option options [Array] :switches an array of hashes describing the
|
33
|
+
# switches and default values that can be supplied to the command
|
34
|
+
#
|
35
|
+
# @yieldparam [Array<Option>] run_options an Array of
|
36
|
+
# Option instances defined from the above +options+
|
37
|
+
#
|
38
|
+
def initialize(name, registry: nil, **options, &block)
|
39
|
+
@name = name
|
40
|
+
@description = options[:description]
|
41
|
+
@summary = options[:summary]
|
42
|
+
@block = block_given? ? block : proc {}
|
43
|
+
@registry = registry || GitCommander::Registry.new
|
44
|
+
@output = options[:output] || STDOUT
|
45
|
+
|
46
|
+
define_command_options(options)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Executes the block for the command with the provided run_options.
|
50
|
+
#
|
51
|
+
# @param run_options [Array<Option>] an array of Option(s) to pass to the {#block} of this Command
|
52
|
+
#
|
53
|
+
def run(run_options = [])
|
54
|
+
assign_option_values(run_options)
|
55
|
+
Runner.new(self).run options.map(&:to_h).reduce(:merge)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Appends the +message+ to the Command's {#output}
|
59
|
+
#
|
60
|
+
# @param message [String] the string to append to the {#output}
|
61
|
+
#
|
62
|
+
def say(message)
|
63
|
+
output.puts message
|
64
|
+
end
|
65
|
+
|
66
|
+
# Adds command-line help text to the {#output} of this Command
|
67
|
+
#
|
68
|
+
def help
|
69
|
+
say "NAME"
|
70
|
+
say " git-cmd #{name} – #{summary}"
|
71
|
+
say "USAGE"
|
72
|
+
say " git-cmd #{name} [command options] #{arguments.map { |arg| "[#{arg.name}]" }.join(" ")}"
|
73
|
+
description_help
|
74
|
+
argument_help
|
75
|
+
options_help
|
76
|
+
end
|
77
|
+
|
78
|
+
# Access to a unique Set of this Command's {#arguments}, {#flags}, and {#switches}
|
79
|
+
#
|
80
|
+
# @return [Set] a unique list of all options this command can accept
|
81
|
+
#
|
82
|
+
def options
|
83
|
+
Set.new(@arguments + @flags + @switches)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Add to this Command's {#arguments}, {#flags}, or {#switches}
|
87
|
+
#
|
88
|
+
# @param option_type [String, Symbol] the type of option to add
|
89
|
+
# @param options [Hash] the options to create the [Option] with
|
90
|
+
#
|
91
|
+
# @option options [String, Symbol] :name the name of the option to add
|
92
|
+
# @option options [Object] :default (nil) the default value of the Option
|
93
|
+
# @option options [String] :description (nil) a description of the Option to use in
|
94
|
+
# help text output
|
95
|
+
# @option options [Object] :value (nil) the value on of the Option
|
96
|
+
#
|
97
|
+
def add_option(option_type, options = {})
|
98
|
+
case option_type.to_sym
|
99
|
+
when :argument
|
100
|
+
@arguments << Option.new(**options)
|
101
|
+
when :flag
|
102
|
+
@flags << Option.new(**options)
|
103
|
+
when :switch
|
104
|
+
@switches << Option.new(**options)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def define_command_options(options)
|
111
|
+
@arguments = options_from_hash(options[:arguments])
|
112
|
+
@flags = options_from_hash(options[:flags])
|
113
|
+
@switches = options_from_hash(options[:switches])
|
114
|
+
end
|
115
|
+
|
116
|
+
def options_from_hash(hash)
|
117
|
+
Array(hash).map { |v| Option.new(**v) }
|
118
|
+
end
|
119
|
+
|
120
|
+
def assign_option_values(command_options)
|
121
|
+
options.each do |option|
|
122
|
+
command_option = command_options.find { |o| o.name == option.name }
|
123
|
+
next if command_option.nil?
|
124
|
+
|
125
|
+
option.value = command_option.value
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def description_help
|
130
|
+
return if description.to_s.empty?
|
131
|
+
|
132
|
+
say "DESCRIPTION"
|
133
|
+
say " #{description}"
|
134
|
+
end
|
135
|
+
|
136
|
+
def argument_help
|
137
|
+
return unless arguments.any?
|
138
|
+
|
139
|
+
say "ARGUMENTS"
|
140
|
+
arguments.each do |argument|
|
141
|
+
default_text = argument.default.nil? ? "" : "(default: #{argument.default.inspect}) "
|
142
|
+
say " #{argument.name} – #{default_text}#{argument.description}"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def options_help
|
147
|
+
return unless flags.any? || switches.any?
|
148
|
+
|
149
|
+
say "COMMAND OPTIONS"
|
150
|
+
flag_help
|
151
|
+
switch_help
|
152
|
+
end
|
153
|
+
|
154
|
+
def flag_help
|
155
|
+
flags.each do |flag|
|
156
|
+
flag_names = ["-#{flag.name.to_s[0]}", "--#{flag.name}"]
|
157
|
+
say " #{flag_names} – #{flag.default.nil? ? "" : "(default: #{flag.default}) "}#{flag.description}"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def switch_help
|
162
|
+
switches.each do |switch|
|
163
|
+
switch_names = [switch.name.to_s[0], "-#{switch.name}"].map { |s| "-#{s}" }.join(", ")
|
164
|
+
say " #{switch_names} – #{switch.default.nil? ? "" : "(default: #{switch.default}) "}#{switch.description}"
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GitCommander
|
4
|
+
class Command
|
5
|
+
# Allows configuring a [GitCommander::Command] with a block
|
6
|
+
class Configurator
|
7
|
+
class ConfigurationError < StandardError; end
|
8
|
+
|
9
|
+
attr_reader :registry
|
10
|
+
|
11
|
+
def initialize(registry)
|
12
|
+
@registry = registry
|
13
|
+
end
|
14
|
+
|
15
|
+
def configure(name, &block)
|
16
|
+
new_command = GitCommander::Command.new(name, registry: registry)
|
17
|
+
new_command.instance_exec new_command, &block if block_given?
|
18
|
+
new_command
|
19
|
+
rescue StandardError => e
|
20
|
+
configuration_error = ConfigurationError.new(e.message)
|
21
|
+
configuration_error.set_backtrace e.backtrace
|
22
|
+
raise configuration_error
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../../loader"
|
4
|
+
|
5
|
+
module GitCommander
|
6
|
+
class Command
|
7
|
+
module Loaders
|
8
|
+
# @abstract Handles loading commands from file
|
9
|
+
class FileLoader < ::GitCommander::Loader
|
10
|
+
class FileNotFoundError < StandardError; end
|
11
|
+
class FileLoadError < StandardError; end
|
12
|
+
|
13
|
+
attr_reader :filename
|
14
|
+
|
15
|
+
def load(filename)
|
16
|
+
raw_loader = Raw.new(registry)
|
17
|
+
@result = raw_loader.load(File.read(filename))
|
18
|
+
rescue Errno::ENOENT => e
|
19
|
+
handle_error FileNotFoundError, e
|
20
|
+
rescue StandardError => e
|
21
|
+
handle_error FileLoadError, e
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def handle_error(error_klass, original_error)
|
27
|
+
error = error_klass.new(original_error.message)
|
28
|
+
error.set_backtrace original_error.backtrace
|
29
|
+
@result.errors << error
|
30
|
+
@result
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../configurator"
|
4
|
+
require_relative "../../loader"
|
5
|
+
require_relative "../../plugin/loader"
|
6
|
+
|
7
|
+
module GitCommander
|
8
|
+
class Command
|
9
|
+
module Loaders
|
10
|
+
# @abstract Handles loading commands from raw strings
|
11
|
+
class Raw < ::GitCommander::Loader
|
12
|
+
class CommandParseError < StandardError; end
|
13
|
+
|
14
|
+
attr_reader :content
|
15
|
+
|
16
|
+
def load(content = "")
|
17
|
+
@content = content
|
18
|
+
instance_eval @content
|
19
|
+
result
|
20
|
+
# In this case, since we're evaluating raw IO in the context of this
|
21
|
+
# instance, we need to catch a wider range of exceptions. Otherwise,
|
22
|
+
# syntax errors would blow this up.
|
23
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
24
|
+
parse_error = CommandParseError.new(e.message)
|
25
|
+
parse_error.set_backtrace e.backtrace
|
26
|
+
result.errors << parse_error
|
27
|
+
result
|
28
|
+
end
|
29
|
+
|
30
|
+
def command(name, &block)
|
31
|
+
result.commands << Configurator.new(registry).configure(name, &block)
|
32
|
+
rescue Configurator::ConfigurationError => e
|
33
|
+
result.errors << e
|
34
|
+
end
|
35
|
+
|
36
|
+
def plugin(name, **options)
|
37
|
+
plugin_result = GitCommander::Plugin::Loader.new(registry).load(name, **options)
|
38
|
+
result.plugins |= plugin_result.plugins
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|