skippy 0.2.0.a → 0.3.0.a

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 (75) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +1 -0
  3. data/.gitmodules +3 -0
  4. data/.idea/.rakeTasks +6 -6
  5. data/.idea/codeStyleSettings.xml +8 -8
  6. data/.idea/encodings.xml +5 -5
  7. data/.idea/inspectionProfiles/Project_Default.xml +7 -7
  8. data/.idea/misc.xml +3 -3
  9. data/.idea/modules.xml +7 -7
  10. data/.idea/skippy.iml +89 -82
  11. data/.idea/vcs.xml +5 -5
  12. data/.rubocop.yml +93 -0
  13. data/.rubocop_todo.yml +24 -0
  14. data/.vscode/launch.json +51 -61
  15. data/.vscode/settings.json +41 -2
  16. data/.vscode/tasks.json +16 -16
  17. data/Gemfile +11 -2
  18. data/README.md +194 -15
  19. data/Rakefile +1 -1
  20. data/app/boot.rb +1 -1
  21. data/app/commands/install.rb +41 -0
  22. data/app/commands/lib.rb +42 -2
  23. data/app/commands/new.rb +22 -8
  24. data/app/commands/template.rb +2 -2
  25. data/bin/rubocop +17 -0
  26. data/bin/ruby-parse +17 -0
  27. data/bin/ruby-rewrite +17 -0
  28. data/debug/skippy.bat +2 -0
  29. data/fixtures/my_lib/{src → modules}/command.rb +0 -0
  30. data/fixtures/my_lib/{src → modules}/geometry.rb +0 -0
  31. data/fixtures/my_lib/modules/gl.rb +4 -0
  32. data/fixtures/my_lib/modules/gl/container.rb +8 -0
  33. data/fixtures/my_lib/modules/gl/control.rb +6 -0
  34. data/fixtures/my_lib/modules/gl/nested/nested.rb +8 -0
  35. data/fixtures/my_lib/{src → modules}/tool.rb +0 -0
  36. data/fixtures/my_lib/skippy.json +1 -1
  37. data/fixtures/my_project/skippy.json +2 -1
  38. data/fixtures/my_project/src/hello_world.rb +2 -2
  39. data/fixtures/my_project/src/hello_world/extension.json +9 -9
  40. data/fixtures/project_with_lib/.skippy/libs/my-lib/modules/command.rb +4 -0
  41. data/fixtures/project_with_lib/.skippy/libs/my-lib/modules/gl.rb +4 -0
  42. data/fixtures/project_with_lib/.skippy/libs/my-lib/modules/gl/container.rb +8 -0
  43. data/fixtures/project_with_lib/.skippy/libs/my-lib/modules/gl/control.rb +6 -0
  44. data/fixtures/project_with_lib/.skippy/libs/my-lib/skippy.json +5 -0
  45. data/fixtures/project_with_lib/.skippy/libs/my-other-lib/modules/something.rb +4 -0
  46. data/fixtures/project_with_lib/.skippy/libs/my-other-lib/skippy.json +5 -0
  47. data/fixtures/project_with_lib/skippy.json +25 -0
  48. data/fixtures/project_with_lib/skippy/commands/example.rb +14 -0
  49. data/fixtures/project_with_lib/src/hello_world.rb +47 -0
  50. data/fixtures/project_with_lib/src/hello_world/extension.json +10 -0
  51. data/fixtures/project_with_lib/src/hello_world/main.rb +21 -0
  52. data/fixtures/project_with_lib/src/hello_world/vendor/my-lib/command.rb +4 -0
  53. data/fixtures/project_with_lib/src/hello_world/vendor/my-other-lib/something.rb +4 -0
  54. data/lib/skippy.rb +2 -0
  55. data/lib/skippy/app.rb +2 -2
  56. data/lib/skippy/cli.rb +41 -20
  57. data/lib/skippy/command.rb +2 -4
  58. data/lib/skippy/config.rb +27 -22
  59. data/lib/skippy/config_accessors.rb +12 -12
  60. data/lib/skippy/group.rb +1 -3
  61. data/lib/skippy/helpers/file.rb +3 -3
  62. data/lib/skippy/installer.rb +49 -0
  63. data/lib/skippy/installer/git.rb +115 -0
  64. data/lib/skippy/installer/local.rb +19 -0
  65. data/lib/skippy/lib_module.rb +16 -16
  66. data/lib/skippy/lib_source.rb +139 -0
  67. data/lib/skippy/library.rb +50 -10
  68. data/lib/skippy/library_manager.rb +116 -18
  69. data/lib/skippy/module_manager.rb +104 -26
  70. data/lib/skippy/namespace.rb +17 -1
  71. data/lib/skippy/project.rb +34 -4
  72. data/lib/skippy/version.rb +3 -1
  73. data/skippy.gemspec +10 -5
  74. metadata +85 -29
  75. data/cSpell.json +0 -18
