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
@@ -0,0 +1,19 @@
1
+ require 'pathname'
2
+
3
+ require 'skippy/installer'
4
+ require 'skippy/library'
5
+
6
+ class Skippy::LocalLibraryInstaller < Skippy::LibraryInstaller
7
+
8
+ # @return [Skippy::Library]
9
+ def install
10
+ info "Installing #{source.basename} from #{source.origin}..."
11
+ library = Skippy::Library.new(source.origin)
12
+ target = path.join(library.name)
13
+ FileUtils.mkdir_p(path)
14
+ # Must remove the destination in order to ensure update installations works.
15
+ FileUtils.copy_entry(source.origin, target, false, false, true)
16
+ Skippy::Library.new(target, source: source)
17
+ end
18
+
19
+ end
@@ -4,14 +4,25 @@ require 'skippy/library'
4
4
 
5
5
  class Skippy::LibModule
6
6
 
7
- attr_reader :path
7
+ attr_reader :path, :library
8
8
 
9
9
  class ModuleNotFoundError < Skippy::Error; end
10
10
 
11
+ # @param [Skippy::Library] library
11
12
  # @param [String] path
12
- def initialize(path)
13
+ def initialize(library, path)
13
14
  @path = Pathname.new(path)
14
15
  raise ModuleNotFoundError, @path.to_s unless @path.file?
16
+ @library = library
17
+ end
18
+
19
+ def <=>(other)
20
+ other.is_a?(self.class) ? name <=> other.name : nil
21
+ end
22
+
23
+ def eql?(other)
24
+ # http://javieracero.com/blog/the-key-to-ruby-hashes-is-eql-hash
25
+ other.is_a?(self.class) && name.casecmp(other.name).zero?
15
26
  end
16
27
 
17
28
  # @param [String]
@@ -19,14 +30,13 @@ class Skippy::LibModule
19
30
  path.basename('.*').to_s
20
31
  end
21
32
 
22
- # @return [Skippy::Library]
23
- def library
24
- Skippy::Library.new(library_path)
33
+ def hash
34
+ name.hash
25
35
  end
26
36
 
27
37
  # @param [String]
28
38
  def name
29
- "#{library_name}/#{basename}"
39
+ "#{library.name}/#{basename}"
30
40
  end
31
41
 
32
42
  # @param [String]
@@ -34,14 +44,4 @@ class Skippy::LibModule
34
44
  name
35
45
  end
36
46
 
37
- private
38
-
39
- def library_name
40
- library_path.basename.to_s
41
- end
42
-
43
- def library_path
44
- path.parent.parent
45
- end
46
-
47
47
  end
