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