data/lib/skippy/app.rb CHANGED
@@ -40,9 +40,9 @@ class Skippy::App
40
40
  def templates
41
41
  result = []
42
42
  templates_source_path.entries.each { |entry|
43
- template_path = templates_source_path.join(entry)
43
+ template_path = templates_source_path.join(entry)
44
44
  next unless template_path.directory?
45
- next if %w[. ..].include?(entry.basename.to_s)
45
+ next if %w(. ..).include?(entry.basename.to_s)
46
46
  result << entry.expand_path(templates_source_path)
47
47
  }
48
48
  result
data/lib/skippy/cli.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require 'pathname'
2
+
1
3
  require 'skippy/app'
2
4
  require 'skippy/command'
3
5
  require 'skippy/group'
@@ -41,14 +43,15 @@ class Skippy::CLI < Skippy::Command
41
43
  end
42
44
 
43
45
  # Verbatim copy from Thor::Runner:
44
- # Override Thor#help so it can give information about any class and any method.
46
+ # Override Thor#help so it can give information about any class and any
47
+ # method.
45
48
  #
46
49
  def help(meth = nil)
47
- if meth && !self.respond_to?(meth)
50
+ if meth && !respond_to?(meth)
48
51
  initialize_thorfiles(meth)
49
52
  klass, command = Thor::Util.find_class_and_command_by_namespace(meth)
50
53
  self.class.handle_no_command_error(command, false) if klass.nil?
51
- klass.start(['-h', command].compact, :shell => shell)
54
+ klass.start(['-h', command].compact, shell: shell)
52
55
  else
53
56
  super
54
57
  end
@@ -58,18 +61,22 @@ class Skippy::CLI < Skippy::Command
58
61
  # If a command is not found on Thor::Runner, method missing is invoked and
59
62
  # Thor::Runner is then responsible for finding the command in all classes.
60
63
  #
61
- def method_missing(meth, *args)
64
+ def method_missing(meth, *args) # rubocop:disable Style/MethodMissing
62
65
  meth = meth.to_s
63
66
  initialize_thorfiles(meth)
64
67
  klass, command = Thor::Util.find_class_and_command_by_namespace(meth)
65
68
  self.class.handle_no_command_error(command, false) if klass.nil?
66
69
  args.unshift(command) if command
67
- klass.start(args, :shell => shell)
70
+ klass.start(args, shell: shell)
68
71
  end
69
72
 
70
73
  # Verbatim copy from Thor::Runner:
71
- desc 'list [SEARCH]', "List the available #{$PROGRAM_NAME} commands (--substring means .*SEARCH)"
72
- method_options :substring => :boolean, :group => :string, :all => :boolean, :debug => :boolean
74
+ desc 'list [SEARCH]',
75
+ "List the available #{$PROGRAM_NAME} commands (--substring means .*SEARCH)"
76
+ method_options substring: :boolean,
77
+ group: :string,
78
+ all: :boolean,
79
+ debug: :boolean
73
80
  def list(search = '')
74
81
  initialize_thorfiles
75
82
 
@@ -77,15 +84,13 @@ class Skippy::CLI < Skippy::Command
77
84
  search = /^#{search}.*/i
78
85
  group = options[:group] || 'standard'
