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