@@ -0,0 +1,139 @@
1
+ require 'digest'
2
+ require 'net/http'
3
+ require 'pathname'
4
+ require 'uri'
5
+
6
+ require 'skippy/error'
7
+ require 'skippy/library'
8
+
9
+ class Skippy::LibrarySource
10
+
11
+ attr_reader :origin, :options
12
+
13
+ class LibraryNotFoundError < Skippy::Error; end
14
+
15
+ # @param [Skippy::Project] project
16
+ # @param [Pathname, String] source
17
+ # @param [Hash] options
18
+ def initialize(project, source, options = {})
19
+ @project = project
20
+ @origin = resolve(source.to_s)
21
+ @options = options
22
+ end
23
+
24
+ def git?
25
+ git_source?(@origin)
26
+ end
27
+
28
+ def local?
29
+ local_source?(@origin)
30
+ end
31
+
32
+ def relative?
33
+ local? && Pathname.new(@origin).relative?
34
+ end
35
+
36
+ def absolute?
37
+ !relative?
38
+ end
39
+
40
+ # @return [String, nil]
41
+ def requirement
42
+ return nil if @options[:requirement].nil?
43
+ # Normalize the version requirement pattern.
44
+ parts = Gem::Requirement.parse(@options[:requirement])
45
+ # .parse will from '1.2.3' return ['=', '1.2.3']. Don't need that.
46
+ parts.delete('=')
47
+ parts.join(' ')
48
+ rescue Gem::Requirement::BadRequirementError
49
+ @options[:requirement]
50
+ end
51
+
52
+ # @return [String]
53
+ def branch
54
+ @options[:branch]
55
+ end
56
+
57
+ # @param [String]
58
+ def basename
59
+ if local?
60
+ Pathname.new(@origin).basename
61
+ else
62
+ uri = URI.parse(@origin)
63
+ Pathname.new(uri.path).basename('.git')
64
+ end
65
+ end
66
+
67
+ # @param [String]
68
+ def lib_path
69
+ if local?
70
+ source = File.expand_path(@origin)
71
+ hash_signature = Digest::SHA1.hexdigest(source)
72
+ "#{basename}_local_#{hash_signature}"
73
+ else
74
+ # https://github.com/thomthom/tt-lib.git
75
+ # ^^^^^^^^^^ ^^^^^^^^ ^^^^^^
76
+ # source_name author basename
77
+ uri = URI.parse(@origin)
78
+ source_name = uri.hostname.gsub(/[.]/, '-')
79
+ author = Pathname.new(uri.path).parent.basename
80
+ "#{basename}_#{author}_#{source_name}"
81
+ end
82
+ end
83
+
84
+ # @param [String]
85
+ def to_s
86
+ @origin.to_s
87
+ end
88
+
89
+ private
90
+
91
+ # @param [String] source
92
+ def resolve(source)
93
+ if git_source?(source)
94
+ resolve_from_git_uri(source)
95
+ elsif lib_name?(source)
96
+ resolve_from_lib_name(source, @project.sources)
97
+ else
98
+ source
99
+ end
100
+ end
101
+
102
+ # @param [String] source
103
+ def git_source?(source)
104
+ source.end_with?('.git') || Pathname.new(source).join('.git').exist?
105
+ end
106
+
107
+ # @param [String] source
108
+ def local_source?(source)
109
+ File.exist?(source)
110
+ end
111
+
112
+ # @param [String] source
113
+ def lib_name?(source)
114
+ !local_source?(source) && source =~ %r{^[^/]+/[^/]+$}
115
+ end
116
+
117
+ # @param [String] source
118
+ # @return [String]
119
+ def resolve_from_git_uri(source)
120
+ uri = URI.parse(source)
121
+ # When logged in, BitBucket will display a URI with the user's username.
122
+ uri.user = ''
123
+ uri.to_s
124
+ end
125
+
126
+ # @param [String] source
127
+ # @return [String]
128
+ def resolve_from_lib_name(source, domains)
129
+ domains.each { |domain|
130
+ uri_str = "https://#{domain}/#{source}.git"
131
+ uri = URI.parse(uri_str)
132
+ response = Net::HTTP.get_response(uri)
133
+ return uri_str if response.is_a?(Net::HTTPSuccess) ||
134
+ response.is_a?(Net::HTTPRedirection)
135
+ }
136
+ raise LibraryNotFoundError, "Library '#{source}' not found"
137
+ end
138
+
139
+ end
@@ -14,44 +14,84 @@ class Skippy::Library
14
14
 
15
15
  CONFIG_FILENAME = 'skippy.json'.freeze
16
16
 
17
- attr_reader :path
17
+ attr_reader :path, :source, :requirement
18
18
 
19
- config_attr_reader :title, key: :name # TODO(thomthom): Clean up this kludge.
19
+ config_attr_reader :name
20
20
  config_attr_reader :version
21
21
 
22
22
  class LibraryNotFoundError < Skippy::Error; end
23
23
 
24
- def initialize(path)
24
+ # @param [Pathname, String] path
25
+ # @param [Skippy::LibrarySource] source
26
+ def initialize(path, source: nil)
27
+ # TODO: Rename LibrarySource - it also contain requirement.
25
28
  @path = Pathname.new(path)
26
29
  raise LibraryNotFoundError, @path.to_s unless @path.directory?
27
- # noinspection RubyResolve
30
+ raise LibraryNotFoundError, config_file.to_s unless config_file.exist?
28
31
  @config = Skippy::Config.load(config_file)
32
+ raise LibraryNotFoundError, 'Not a Skippy Library' unless @config[:library]
33
+ @source = source
29
34
  end
30
35
 
31
- def name
32
- path.basename.to_s
36
+ def <=>(other)
37
+ # TODO: This isn't taking into account version. Maybe take that into account
38
+ # and implement Comparable.
39
+ other.is_a?(self.class) ? name <=> other.name : nil
33
40
  end
34
41
 
