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,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitCommander
4
+ class Command
5
+ # @abstract Wraps [Command] arguments, flags, and switches in a generic
6
+ # object to normalize their representation in the context of a
7
+ # [Command].
8
+ class Option
9
+ attr_reader :default, :description, :name
10
+ attr_writer :value
11
+
12
+ # Creates a [Option] object.
13
+ #
14
+ # @param name [String, Symbol] the name of the option, these are unique per [Command]
15
+ # @param default [anything] the default value the option should have
16
+ # @param description [String] a description of the option for display in
17
+ # the [Command]'s help text
18
+ # @param value [anything] a value for the option
19
+ def initialize(name:, default: nil, description: nil, value: nil)
20
+ @name = name.to_sym
21
+ @default = default
22
+ @description = description
23
+ @value = value
24
+ end
25
+
26
+ def value
27
+ @value || @default
28
+ end
29
+
30
+ def ==(other)
31
+ other.class == self.class &&
32
+ other.name == name &&
33
+ other.default == default &&
34
+ other.description == description
35
+ end
36
+ alias eql? ==
37
+
38
+ def to_h
39
+ { name => value }
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitCommander
4
+ class Command
5
+ # @abstract A container to execute blocks defined in command definitions
6
+ #
7
+ # Command @block will be executed in this class' context and methods will be
8
+ # delegated based on methods defined here, or in plugins.
9
+ class Runner
10
+ attr_reader :command
11
+
12
+ undef :system
13
+
14
+ def initialize(command)
15
+ @command = command
16
+ end
17
+
18
+ def run(options = {})
19
+ GitCommander.logger.info "Running '#{command.name}' with arguments: #{options.inspect}"
20
+ instance_exec(options, &command.block)
21
+ end
22
+
23
+ def say(message)
24
+ command.say message
25
+ end
26
+
27
+ def respond_to_missing?(method_sym, include_all = false)
28
+ plugin_executor(method_sym).respond_to?(method_sym, include_all) ||
29
+ super(method_sym, include_all)
30
+ end
31
+
32
+ def method_missing(method_sym, *arguments, &block)
33
+ return plugin_executor(method_sym) if plugin_executor(method_sym)
34
+
35
+ super
36
+ end
37
+
38
+ private
39
+
40
+ def plugin_executor(plugin_name)
41
+ @plugin_executor ||= command.registry.find_plugin(plugin_name)&.executor
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitCommander
4
+ # Establishes values to be set by loaders
5
+ module CommandLoaderOptions
6
+ def summary(value = nil)
7
+ return @summary = value if value
8
+
9
+ @summary
10
+ end
11
+
12
+ def description(value = nil)
13
+ return @description = value if value
14
+
15
+ @description
16
+ end
17
+
18
+ def argument(arg_name, options = {})
19
+ add_option :argument, options.merge(name: arg_name)
20
+ end
21
+
22
+ def flag(flag_name, options = {})
23
+ add_option :flag, options.merge(name: flag_name)
24
+ end
25
+
26
+ def switch(switch_name, options = {})
27
+ add_option :switch, options.merge(name: switch_name)
28
+ end
29
+
30
+ def on_run(&on_run)
31
+ @block = on_run
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/inline"
4
+ require_relative "loader_result"
5
+
6
+ module GitCommander
7
+ # @abstract The interface class outlining requirements for an operational Loader
8
+ class Loader
9
+ # Let the loaders proxy system calls in the git-commander context
10
+ undef :system
11
+
12
+ attr_reader :registry, :result
13
+
14
+ def initialize(registry)
15
+ @registry = registry
16
+ @result = LoaderResult.new
17
+ end
18
+
19
+ # Expected to return an instance of GitCommander::LoaderResult
20
+ def load(_options = {})
21
+ raise NotImplementedError
22
+ end
23
+
24
+ def system
25
+ GitCommander::System
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitCommander
4
+ # @abstract A simple object to wrap errors loading any given loader
5
+ class LoaderResult
6
+ attr_accessor :commands, :plugins, :errors
7
+
8
+ def initialize
9
+ @errors = []
10
+ @commands = []
11
+ @plugins = []
12
+ end
13
+
14
+ def success?
15
+ Array(errors).empty?
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+
5
+ module GitCommander
6
+ # Handles logging for GitCommander
7
+ class Logger < ::Logger
8
+ DEFAULT_LOG_FILE = "/tmp/git-commander.log"
9
+
10
+ def initialize(*args)
11
+ log_file = args.shift || log_file_path
12
+ args.unshift(log_file)
13
+ super(*args)
14
+ @formatter = SimpleFormatter.new
15
+ end
16
+
17
+ # Simple formatter which only displays the message.
18
+ class SimpleFormatter < ::Logger::Formatter
19
+ # This method is invoked when a log event occurs
20
+ def call(severity, _timestamp, _progname, msg)
21
+ "#{severity}: #{String === msg ? msg : msg.inspect}\n"
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def log_file_path
28
+ return @log_file_path unless @log_file_path.to_s.empty?
29
+
30
+ # Here we have to run the command in isolation to avoid a recursive loop
31
+ # to log this command run to fetch the config setting.
32
+ configured_log_file_path = `git config --get commander.log-file-path`
33
+
34
+ return @log_file_path = DEFAULT_LOG_FILE if configured_log_file_path.empty?
35
+
36
+ @log_file_path = configured_log_file_path
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "plugin/executor"
4
+
5
+ module GitCommander
6
+ #
7
+ # @abstract Allows for proxying methods to a plugin from within the context of
8
+ # a Command's block.
9
+ #
10
+ # A Plugin provides additional external instances to a Command's @block
11
+ # context. Plugins can define their own inline gems, and can define
12
+ # additional Commands.
13
+ #
14
+ # @example A simple `git` plugin
15
+ # require "git"
16
+ # git_instance = Git.open(Dir.pwd, log: GitCommander.logger)
17
+ # GitCommander::Plugin.new(:git, source_instance: git_instance)
18
+ #
19
+ class Plugin
20
+ class CommandNotFound < StandardError; end
21
+
22
+ attr_accessor :commands, :executor, :name, :registry
23
+
24
+ # Creates a Plugin object. +name+ is the name of the plugin.
25
+ #
26
+ # Options include:
27
+ #
28
+ # +source_instance+ - an instance of an object to use in the Command's block context
29
+ # +registry+ - a Registry instance for where this Plugin will be stored for lookup
30
+ def initialize(name, source_instance: nil, registry: nil)
31
+ @name = name
32
+ @executor = Executor.new(source_instance) if source_instance
33
+ @registry = registry || GitCommander::Registry.new
34
+ end
35
+
36
+ def find_command(command_name)
37
+ GitCommander.logger.debug "[#{logger_tag}] looking up command: #{command_name.inspect}"
38
+ command = commands[command_name.to_s.to_sym]
39
+ raise CommandNotFound, "[#{logger_tag}] #{command_name} does not exist for this plugin" if command.nil?
40
+
41
+ command
42
+ end
43
+
44
+ private
45
+
46
+ def logger_tag
47
+ [name, "plugin"].compact.join(" ")
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "delegate"
4
+
5
+ module GitCommander
6
+ class Plugin
7
+ class Executor < SimpleDelegator
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../command/configurator"
4
+ require_relative "../loader"
5
+
6
+ module GitCommander
7
+ class Plugin
8
+ # @abstract Handles loading native plugins by name.
9
+ class Loader < ::GitCommander::Loader
10
+ class NotFoundError < StandardError; end
11
+ class LoadError < StandardError; end
12
+
13
+ NATIVE_PLUGIN_DIR = File.expand_path(File.join(__dir__, "..", "plugins"))
14
+
15
+ attr_reader :content, :commands, :name
16
+
17
+ def initialize(registry)
18
+ @commands = []
19
+ super
20
+ end
21
+
22
+ def load(name)
23
+ @plugin = GitCommander::Plugin.new(
24
+ resolve_plugin_name(name),
25
+ source_instance: instance_eval(resolve_content(name))
26
+ )
27
+ @plugin.commands = @commands
28
+ result.plugins << @plugin
29
+ result.commands |= @commands
30
+ result
31
+ rescue Errno::ENOENT, Errno::EACCES => e
32
+ handle_error LoadError, e
33
+ rescue StandardError => e
34
+ handle_error NotFoundError, e
35
+ end
36
+
37
+ def resolve_plugin_name(native_name_or_filename)
38
+ return @name = native_name_or_filename if native_name_or_filename.is_a? Symbol
39
+
40
+ @name = File.basename(native_name_or_filename).split(".").first.to_sym
41
+ end
42
+
43
+ def resolve_content(native_name_or_filename)
44
+ if native_name_or_filename.is_a? Symbol
45
+ return @content = File.read("#{NATIVE_PLUGIN_DIR}/#{native_name_or_filename}.rb")
46
+ end
47
+
48
+ @content = File.read(native_name_or_filename)
49
+ end
50
+
51
+ def command(name, &block)
52
+ GitCommander.logger.debug("Loading command :#{name} from plugin #{@name}")
53
+ @commands << Command::Configurator.new(registry).configure("#{plugin_name_formatted_for_cli}:#{name}".to_sym, &block)
54
+ rescue Command::Configurator::ConfigurationError => e
55
+ result.errors << e
56
+ end
57
+
58
+ def plugin(name, **options)
59
+ plugin_result = GitCommander::Plugin::Loader.new(registry).load(name, **options)
60
+ result.plugins |= plugin_result.plugins
61
+ end
62
+
63
+ private
64
+
65
+ def plugin_name_formatted_for_cli
66
+ @name.to_s.gsub("_", "-").to_sym
67
+ end
68
+
69
+ def handle_error(error_klass, original_error)
70
+ error = error_klass.new(original_error.message)
71
+ error.set_backtrace original_error.backtrace
72
+ @result.errors << error
73
+ @result
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ gemfile do
4
+ source "https://rubygems.org"
5
+ gem "rugged"
6
+ end
7
+
8
+ CONFIG_FILE_PATH = "#{ENV["HOME"]}/.gitconfig.commander"
9
+
10
+ # @private
11
+ # Overrides Rugged::Repository#global_config so that we can use a custom config
12
+ # file for all git-commander related configurations
13
+ class RuggedRepositoryWithCustomConfig < SimpleDelegator
14
+ attr_reader :global_config
15
+
16
+ def initialize(repository)
17
+ @global_config = Rugged::Config.new(CONFIG_FILE_PATH)
18
+ super repository
19
+ end
20
+ end
21
+
22
+ unless File.exist?(CONFIG_FILE_PATH)
23
+ system.run "touch #{CONFIG_FILE_PATH}"
24
+ system.say "Created #{CONFIG_FILE_PATH} for git-commander specific configurations."
25
+ system.run "git config --global --add include.path \"#{CONFIG_FILE_PATH}\""
26
+ system.say "Added #{CONFIG_FILE_PATH} to include.path in $HOME/.gitconfig"
27
+ end
28
+
29
+ RuggedRepositoryWithCustomConfig.new(
30
+ Rugged::Repository.new(Dir.pwd)
31
+ )
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ gemfile do
4
+ source "https://rubygems.org"
5
+ gem "octokit"
6
+ end
7
+
8
+ plugin :git
9
+ plugin :prompt
10
+
11
+ command :setup do |cmd|
12
+ cmd.summary "Connects to GitHub, creates an access token, and stores it in the git-cmd section of your git config"
13
+
14
+ cmd.on_run do
15
+ gh_user = prompt.ask("Please enter your GitHub username", required: true)
16
+ gh_password = promt.mask("Please enter your GitHub password (this is NOT stored): ", required: true)
17
+
18
+ github.login = gh_user
19
+ github.password = gh_password
20
+
21
+ # Check for 2-factor requirements
22
+ begin
23
+ github.user
24
+ rescue Octokit::Unauthorized
25
+ github.user(
26
+ gh_user,
27
+ headers: { "X-GitHub-OTP" => prompt.ask("Please enter your two-factor authentication code") }
28
+ )
29
+ end
30
+
31
+ say "GitHub account successfully setup!"
32
+ end
33
+ end
34
+
35
+ Octokit::Client.new
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ gemfile do
4
+ source "https://rubygems.org"
5
+ gem "tty-prompt"
6
+ end
7
+
8
+ TTY::Prompt.new
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ GitCommander::System
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "loader"
4
+ require_relative "plugin/loader"
5
+ require_relative "command/loaders/file_loader"
6
+ require_relative "command/loaders/raw"
7
+
8
+ module GitCommander
9
+ # @abstract Manages available GitCommander commands
10
+ class Registry
11
+ class CommandNotFound < StandardError; end
12
+ class LoadError < StandardError; end
13
+
14
+ attr_accessor :commands, :name, :plugins
15
+
16
+ def initialize
17
+ @commands = {}
18
+ @plugins = {}
19
+ end
20
+
21
+ # Adds a command to the registry
22
+ #
23
+ # @param [String, Symbol] command_name the name of the command to add to the
24
+ # registry
25
+ def register(command_name, **options, &block)
26
+ command = GitCommander::Command.new(command_name.to_sym, registry: self, **options.merge(block: block))
27
+ register_command(command)
28
+ end
29
+
30
+ # Adds a pre-built command to the registry
31
+ # @param [Command] command the Command instance to add to the registry
32
+ def register_command(command)
33
+ GitCommander.logger.debug "[#{logger_tag}] Registering command `#{command.name}` with args: #{command.inspect}..."
34
+
35
+ commands[command.name] = command
36
+ end
37
+
38
+ # Adds a pre-built Plugin to the registry
39
+ # @param [Plugin] plugin the Plugin instance to add to the registry
40
+ def register_plugin(plugin)
41
+ GitCommander.logger.debug "[#{logger_tag}] Registering plugin `#{plugin.name}`..."
42
+
43
+ plugins[plugin.name] = plugin
44
+ end
45
+
46
+ # Adds command(s) to the registry using the given loader
47
+ #
48
+ # @param [CommandLoader] loader the class to use to load with
49
+ def load(loader, *args)
50
+ result = loader.new(self).load(*args)
51
+
52
+ if result.success?
53
+ result.plugins.each { |plugin| register_plugin(plugin) }
54
+ result.commands.each { |cmd| register_command(cmd) }
55
+ end
56
+
57
+ result
58
+ end
59
+
60
+ # Looks up a command in the registry
61
+ #
62
+ # @param [String, Symbol] command_name the name of the command to look up in the
63
+ # registry
64
+ #
65
+ # @example Fetch a command from the registry
66
+ # registry = GitCommander::Registry.new
67
+ # registry.register :wtf
68
+ # registry.find :wtf
69
+ #
70
+ # @raise [CommandNotFound] when no command is found in the registry
71
+ # @return [GitCommander::Command, #run] a command object that responds to #run
72
+ def find(command_name)
73
+ GitCommander.logger.debug "[#{logger_tag}] looking up command: #{command_name.inspect}"
74
+ command = commands[command_name.to_s.to_sym]
75
+ raise CommandNotFound, "[#{logger_tag}] #{command_name} does not exist in the registry" if command.nil?
76
+
77
+ command
78
+ end
79
+
80
+ def find_plugin(plugin_name)
81
+ GitCommander.logger.debug "[#{logger_tag}] looking up plugin: #{plugin_name.inspect}"
82
+ plugins[plugin_name.to_s.to_sym]
83
+ end
84
+
85
+ private
86
+
87
+ def logger_tag
88
+ [name, "registry"].compact.join(" ")
89
+ end
90
+ end
91
+ end