79
86
 
80
- klasses = Thor::Base.subclasses.select do |k|
87
+ klasses = Thor::Base.subclasses.select { |k|
81
88
  (options[:all] || k.group == group) && k.namespace =~ search
82
- end
89
+ }
83
90
 
84
91
  display_klasses(false, false, klasses)
85
92
  end
86
93
 
87
- private
88
-
89
94
  # Based on Thor::Runner, with exception of program name.
90
95
  def self.banner(command, all = false, subcommand = false)
91
96
  "#{$PROGRAM_NAME} " + command.formatted_usage(self, all, subcommand)
@@ -96,6 +101,8 @@ class Skippy::CLI < Skippy::Command
96
101
  true
97
102
  end
98
103
 
104
+ private
105
+
99
106
  # This is one of the places this runner differ from Thor::Runner. It will
100
107
  # instead load files for the current project.
101
108
  #
@@ -107,7 +114,16 @@ class Skippy::CLI < Skippy::Command
107
114
  return unless project.exist?
108
115
  project.command_files { |filename|
109
116
  unless Thor::Base.subclass_files.keys.include?(File.expand_path(filename))
110
- Thor::Util.load_thorfile(filename, nil, options[:debug])
117
+ begin
118
+ Thor::Util.load_thorfile(filename, nil, options[:debug])
119
+ rescue ScriptError, StandardError => error
120
+ command_path = Pathname.new(filename).relative_path_from(project.path)
121
+ say "Error loading: #{command_path} (#{error})", :red
122
+ if options[:debug]
123
+ say error.inspect, :red
124
+ say error.backtrace.join("\n"), :red
125
+ end
126
+ end
111
127
  end
112
128
  }
113
129
  end
@@ -120,15 +136,20 @@ class Skippy::CLI < Skippy::Command
120
136
  end
121
137
 
122
138
  # Based on Thor::Runner:
123
- def display_klasses(_with_modules = false, show_internal = false, klasses = Thor::Base.subclasses)
139
+ def display_klasses(_with_modules = false,
140
+ show_internal = false,
141
+ klasses = Thor::Base.subclasses)
142
+
124
143
  unless show_internal
125
144
  klasses -= [
126
145
  Thor, Thor::Runner, Thor::Group,
127
- Skippy, Skippy::CLI, Skippy::Command, Skippy::Command::Group
146
+ Skippy, Skippy::CLI, Skippy::Command, Skippy::Command::Group,
128
147
  ]
129
148
  end
130
149
 
131
- fail Error, "No #{$PROGRAM_NAME.capitalize} commands available" if klasses.empty?
150
+ if klasses.empty?
151
+ raise Error, "No #{$PROGRAM_NAME.capitalize} commands available"
152
+ end
132
153
 
133
154
  list = Hash.new { |h, k| h[k] = [] }
134
155
  groups = klasses.select { |k| k.ancestors.include?(Thor::Group) }
@@ -177,16 +198,16 @@ class Skippy::CLI < Skippy::Command
177
198
  # TODO(thomthom): Because of the odd issue with col_width mentioned in
178
199
  # `display_klasses` the table isn't truncated. Can probably re-enable if
179
200
  # the col_width issue is fixed.
180
- #print_table(list, :truncate => true, :indent => 2, :colwidth => col_width)
201
+ # print_table(list, :truncate => true, :indent => 2, :colwidth => col_width)
181
202
  width = (col_width + 2) * 2
182
- print_table(list, :indent => 2, :colwidth => width)
203
+ print_table(list, indent: 2, colwidth: width)
183
204
  end
184
- alias_method :display_tasks, :display_commands
205
+ alias display_tasks display_commands
185
206
 
186
207
  # Based on Thor::Runner, skipping the yaml stuff:
187
208
  def show_modules
188
- info = []
189
- labels = %w[Modules Namespaces]
209
+ info = []
210
+ labels = %w(Modules Namespaces)
190
211
 
191
212
  info << labels
192
213
  info << ['-' * labels[0].size, '-' * labels[1].size]
