git-commander 0.1.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.
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