skippy 0.2.0.a → 0.3.0.a

Sign up to get free protection for your applications and to get access to all the features.
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