42
+ def eql?(other)
43
+ # http://javieracero.com/blog/the-key-to-ruby-hashes-is-eql-hash
44
+ # TODO: Compare using #hash.
45
+ other.is_a?(self.class) && name.casecmp(other.name).zero?
46
+ end
47
+
48
+ def hash
49
+ # TODO: This doesn't take into account version. Right now LibraryManager
50
+ # relies on this to avoid listing the same lib multiple times.
51
+ # But maybe this hash should reflect version differences and the
52
+ # library manager enforce library uniqueness differently.
53
+ name.hash
54
+ end
55
+
56
+ # @return [Array<Skippy::LibModule>]
35
57
  def modules
58
+ # .rb files in the library's modules_path are considered modules.
36
59
  libs = modules_path.children(false).select { |file|
37
- file.extname.downcase == '.rb'
60
+ file.extname.casecmp('.rb').zero?
38
61
  }
39
62
  libs.map! { |lib|
40
63
  path = modules_path.join(lib)
41
- Skippy::LibModule.new(path)
64
+ Skippy::LibModule.new(self, path)
42
65
  }
43
66
  libs
44
67
  end
45
68
 
69
+ # @return [Hash]
70
+ def to_h
71
+ hash = {
72
+ name: name, # TODO: Could be issue as UUID if name changes...
73
+ version: version,
74
+ source: source.origin,
75
+ }
76
+ hash[:requirement] = source.requirement unless source.requirement.nil?
77
+ hash
78
+ end
79
+
80
+ # @return [String]
81
+ def to_s
82
+ name
83
+ end
84
+
46
85
  private
47
86
 
87
+ # @return [Pathname]
48
88
  def config_file
49
89
  path.join(CONFIG_FILENAME)
50
90
  end
51
91
 
92
+ # @return [Pathname]
52
93
  def modules_path
53
- # TODO(thomthom): Make this configurable and default to 'lib'?
54
- path.join('src')
94
+ path.join('modules')
55
95
  end
56
96
 
57
97
  end
@@ -1,10 +1,24 @@
1
+ require 'git'
1
2
  require 'json'
3
+ require 'naturally'
2
4
  require 'pathname'
5
+ require 'set'
3
6
 
4
7
  require 'skippy/helpers/file'
8
+ require 'skippy/installer/git'
9
+ require 'skippy/installer/local'
10
+ require 'skippy/error'
11
+ require 'skippy/lib_source'
5
12
  require 'skippy/library'
6
13
  require 'skippy/project'
7
14
 
15
+ module Skippy
16
+
17
+ class LibraryNotFound < Skippy::Error; end
18
+ class UnknownSourceType < Skippy::Error; end
19
+
20
+ end
21
+
8
22
  class Skippy::LibraryManager
9
23
 
10
24
  include Enumerable
@@ -16,13 +30,12 @@ class Skippy::LibraryManager
16
30
  def initialize(project)
17
31
  raise TypeError, 'expected a Project' unless project.is_a?(Skippy::Project)
18
32
  @project = project
33
+ @libraries = SortedSet.new(discover_libraries)
19
34
  end
20
35
 
21
36
  # @yield [Skippy::Library]
22
37
  def each
23
- directories(path).each { |lib_path|
24
- yield Skippy::Library.new(lib_path)
25
- }
38
+ @libraries.each { |library| yield library }
26
39
  self
27
40
  end
28
41
 
@@ -30,34 +43,80 @@ class Skippy::LibraryManager
30
43
  to_a.empty?
31
44
  end
32
45
 
46
+ # @param [String] library_name
47
+ # @return [Skippy::LibModule, nil]
48
+ def find_library(library_name)
49
+ find { |lib| lib.name.casecmp(library_name).zero? }
50
+ end
51
+
33
52
  # @param [String] module_name
34
53
  # @return [Skippy::LibModule, nil]
35
54
  def find_module(module_name)
36
55
  library_name, module_name = module_name.split('/')
37
- raise ArgumentError, 'expected a module path' if library_name.nil? || module_name.nil?
38
- library = find { |lib| lib.name == library_name }
56
+ if library_name.nil? || module_name.nil?
57
+ raise ArgumentError, 'expected a module path'
58
+ end
59
+ library = find_library(library_name)
39
60
  return nil if library.nil?
40
- library.modules.find { |mod| mod.basename == module_name }
61
+ library.modules.find { |mod| mod.basename.casecmp(module_name).zero? }
62
+ end
63
+
64
+ # @raise [Skippy::LibModule::ModuleNotFoundError]
65
+ # @param [String] module_name
66
+ # @return [Skippy::LibModule]
67
+ def find_module_or_fail(module_name)
68
+ lib_module = find_module(module_name)
69
+ if lib_module.nil?
70
+ raise Skippy::LibModule::ModuleNotFoundError,
71
+ "module '#{module_name}' not found"
72
+ end
73
+ lib_module
41
74
  end