@@ -3,12 +3,10 @@ require 'thor'
3
3
  module Skippy
4
4
  class Command < Thor
5
5
 
6
- protected
7
-
8
6
  # Customize the banner as we don't care for the 'skippy' prefix for each
9
7
  # item in the list.
10
- def self.banner(command, namespace = nil, subcommand = false)
11
- "#{command.formatted_usage(self, true, subcommand)}"
8
+ def self.banner(command, _namespace = nil, subcommand = false)
9
+ command.formatted_usage(self, true, subcommand).to_s
12
10
  end
13
11
 
14
12
  end
data/lib/skippy/config.rb CHANGED
@@ -5,7 +5,7 @@ require 'skippy/error'
5
5
 
6
6
  class Skippy::Config < Hash
7
7
 
8
- attr_accessor :path
8
+ attr_reader :path
9
9
 
10
10
  class MissingPathError < Skippy::Error; end
11
11
 
@@ -14,25 +14,11 @@ class Skippy::Config < Hash
14
14
  json = File.read(path)
15
15
  config = JSON.parse(json,
16
16
  symbolize_names: true,
17
- object_class: self
18
- )
17
+ object_class: self)
19
18
  else
20
- config = self.new
19
+ config = new
21
20
  end
22
- # Need to merge nested defaults.
23
- config.merge!(defaults) { |_key, value, default|
24
- if value.is_a?(Hash) && default.is_a?(Hash)
25
- # Deep merge in order to merge nested hashes.
26
- # Note: This currently doesn't merge arrays.
27
- # http://stackoverflow.com/a/9381776/486990
28
- merger = proc { |_k, v1, v2|
29
- Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2
30
- }
31
- default.merge(value, &merger)
32
- else
33
- value || default
34
- end
35
- }
21
+ config.merge_defaults(defaults)
36
22
  config.path = path
37
23
  config
38
24
  end
@@ -77,7 +63,7 @@ class Skippy::Config < Hash
77
63
  if hash.keys.first.is_a?(String)
78
64
  update_from_key_paths(hash)
79
65
  else
80
- update_from_hash(hash)
66
+ deep_merge!(hash)
81
67
  end
82
68
  self
83
69
  end
@@ -86,11 +72,30 @@ class Skippy::Config < Hash
86
72
  "#{super}:#{self.class.name}"
87
73
  end
88
74
 
75
+ # @param [Hash] defaults
76
+ def merge_defaults(defaults)
77
+ merge!(defaults) { |_key, value, default|
78
+ if value.is_a?(Hash) && default.is_a?(Hash)
79
+ # Deep merge in order to merge nested hashes.
80
+ # Note: This currently doesn't merge arrays.
81
+ # http://stackoverflow.com/a/9381776/486990
82
+ merger = proc { |_k, v1, v2|
83
+ v1.is_a?(Hash) && v2.is_a?(Hash) ? v1.merge(v2, &merger) : v2
84
+ }
85
+ default.merge(value, &merger)
86
+ else
87
+ # TODO(thomthom): Should `merger` include this logic?
88
+ value || default
89
+ end
90
+ }
91
+ end
92
+
89
93
  private
90
94
 
91
- def update_from_hash(hash)
95
+ # @param [Hash] hash
96
+ def deep_merge!(hash)
92
97
  merger = proc { |_key, v1, v2|
93
- Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2
98
+ v1.is_a?(Hash) && v2.is_a?(Hash) ? v1.merge(v2, &merger) : v2
94
99
  }
95
100
  merge!(hash, &merger)
96
101
  end
@@ -105,7 +110,7 @@ class Skippy::Config < Hash
105
110
  if key_path.is_a?(Symbol)
106
111
  [key_path]
107
112
  else
108
- key_path.split('/').map { |key| key.intern }
113
+ key_path.split('/').map(&:intern)
109
114
  end
110
115
  end
111
116
 
@@ -5,38 +5,38 @@ module Skippy::ConfigAccessors
5
5
 
6
6
  private
7
7
 
