git-commander 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.rubocop.yml +38 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +5 -0
  6. data/.yardopts +1 -0
  7. data/CODE_OF_CONDUCT.md +74 -0
  8. data/Gemfile +6 -0
  9. data/Gemfile.lock +39 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +196 -0
  12. data/Rakefile +7 -0
  13. data/bin/console +14 -0
  14. data/bin/setup +8 -0
  15. data/exe/git-cmd +35 -0
  16. data/git-commander.gemspec +31 -0
  17. data/lib/git_commander.rb +17 -0
  18. data/lib/git_commander/cli.rb +126 -0
  19. data/lib/git_commander/command.rb +168 -0
  20. data/lib/git_commander/command/configurator.rb +26 -0
  21. data/lib/git_commander/command/loaders/file_loader.rb +35 -0
  22. data/lib/git_commander/command/loaders/raw.rb +43 -0
  23. data/lib/git_commander/command/option.rb +43 -0
  24. data/lib/git_commander/command/runner.rb +45 -0
  25. data/lib/git_commander/command_loader_options.rb +34 -0
  26. data/lib/git_commander/loader.rb +28 -0
  27. data/lib/git_commander/loader_result.rb +18 -0
  28. data/lib/git_commander/logger.rb +39 -0
  29. data/lib/git_commander/plugin.rb +50 -0
  30. data/lib/git_commander/plugin/executor.rb +10 -0
  31. data/lib/git_commander/plugin/loader.rb +77 -0
  32. data/lib/git_commander/plugins/git.rb +31 -0
  33. data/lib/git_commander/plugins/github.rb +35 -0
  34. data/lib/git_commander/plugins/prompt.rb +8 -0
  35. data/lib/git_commander/plugins/system.rb +3 -0
  36. data/lib/git_commander/registry.rb +91 -0
  37. data/lib/git_commander/rspec.rb +3 -0
  38. data/lib/git_commander/rspec/plugin_helpers.rb +82 -0
  39. data/lib/git_commander/system.rb +76 -0
  40. data/lib/git_commander/version.rb +5 -0
  41. 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