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,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