42
75
 
43
76
  # @param [Pathname, String] source
44
- def install(source)
77
+ # @return [Skippy::Library]
78
+ def install(source, options = {})
45
79
  raise Skippy::Project::ProjectNotSavedError unless project.exist?
46
- library = Skippy::Library.new(source)
80
+ lib_source = Skippy::LibrarySource.new(project, source, options)
47
81
 
48
- target = path.join(library.name)
82
+ installer = get_installer(lib_source)
83
+ if block_given?
84
+ installer.on_status { |type, message|
85
+ yield type, message
86
+ }
87
+ end
88
+ library = installer.install
49
89
 
50
- FileUtils.mkdir_p(path)
51
- FileUtils.copy_entry(source, target)
90
+ @libraries.delete(library)
91
+ @libraries << library
52
92
 
53
- project.config.push(:libraries, {
54
- name: library.name,
55
- version: library.version,
56
- source: source
57
- })
93
+ project.modules.update(library)
58
94
 
59
- project.save
95
+ library
96
+ end
60
97
 
98
+ # @param [Skippy::Library, String] source
99
+ # @return [Skippy::Library]
100
+ def uninstall(lib)
101
+ raise Skippy::Project::ProjectNotSavedError unless project.exist?
102
+ library = lib.is_a?(Skippy::Library) ? lib : find_library(lib)
103
+ raise Skippy::LibraryNotFound, 'Library not found' if library.nil?
104
+ # Uninstall modules first - using the module manager.
105
+ vendor_path = project.modules.vendor_path
106
+ vendor_module_path = vendor_path.join(library.name)
107
+ library.modules.each { |mod|
108
+ project.modules.remove(mod.name)
109
+ }
110
+ vendor_module_path.rmtree if vendor_module_path.exist?
111
+ # Remove the vendor path - no need to package unused directories.
112
+ if vendor_path.exist? && vendor_path.children.empty?
113
+ vendor_path.rmdir
114
+ end
115
+ raise 'Unable to remove vendor modules' if vendor_module_path.exist?
116
+ # Now the library itself is safe to remove.
117
+ library.path.rmtree if library.path.exist?
118
+ raise 'Unable to remove library' if library.path.exist?
119
+ @libraries.delete(library)
61
120
  library
62
121
  end
63
122
 
@@ -65,11 +124,50 @@ class Skippy::LibraryManager
65
124
  def length
66
125
  to_a.length
67
126
  end
68
- alias :size :length
127
+ alias size length
69
128
 
70
129
  # @return [Pathname]
71
130
  def path
72
131
  project.path('.skippy/libs')
73
132
  end
74
133
 
134
+ private
135
+
136
+ # @return [Array<Skippy::Library>]
137
+ def discover_libraries
138
+ project.config.get(:libraries, []).map { |lib_config|
139
+ begin
140
+ library_from_config(lib_config)
141
+ rescue Skippy::Library::LibraryNotFoundError => error
142
+ # TODO: Revisit how to handle this.
143
+ warn "Unable to load library: #{error.message}"
144
+ nil
145
+ end
146
+ }.compact
147
+ end
148
+
149
+ # @param [Hash] config
150
+ def library_from_config(config)
151
+ options = {}
152
+ options[:requirement] = config[:requirement] if config[:requirement]
153
+ source = Skippy::LibrarySource.new(project, config[:source], options)
154
+ directories(path).each { |directory|
155
+ library = Skippy::Library.new(directory, source: source)
156
+ return library if library.name.casecmp(config[:name]).zero?
157
+ }
158
+ nil
159
+ end
160
+
161
+ # @param [Skippy::LibrarySource] lib_source
162
+ # @return [Skippy::Installer]
163
+ def get_installer(lib_source)
164
+ if lib_source.git?
165
+ Skippy::GitLibraryInstaller.new(project, lib_source)
166
+ elsif lib_source.local?
167
+ Skippy::LocalLibraryInstaller.new(project, lib_source)
168
+ else
169
+ raise Skippy::UnknownSourceType, "Unable to handle source: #{lib_source}"
170
+ end
171
+ end
172
+
75
173
  end