8
- def config_attr(*symbols, key: nil, type: nil)
9
- config_attr_reader(*symbols, key: key, type: type)
8
+ def config_attr(*symbols, key: nil, default: nil, type: nil)
9
+ config_attr_reader(*symbols, key: key, type: type, default: default)
10
10
  config_attr_writer(*symbols, key: key, type: type)
11
11
  nil
12
12
  end
13
13
 
14
- def config_attr_reader(*symbols, key: nil, type: nil)
15
- self.class_eval {
14
+ def config_attr_reader(*symbols, key: nil, default: nil, type: nil)
15
+ class_eval do
16
16
  symbols.each { |symbol|
17
17
  raise TypeError unless symbol.is_a?(Symbol)
18
- define_method(symbol) {
19
- value = @config.get(key || symbol)
18
+ define_method(symbol) do
19
+ value = @config.get(key || symbol, default)
20
20
  value = type.new(value) if type && !value.is_a?(type)
21
21
  value
22
- }
22
+ end
23
23
  }
24
- }
24
+ end
25
25
  nil
26
26
  end
27
27
 
28
28
  def config_attr_writer(*symbols, key: nil, type: nil)
29
- self.class_eval {
29
+ class_eval do
30
30
  symbols.each { |symbol|
31
31
  raise TypeError unless symbol.is_a?(Symbol)
32
32
  symbol_set = "#{symbol}=".intern
33
- define_method(symbol_set) { |value|
33
+ define_method(symbol_set) do |value|
34
34
  value = type.new(value) if type && !value.is_a?(type)
35
35
  @config.set(key || symbol, value)
36
36
  value
37
- }
37
+ end
38
38
  }
39
- }
39
+ end
40
40
  nil
41
41
  end
42
42
 
data/lib/skippy/group.rb CHANGED
@@ -5,12 +5,10 @@ require 'skippy/command'
5
5
  module Skippy
6
6
  class Command::Group < Thor::Group
7
7
 
8
- protected
9
-
10
8
  # Customize the banner as we don't care for the 'skippy' prefix for each
11
9
  # item in the list.
12
10
  def self.banner
13
- "#{self_command.formatted_usage(self, false)}"
11
+ self_command.formatted_usage(self, false).to_s
14
12
  end
15
13
 
16
14
  end
@@ -1,13 +1,13 @@
1
1
  module Skippy::Helpers
2
2
  module File
3
3
 
4
+ extend self
5
+
4
6
  # @param [Pathname]
5
7
  # @return [Array<Pathname>]
6
8
  def directories(pathname)
7
9
  return [] unless pathname.exist?
8
- pathname.children.select { |child|
9
- child.directory?
10
- }
10
+ pathname.children.select(&:directory?)
11
11
  end
12
12
 
13
13
  end
@@ -0,0 +1,49 @@
1
+ require 'skippy/lib_source'
2
+ require 'skippy/library'
3
+ require 'skippy/project'
4
+
5
+ class Skippy::LibraryInstaller
6
+
7
+ attr_reader :project, :source
8
+
9
+ # @param [Skippy::Project] project
10
+ # @param [Skippy::LibrarySource] source
11
+ def initialize(project, lib_source)
12
+ @project = project
13
+ @source = lib_source
14
+ @messager = nil
15
+ end
16
+
17
+ def on_status(&block)
18
+ @messager = block
19
+ end
20
+
21
+ # @return [Skippy::Library]
22
+ def install
23
+ raise NotImplementedError
24
+ end
25
+
26
+ private
27
+
28
+ # @param [Symbol] type
29
+ # @param [String] message
30
+ def status(type, message)
31
+ @messager.call(type, message) if @messager
32
+ end
33
+
34
+ # @param [String] message
35
+ def info(message)
36
+ status(:info, message)
37
+ end
38
+
39
+ # @param [String] message
40
+ def warning(message)
41
+ status(:warning, "Warning: #{message}")
42
+ end
43
+
44
+ # @return [Pathname]
45
+ def path
46
+ project.libraries.path
47
+ end
48
+
49
+ end
@@ -0,0 +1,115 @@
1
+ require 'git'
2
+ require 'naturally'
3
+ require 'pathname'
4
+
5
+ require 'skippy/error'
6
+ require 'skippy/installer'
7
+ require 'skippy/library'
8
+
9
+ module Skippy
10
+
11
+ class BranchNotFound < Skippy::Error; end
12
+ class TagNotFound < Skippy::Error; end
13
+
14
+ end
15
+
16
+ class Skippy::GitLibraryInstaller < Skippy::LibraryInstaller
17
+
18
+ # @return [Skippy::Library]
19
+ def install
20
+ info "Installing #{source.basename} from #{source.origin}..."
21
+ target = path.join(source.lib_path)
22
+ previous_commit = nil
23
+ if target.directory?
24
+ git, previous_commit = update_repository(target)
25
+ else
26
+ git = clone_repository(source.origin, target)
27
+ end
28
+ begin
29
+ checkout_branch(git, source.branch) if source.branch
30
+ checkout_tag(git, source.requirement) unless edge_version?(source.requirement)
31
+ rescue Skippy::Error
32
+ git.checkout(previous_commit) if previous_commit
33
+ raise
34
+ end
35
+ library = Skippy::Library.new(target, source: source)
36
+ library
37
+ end
38
+
39
+ private
40
+
41
+ # @param [URI] uri
42
+ # @param [Pathname] target
43
+ # @return [Git::Base]
44
+ def clone_repository(uri, target)
45
+ info 'Cloning...'
46
+ Git.clone(uri, target.basename, path: target.parent)
47
+ end
48
+
49
+ # @param [Pathname] target
50
+ # @return [Array(Git::Base, Git::Commit)]
51
+ def update_repository(target)
52
+ info 'Updating...'
53
+ library = Skippy::Library.new(target)
54
+ info "Current version: #{library.version}"
55
+ git = Git.open(target)
56
+ previous_commit = git.object('HEAD^').class
57
+ git.reset_hard
58
+ git.pull
59
+ [git, previous_commit]
60
+ end
61
+
62
+ # @param [Git::Base]
63
+ # @param [String] branch
64
+ def checkout_branch(git, branch)
65
+ branches = git.braches.map(&:name)
66
+ info "Branches: #{branches.inspect}"
67
+ unless branches.include?(branch)
68
+ raise Skippy::BranchNotFound, "Found no branch named: '#{branch}'"
69
+ end
70
+ git.checkout(branch)
71
+ nil
72
+ end
73
+
74
+ # @param [Git::Base]
75
+ # @param [String] version
76
+ def checkout_tag(git, version)
77
+ tags = Naturally.sort_by(git.tags, :name)
78
+ tag = latest_version?(version) ? tags.last : resolve_tag(tags, version)
79
+ raise Skippy::TagNotFound, "Found no version: '#{version}'" if tag.nil?
80
+ git.checkout(tag)
81
+ # Verify the library version with the tagged version.
82
+ target = path.join(source.lib_path)
83
+ library = Skippy::Library.new(target)
84
+ unless library.version.casecmp(tag.name).zero?
85
+ warning "skippy.json version (#{library.version}) differ from "\
86
+ "tagged version (#{tag.name})"
87
+ end
88
+ nil
89
+ end
90
+
91
+ # Resolve version numbers like RubyGem.
92
+ #
93
+ # @param [Array<Git::Tag>] tags List of tags sorted with newest first
94
+ # @param [String] version
95
+ # @return [Git::Tag]
96
+ def resolve_tag(tags, version)
97
+ requirement = Gem::Requirement.new(version)
98
+ tags.reverse.find { |tag|
99
+ next false unless Gem::Version.correct?(tag.name)
100
+ tag_version = Gem::Version.new(tag.name)
101
+ requirement.satisfied_by?(tag_version)
102
+ }
103
+ end
104
+
105
+ # @param [String] version
106
+ def edge_version?(version)
107
+ version && version.casecmp('edge').zero?
108
+ end
109
+
110
+ # @param [String] version
111
+ def latest_version?(version)
112
+ version.nil? || version.casecmp('latest').zero?
113
+ end
114
+
115
+ end