php-composer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +1006 -0
  5. data/Gemfile +15 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +35 -0
  8. data/Rakefile +1 -0
  9. data/lib/composer.rb +52 -0
  10. data/lib/composer/error.rb +8 -0
  11. data/lib/composer/json/json_file.rb +270 -0
  12. data/lib/composer/json/json_formatter.rb +159 -0
  13. data/lib/composer/json/json_validaton_error.rb +29 -0
  14. data/lib/composer/manager.rb +79 -0
  15. data/lib/composer/package/alias_package.rb +273 -0
  16. data/lib/composer/package/base_package.rb +130 -0
  17. data/lib/composer/package/complete_package.rb +55 -0
  18. data/lib/composer/package/dumper/hash_dumper.rb +169 -0
  19. data/lib/composer/package/link.rb +51 -0
  20. data/lib/composer/package/link_constraint/base_constraint.rb +36 -0
  21. data/lib/composer/package/link_constraint/empty_constraint.rb +35 -0
  22. data/lib/composer/package/link_constraint/multi_constraint.rb +67 -0
  23. data/lib/composer/package/link_constraint/specific_constraint.rb +41 -0
  24. data/lib/composer/package/link_constraint/version_constraint.rb +221 -0
  25. data/lib/composer/package/loader/hash_loader.rb +316 -0
  26. data/lib/composer/package/loader/json_loader.rb +47 -0
  27. data/lib/composer/package/loader/project_attributes_loader.rb +71 -0
  28. data/lib/composer/package/loader/project_root_package_loader.rb +28 -0
  29. data/lib/composer/package/package.rb +118 -0
  30. data/lib/composer/package/root_alias_package.rb +37 -0
  31. data/lib/composer/package/root_package.rb +37 -0
  32. data/lib/composer/package/version/version_parser.rb +583 -0
  33. data/lib/composer/package/version/version_selector.rb +106 -0
  34. data/lib/composer/provider.rb +94 -0
  35. data/lib/composer/repository/array_repository.rb +195 -0
  36. data/lib/composer/repository/filesystem_repository.rb +86 -0
  37. data/lib/composer/repository/writeable_array_repository.rb +60 -0
  38. data/lib/composer/version.rb +3 -0
  39. data/php-composer.gemspec +31 -0
  40. data/resources/composer-schema.json +421 -0
  41. metadata +188 -0
@@ -0,0 +1,47 @@
1
+ #
2
+ # This file was ported to ruby from Composer php source code.
3
+ # Original Source: Composer\Package\Loader\JsonLoader.php
4
+ #
5
+ # (c) Nils Adermann <naderman@naderman.de>
6
+ # Jordi Boggiano <j.boggiano@seld.be>
7
+ #
8
+ # For the full copyright and license information, please view the LICENSE
9
+ # file that was distributed with this source code.
10
+ #
11
+
12
+ module Composer
13
+ module Package
14
+ module Loader
15
+
16
+ # Loads a package from a json string or JsonFile
17
+ # @author Ioannis Kappas <ikappas@devworks.gr>
18
+ # @php_author Konstantin Kudryashiv <ever.zet@gmail.com>
19
+ class JsonLoader
20
+
21
+ def initialize(loader)
22
+ @loader = loader
23
+ end
24
+
25
+ # Load a json string or file
26
+ # Param: string|JsonFile json A filename, json string or JsonFile instance to load the package from
27
+ # Returns: Composer::Package::Package
28
+ def load(json)
29
+ if json.instance_of?(Composer::Json::JsonFile)
30
+ config = json.read
31
+ elsif File.exist?(json)
32
+ config = Composer::Json::JsonFile.parse_json(
33
+ File.open(filepath, "r") { |f| f.read },
34
+ json
35
+ )
36
+ elsif json.class === "String"
37
+ config = Composer::Json::JsonFile.parse_json(
38
+ json
39
+ )
40
+ end
41
+ @loader.load(config)
42
+ end
43
+
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,71 @@
1
+ require 'digest/crc32'
2
+
3
+ module Composer
4
+ module Package
5
+ module Loader
6
+
7
+ # Loads a package from project attributes
8
+ # @author Ioannis Kappas <ikappas@devworks.gr>
9
+ class ProjectAttributesLoader
10
+
11
+ def initialize(loader)
12
+ @loader = loader
13
+ end
14
+
15
+ # Load a json string or file
16
+ # Param: string|JsonFile json A filename, json string or JsonFile instance to load the package from
17
+ # Returns: Composer::Package::Package
18
+ def load(project, ref, type = 'library')
19
+
20
+ version = (ref.instance_of?(Gitlab::Git::Branch)) ? "dev-#{ref.name}" : ref.name
21
+
22
+ config = {
23
+ 'name' => project.path_with_namespace.gsub(/\s/, '').downcase,
24
+ 'description' => project.description,
25
+ 'version' => version,
26
+ 'uid' => Digest::CRC32.checksum(ref.name + ref.target),
27
+ 'source' => {
28
+ 'url' => project.url_to_repo,
29
+ 'type' => 'git',
30
+ 'reference' => ref.target
31
+ },
32
+ 'dist' => {
33
+ 'url' => [project.web_url, 'repository', 'archive.zip?ref=' + ref.name].join('/'),
34
+ 'type' => 'zip'
35
+ },
36
+ 'type' => type,
37
+ 'homepage' => project.web_url
38
+ }
39
+
40
+ if time = get_time(project, ref)
41
+ log("Ref: #{ref.to_json} Time: #{time}")
42
+ config['time'] = time
43
+ end
44
+
45
+ if keywords = get_keywords(project)
46
+ config['keywords'] = keywords
47
+ end
48
+
49
+ @loader.load(config)
50
+ end
51
+
52
+ private
53
+
54
+ def get_time(project, ref)
55
+ commit = project.repository.commit(ref.target)
56
+ commit.committed_date.strftime('%Y-%m-%d %H:%M:%S')
57
+ rescue
58
+ # If there's a problem, just skip the "time" field
59
+ end
60
+
61
+ def get_keywords(project)
62
+ project.tags.collect { |t| t['name'] }
63
+ end
64
+
65
+ def log(message)
66
+ Gitlab::AppLogger.error("ComposerService: #{message}")
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,28 @@
1
+ module Composer
2
+ module Package
3
+ module Loader
4
+
5
+ # Loads a package from the project root package
6
+ # @author Ioannis Kappas <ikappas@devworks.gr>
7
+ class ProjectRootPackageLoader
8
+
9
+ def initialize(loader)
10
+ @loader = loader
11
+ end
12
+
13
+ # Load a project ref
14
+ # Param: string|JsonFile json A filename, json string or JsonFile instance to load the package from
15
+ # Returns: Composer::Package::Package
16
+ def load(project, ref)
17
+
18
+ config = Composer::Json::JsonFile.parse_json(
19
+ project.repository.blob_at(ref.target, 'composer.json')
20
+ )
21
+
22
+ @loader.load(config)
23
+ end
24
+
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,118 @@
1
+ #
2
+ # This file was ported to ruby from Composer php source code file.
3
+ # Original Source: Composer\Package\Package.php
4
+ #
5
+ # (c) Nils Adermann <naderman@naderman.de>
6
+ # Jordi Boggiano <j.boggiano@seld.be>
7
+ #
8
+ # For the full copyright and license information, please view the LICENSE
9
+ # file that was distributed with this source code.
10
+ #
11
+
12
+ require 'digest/crc32'
13
+
14
+ module Composer
15
+ module Package
16
+
17
+ # Core package definitions that are needed to resolve dependencies
18
+ # and install packages
19
+ class Package < Composer::Package::BasePackage
20
+
21
+ attr_reader :stability
22
+
23
+ attr_accessor :installation_source, :source_type,
24
+ :source_url, :source_reference, :source_mirrors, :dist_type,
25
+ :dist_url, :dist_reference, :dist_sha1_checksum,
26
+ :dist_mirrors, :release_date, :extra, :binaries, :requires,
27
+ :conflicts, :provides, :replaces, :dev_requires, :suggests,
28
+ :autoload, :dev_autoload, :include_paths, :archive_excludes,
29
+ :notification_url
30
+
31
+ # complete package attributes
32
+
33
+
34
+ # Creates a new in memory package.
35
+ # Param: string name The package's name
36
+ # Param: string version The package's version
37
+ # Param: string prettyVersion The package's non-normalized version
38
+ def initialize(name, version, pretty_version)
39
+ super(name)
40
+
41
+ # default values
42
+ @extra = {}
43
+ @binaries = []
44
+ @requires = {}
45
+ @conflicts = {}
46
+ @provides = {}
47
+ @replaces = {}
48
+ @dev_requires = {}
49
+ @suggests = {}
50
+ @autoload = {}
51
+ @dev_autoload = {}
52
+ @include_paths = []
53
+ @archive_excludes = []
54
+
55
+ # init package attributes
56
+ replace_version(version, pretty_version)
57
+
58
+ end
59
+
60
+ def attributes
61
+ dumper = Composer::Package::Dumper::HashDumper.new
62
+ dumper.dump(self)
63
+ end
64
+
65
+ # Set package type
66
+ # Param: string type
67
+ def type=(type)
68
+ @type = type
69
+ end
70
+
71
+ # Get package type
72
+ # Return: string
73
+ def type
74
+ @type ? @type : 'library'
75
+ end
76
+
77
+ def target_dir=(target_dir)
78
+ @target_dir = target_dir
79
+ end
80
+
81
+ def target_dir
82
+ return unless @target_dir
83
+ regex = '(?:^|[\\\\/]+)\.\.?(?:[\\\\/]+|$)(?:\.\.?(?:[\\\\/]+|$))*'
84
+ @target_dir.gsub(/#{regex}/x, '/').gsub(/^\/+/, '')
85
+ end
86
+
87
+ # Returns package unique name, constructed from name, version and
88
+ # release type.
89
+ # Return: string
90
+ def unique_name
91
+ "#{name}-#{version}"
92
+ end
93
+
94
+ def pretty_string
95
+ "#{pretty_name} #{pretty_version}"
96
+ end
97
+
98
+ # Determine if development package
99
+ # Return: true if development package; Otherwise false.
100
+ def is_dev
101
+ @dev
102
+ end
103
+
104
+ # Replaces current version and pretty version with passed values.
105
+ # It also sets stability.
106
+ # Param: string version The package's normalized version
107
+ # Param: string prettyVersion The package's non-normalized version
108
+ def replace_version(version, pretty_version)
109
+ @version = version
110
+ @pretty_version = pretty_version
111
+
112
+ @stability = Composer::Package::Version::VersionParser::parse_stability(version)
113
+ @dev = @stability === 'dev'
114
+ end
115
+
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,37 @@
1
+ #
2
+ # This file was ported to ruby from Composer php source code file.
3
+ # Original Source: Composer\Package\RootAliasPackage.php
4
+ #
5
+ # (c) Nils Adermann <naderman@naderman.de>
6
+ # Jordi Boggiano <j.boggiano@seld.be>
7
+ #
8
+ # For the full copyright and license information, please view the LICENSE
9
+ # file that was distributed with this source code.
10
+ #
11
+
12
+ module Composer
13
+ module Package
14
+
15
+ # The root package represents the project's composer.json
16
+ # and contains additional metadata
17
+ class RootAliasPackage < Composer::Package::CompletePackage
18
+
19
+ attr_accessor :minimum_stability, :prefer_stable, :stability_flags,
20
+ :references, :aliases
21
+
22
+ # Creates a new root package in memory package.
23
+ # Param: string name The package's name
24
+ # Param: string version The package's version
25
+ # Param: string prettyVersion The package's non-normalized version
26
+ def initialize(name, version, pretty_version)
27
+ super(name, version, pretty_version)
28
+
29
+ @minimum_stability = 'stable'
30
+ @stability_flags = []
31
+ @references = []
32
+ @aliases = []
33
+ end
34
+
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,37 @@
1
+ #
2
+ # This file was ported to ruby from Composer php source code file.
3
+ # Original Source: Composer\Package\RootPackage.php
4
+ #
5
+ # (c) Nils Adermann <naderman@naderman.de>
6
+ # Jordi Boggiano <j.boggiano@seld.be>
7
+ #
8
+ # For the full copyright and license information, please view the LICENSE
9
+ # file that was distributed with this source code.
10
+ #
11
+
12
+ module Composer
13
+ module Package
14
+
15
+ # The root package represents the project's composer.json
16
+ # and contains additional metadata
17
+ class RootPackage < Composer::Package::CompletePackage
18
+
19
+ attr_accessor :minimum_stability, :prefer_stable, :stability_flags,
20
+ :references, :aliases
21
+
22
+ # Creates a new root package in memory package.
23
+ # Param: string name The package's name
24
+ # Param: string version The package's version
25
+ # Param: string prettyVersion The package's non-normalized version
26
+ def initialize(name, version, pretty_version)
27
+ super(name, version, pretty_version)
28
+
29
+ @minimum_stability = 'stable'
30
+ @stability_flags = []
31
+ @references = []
32
+ @aliases = []
33
+ end
34
+
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,583 @@
1
+ #
2
+ # This file was ported to ruby from Composer php source code file.
3
+ # Original Source: Composer\Package\Version\VersionParser.php
4
+ #
5
+ # (c) Nils Adermann <naderman@naderman.de>
6
+ # Jordi Boggiano <j.boggiano@seld.be>
7
+ #
8
+ # For the full copyright and license information, please view the LICENSE
9
+ # file that was distributed with this source code.
10
+ #
11
+
12
+ module Composer
13
+ module Package
14
+ module Version
15
+ # Version Parser
16
+ #
17
+ # PHP Authors:
18
+ # Jordi Boggiano <j.boggiano@seld.be>
19
+ #
20
+ # Ruby Authors:
21
+ # Ioannis Kappas <ikappas@devworks.gr>
22
+ class VersionParser
23
+ MODIFIER_REGEX = '[._-]?(?:(stable|beta|b|RC|alpha|a|patch|pl|p)(?:[.-]?(\d+))?)?([.-]?dev)?'.freeze()
24
+
25
+ class << self
26
+
27
+ # Returns the stability of a version
28
+ #
29
+ # Params:
30
+ # +version+:: string The version to parse for stability
31
+ #
32
+ # Returns:
33
+ # string The version's stability
34
+ def parse_stability(version)
35
+ raise ArgumentError, 'version must be specified' unless version
36
+ raise TypeError, 'version must be of type String' unless version.is_a?(String)
37
+ raise UnexpectedValueError, 'version string must not be empty' if version.empty?
38
+
39
+ version = version.gsub(/#.+$/i, '')
40
+
41
+ if version.start_with?('dev-') || version.end_with?('-dev')
42
+ return 'dev'
43
+ end
44
+
45
+ if matches = /#{MODIFIER_REGEX}$/i.match(version.downcase)
46
+ if matches[3]
47
+ return 'dev'
48
+ elsif matches[1]
49
+ if 'beta' === matches[1] || 'b' === matches[1]
50
+ return 'beta'
51
+ elsif 'alpha' === matches[1] || 'a' === matches[1]
52
+ return 'alpha'
53
+ elsif 'rc' === matches[1]
54
+ return 'RC'
55
+ end
56
+ end
57
+ end
58
+
59
+ 'stable'
60
+ end
61
+
62
+ # Normalize the specified stability
63
+ # Param: string stability
64
+ # Return: string
65
+ def normalize_stability(stability)
66
+ raise ArgumentError, 'stability must be specified' unless stability
67
+ raise TypeError, 'stability must be of type String' unless stability.is_a?(String)
68
+ stability = stability.downcase
69
+ stability === 'rc' ? 'RC' : stability
70
+ end
71
+
72
+ # Formats package version
73
+ # Param: Composer::Package::Package package
74
+ # Param: boolean truncate
75
+ # Return: string
76
+ def format_version(package, truncate = true)
77
+ if !package.is_dev || !['hg', 'git'].include?(package.source_type)
78
+ return package.pretty_version
79
+ end
80
+
81
+ # if source reference is a sha1 hash -- truncate
82
+ if truncate && package.source_reference.length === 40
83
+ return "#{package.pretty_version} #{package.source_reference[0..6]}"
84
+ end
85
+
86
+ "#{package.pretty_version} #{package.source_reference}"
87
+ end
88
+ end
89
+
90
+ # Normalizes a version string to be able to perform comparisons on it
91
+ #
92
+ # Params:
93
+ # +version+:: <tt>String</tt> The version string to normalize
94
+ # +full_version+:: <tt>String</tt> optional complete version string to
95
+ # give more context
96
+ #
97
+ # Throws:
98
+ # +InvalidVersionStringError+
99
+ #
100
+ # Returns:
101
+ # +String+
102
+ def normalize(version, full_version = nil)
103
+ raise ArgumentError, 'version must be specified' unless version
104
+ raise TypeError, 'version must be of type String' unless version.is_a?(String)
105
+ raise UnexpectedValueError, 'version string must not be empty' if version.empty?
106
+
107
+ version.strip!
108
+ if full_version == nil
109
+ full_version = version
110
+ end
111
+
112
+ # ignore aliases and just assume the alias is required
113
+ # instead of the source
114
+ if matches = /^([^,\s]+) +as +([^,\s]+)$/.match(version)
115
+ version = matches[1]
116
+ end
117
+
118
+ # ignore build metadata
119
+ if matches = /^([^,\s+]+)\+[^\s]+$/.match(version)
120
+ version = matches[1]
121
+ end
122
+
123
+ # match master-like branches
124
+ if matches = /^(?:dev-)?(?:master|trunk|default)$/i.match(version)
125
+ return '9999999-dev'
126
+ end
127
+
128
+ if 'dev-' === version[0...4].downcase
129
+ return "dev-#{version[4..version.size]}"
130
+ end
131
+
132
+ # match classical versioning
133
+ index = 0
134
+ if matches = /^v?(\d{1,3})(\.\d+)?(\.\d+)?(\.\d+)?#{MODIFIER_REGEX}$/i.match(version)
135
+ version = ''
136
+ matches.to_a[1..4].each do |c|
137
+ version += c ? c : '.0'
138
+ end
139
+ index = 5
140
+ elsif matches = /^v?(\d{4}(?:[.:-]?\d{2}){1,6}(?:[.:-]?\d{1,3})?)#{MODIFIER_REGEX}$/i.match(version)
141
+ version = matches[1].gsub(/\D/, '-')
142
+ index = 2
143
+ elsif matches = /^v?(\d{4,})(\.\d+)?(\.\d+)?(\.\d+)?#{MODIFIER_REGEX}$/i.match(version)
144
+ version = ''
145
+ matches.to_a[1..4].each do |c|
146
+ version << (c.nil? ? '.0' : c)
147
+ end
148
+ index = 5
149
+ end
150
+
151
+ # add version modifiers if a version was matched
152
+ if index > 0
153
+ if matches[index]
154
+ if 'stable' === matches[index]
155
+ return version
156
+ end
157
+ stability = expand_stability(matches[index])
158
+ version = "#{version}-#{stability ? stability : matches[index]}#{matches[index + 1] ? matches[index + 1] : ''}"
159
+ end
160
+
161
+ if matches[index + 2]
162
+ version = "#{version}-dev"
163
+ end
164
+
165
+ return version
166
+ end
167
+
168
+ # match dev branches
169
+ if matches = /(.*?)[.-]?dev$/i.match(version)
170
+ begin
171
+ return normalize_branch(matches[1])
172
+ rescue
173
+ end
174
+ end
175
+
176
+ extra_message = ''
177
+ if matches = / +as +#{Regexp.escape(version)}$/.match(full_version)
178
+ extra_message = " in \"#{full_version}\", the alias must be an exact version"
179
+ elsif matches = /^#{Regexp.escape(version)} +as +/.match(full_version)
180
+ extra_message = " in \"#{full_version}\", the alias source must be an exact version, if it is a branch name you should prefix it with dev-"
181
+ end
182
+
183
+ raise UnexpectedValueError, "Invalid version string \"#{version}\"#{extra_message}"
184
+ end
185
+
186
+ # Normalizes a branch name to be able to perform comparisons on it
187
+ #
188
+ # Params:
189
+ # +name+:: string The branch name to normalize
190
+ #
191
+ # Returns:
192
+ # string The normalized branch name
193
+ def normalize_branch(name)
194
+ name.strip!
195
+
196
+ if ['master', 'trunk', 'default'].include?(name)
197
+ return normalize(name)
198
+ end
199
+
200
+ if matches = /^v?(\d+)(\.(?:\d+|[xX*]))?(\.(?:\d+|[xX*]))?(\.(?:\d+|[xX*]))?$/i.match(name)
201
+ version = ''
202
+
203
+ # for i in 0..3
204
+ # # version << matches[i] ? matches[i].gsub('*', 'x').gsub('X', 'x') : '.x'
205
+ # end
206
+ matches.captures.each { |match| version << (match != nil ? match.gsub('*', 'x').gsub('X', 'x') : '.x') }
207
+ return "#{version.gsub('x', '9999999')}-dev"
208
+ end
209
+
210
+ "dev-#{name}"
211
+ end
212
+
213
+ # Params:
214
+ # +source+:: string source package name
215
+ # +source_version+:: string source package version (pretty version ideally)
216
+ # +description+:: string link description (e.g. requires, replaces, ..)
217
+ # +links+:: array An array of package name => constraint mappings
218
+ #
219
+ # Returns:
220
+ # Link[]
221
+ def parse_links(source, source_version, description, links)
222
+ res = {}
223
+ links.each do |target, constraint|
224
+ if 'self.version' === constraint
225
+ parsed_constraint = parse_constraints(source_version)
226
+ else
227
+ parsed_constraint = parse_constraints(constraint)
228
+ end
229
+ res[target.downcase] = Composer::Package::Link.new(
230
+ source,
231
+ target,
232
+ parsed_constraint,
233
+ description,
234
+ constraint
235
+ )
236
+ end
237
+ res
238
+ end
239
+
240
+ def parse_constraints(constraints)
241
+ raise ArgumentError, 'version must be specified' unless constraints
242
+ raise TypeError, 'version must be of type String' unless constraints.is_a?(String)
243
+ raise UnexpectedValueError, 'version string must not be empty' if constraints.empty?
244
+
245
+ pretty_constraint = constraints
246
+
247
+ stabilites = Composer::Package::BasePackage.stabilities.keys.join('|')
248
+ if match = /^([^,\s]*?)@(#{stabilites})$/i.match(constraints)
249
+ constraints = match[1].nil? || match[1].empty? ? '*' : match[1]
250
+ end
251
+
252
+ if match = /^(dev-[^,\s@]+?|[^,\s@]+?\.x-dev)#.+$/i.match(constraints)
253
+ constraints = match[1]
254
+ end
255
+
256
+ # or_constraints = preg_split('{\s*\|\|?\s*}', trim(constraints))
257
+ or_constraints = constraints.strip.split(/\s*\|\|?\s*/)
258
+ or_groups = []
259
+ or_constraints.each do |constraints|
260
+
261
+ # and_constraints = preg_split('{(?<!^|as|[=>< ,]) *(?<!-)[, ](?!-) *(?!,|as|$)}', constraints)
262
+ and_constraints = constraints.split(/(?<!^|as|[=>< ,]) *(?<!-)[, ](?!-) *(?!,|as|$)/)
263
+
264
+ if and_constraints.length > 1
265
+ constraint_objects = []
266
+ and_constraints.each do |constraint|
267
+ constraint_objects << parse_constraint(constraint)
268
+ end
269
+ else
270
+ constraint_objects = parse_constraint(and_constraints[0])
271
+ end
272
+
273
+ if constraint_objects.length === 1
274
+ constraint = constraint_objects[0]
275
+ else
276
+ constraint = Composer::Package::LinkConstraint::MultiConstraint.new(constraint_objects)
277
+ end
278
+
279
+ or_groups << constraint
280
+ end
281
+
282
+ if or_groups.length === 1
283
+ constraint = or_groups[0]
284
+ else
285
+ constraint = Composer::Package::LinkConstraint::MultiConstraint.new(or_groups, false)
286
+ end
287
+
288
+ constraint.pretty_string = pretty_constraint
289
+
290
+ constraint
291
+ end
292
+
293
+ # Extract numeric prefix from alias, if it is in numeric format,
294
+ # suitable for version comparison
295
+ #
296
+ # Params:
297
+ # +branch+:: string Branch name (e.g. 2.1.x-dev)
298
+ #
299
+ # Returns:
300
+ # string|false Numeric prefix if present (e.g. 2.1.) or false
301
+ def parse_numeric_alias_prefix(branch)
302
+ if matches = /^(?<version>(\d+\.)*\d+)(?:\.x)?-dev$/i.match(branch)
303
+ return "#{matches['version']}."
304
+ end
305
+ false
306
+ end
307
+
308
+ # Parses a name/version pairs and returns an array of pairs + the
309
+ #
310
+ # Params:
311
+ # +pairs+:: array a set of package/version pairs separated by ":", "=" or " "
312
+ #
313
+ # Returns:
314
+ # array[] array of arrays containing a name and (if provided) a version
315
+ def parse_name_version_pairs(pairs)
316
+ pairs = pairs.values
317
+ result = []
318
+
319
+ for i in 0..(pairs.length - 1)
320
+ pair = pairs[i].strip!.gsub(/^([^=: ]+)[=: ](.*)$/, '$1 $2')
321
+ if nil === pair.index(' ') && pairs.key?(i + 1) && nil === pairs[i + 1].index('/')
322
+ pair = "#{pair} #{pairs[i + 1]}"
323
+ i = i + 1
324
+ end
325
+
326
+ if pair.index(' ')
327
+ name, version = pair.split(' ', 2)
328
+ result << { 'name' => name, 'version' => version }
329
+ else
330
+ result << { 'name' => pair }
331
+ end
332
+
333
+ end
334
+
335
+ result
336
+ end
337
+
338
+ # PRIVATE METHODS
339
+ private
340
+
341
+ def parse_constraint(constraint)
342
+
343
+ stabilites = Composer::Package::BasePackage.stabilities.keys.join('|')
344
+ if match = /^([^,\s]+?)@(#{stabilites})$/i.match(constraint)
345
+ constraint = match[1]
346
+ if match[2] != 'stable'
347
+ stability_modifier = match[2]
348
+ end
349
+ end
350
+
351
+ if /^[xX*](\.[xX*])*$/i.match(constraint)
352
+ return [
353
+ Composer::Package::LinkConstraint::EmptyConstraint.new
354
+ ]
355
+ end
356
+
357
+ version_regex = '(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?' + MODIFIER_REGEX
358
+
359
+ # match tilde constraints
360
+ # like wildcard constraints, unsuffixed tilde constraints say that they must be greater than the previous
361
+ # version, to ensure that unstable instances of the current version are allowed.
362
+ # however, if a stability suffix is added to the constraint, then a >= match on the current version is
363
+ # used instead
364
+ if matches = /^~>?#{version_regex}$/i.match(constraint)
365
+
366
+ if constraint[0...2] === '~>'
367
+ raise UnexpectedValueError,
368
+ "Could not parse version constraint #{constraint}: \
369
+ Invalid operator \"~>\", you probably meant to use the \"~\" operator"
370
+ end
371
+
372
+ # Work out which position in the version we are operating at
373
+ if matches[4] && matches[4] != ''
374
+ position = 4
375
+ elsif matches[3] && matches[3] != ''
376
+ position = 3
377
+ elsif matches[2] && matches[2] != ''
378
+ position = 2
379
+ else
380
+ position = 1
381
+ end
382
+
383
+ # Calculate the stability suffix
384
+ stability_suffix = ''
385
+ if !matches[5].nil? && !matches[5].empty?
386
+ stability_suffix << "-#{expand_stability(matches[5])}"
387
+ if !matches[6].nil? && !matches[6].empty?
388
+ stability_suffix << matches[6]
389
+ end
390
+ end
391
+
392
+ if !matches[7].nil? && !matches[7].empty?
393
+ stability_suffix << '-dev'
394
+ end
395
+
396
+ stability_suffix = '-dev' if stability_suffix.empty?
397
+
398
+ low_version = manipulate_version_string(matches.to_a, position, 0) + stability_suffix
399
+ lower_bound = Composer::Package::LinkConstraint::VersionConstraint.new('>=', low_version)
400
+
401
+ # For upper bound, we increment the position of one more significance,
402
+ # but high_position = 0 would be illegal
403
+ high_position = [1, position - 1].max
404
+ high_version = manipulate_version_string(matches.to_a, high_position, 1) + '-dev'
405
+ upper_bound = Composer::Package::LinkConstraint::VersionConstraint.new('<', high_version)
406
+
407
+ return [
408
+ lower_bound,
409
+ upper_bound
410
+ ]
411
+ end
412
+
413
+ # match caret constraints
414
+ if matches = /^\^#{version_regex}($)/i.match(constraint)
415
+ # Create comparison array
416
+ has_match = []
417
+ for i in 0..(matches.to_a.length - 1)
418
+ has_match[i] = !matches[i].nil? && !matches[i].empty?
419
+ end
420
+
421
+ # Work out which position in the version we are operating at
422
+ if matches[1] != '0' || !has_match[2]
423
+ position = 1
424
+ elsif matches[2] != '0' || !has_match[3]
425
+ position = 2
426
+ else
427
+ position = 3
428
+ end
429
+
430
+ # Calculate the stability suffix
431
+ stability_suffix = ''
432
+ if !has_match[5] && !has_match[7]
433
+ stability_suffix << '-dev'
434
+ end
435
+
436
+ low_pretty = "#{constraint}#{stability_suffix}"
437
+ low_version = normalize(low_pretty[1..low_pretty.length - 1])
438
+ lower_bound = Composer::Package::LinkConstraint::VersionConstraint.new('>=', low_version)
439
+
440
+ # For upper bound, we increment the position of one more significance,
441
+ # but high_position = 0 would be illegal
442
+ high_version = manipulate_version_string(matches.to_a, position, 1) + '-dev'
443
+ upper_bound = Composer::Package::LinkConstraint::VersionConstraint.new('<', high_version)
444
+
445
+ return [
446
+ lower_bound,
447
+ upper_bound
448
+ ]
449
+ end
450
+
451
+ # match wildcard constraints
452
+ if matches = /^(\d+)(?:\.(\d+))?(?:\.(\d+))?\.[xX*]$/.match(constraint)
453
+ if matches[3] && matches[3] != ''
454
+ position = 3
455
+ elsif matches[2] && matches[2] != ''
456
+ position = 2
457
+ else
458
+ position = 1
459
+ end
460
+
461
+ low_version = manipulate_version_string(matches.to_a, position) + '-dev'
462
+ high_version = manipulate_version_string(matches.to_a, position, 1) + '-dev'
463
+
464
+ if low_version === "0.0.0.0-dev"
465
+ return [Composer::Package::LinkConstraint::VersionConstraint.new('<', high_version)]
466
+ end
467
+
468
+ return [
469
+ Composer::Package::LinkConstraint::VersionConstraint.new('>=', low_version),
470
+ Composer::Package::LinkConstraint::VersionConstraint.new('<', high_version),
471
+ ]
472
+ end
473
+
474
+ # match hyphen constraints
475
+ if matches = /^(#{version_regex}) +- +(#{version_regex})($)/i.match(constraint)
476
+
477
+ match_from = matches[1]
478
+ match_to = matches[9]
479
+
480
+ # Create comparison array
481
+ has_match = []
482
+ for i in 0..(matches.to_a.length - 1)
483
+ has_match[i] = !matches[i].nil? && !matches[i].empty?
484
+ end
485
+
486
+ # Calculate the stability suffix
487
+ low_stability_suffix = ''
488
+ if !has_match[6] && !has_match[8]
489
+ low_stability_suffix = '-dev'
490
+ end
491
+
492
+ low_version = normalize(match_from)
493
+ lower_bound = Composer::Package::LinkConstraint::VersionConstraint.new('>=', low_version + low_stability_suffix)
494
+
495
+ # high_version = matches[10]
496
+
497
+ if (has_match[11] && has_match[12]) || has_match[14] || has_match[16]
498
+ high_version = normalize(match_to)
499
+ upper_bound = Composer::Package::LinkConstraint::VersionConstraint.new('<=', high_version)
500
+ else
501
+ high_match = ['', matches[10], matches[11], matches[12], matches[13]]
502
+ high_version = manipulate_version_string(high_match, (!has_match[11] ? 1 : 2), 1) + '-dev'
503
+ upper_bound = Composer::Package::LinkConstraint::VersionConstraint.new('<', high_version)
504
+ end
505
+
506
+ return [
507
+ lower_bound,
508
+ upper_bound
509
+ ]
510
+ end
511
+
512
+ # match operators constraints
513
+ if matches = /^(<>|!=|>=?|<=?|==?)?\s*(.*)/.match(constraint)
514
+ begin
515
+ version = normalize(matches[2])
516
+ stability = VersionParser::parse_stability(version)
517
+ if !stability_modifier.nil? && !stability_modifier.empty? && (stability === 'stable')
518
+ version << "-#{stability_modifier}"
519
+ elsif matches[1] === '<'
520
+ unless /-#{MODIFIER_REGEX}$/.match(matches[2].downcase)
521
+ version << '-dev'
522
+ end
523
+ end
524
+ operator = matches[1].nil? ? '=' : matches[1]
525
+ return [Composer::Package::LinkConstraint::VersionConstraint.new(operator, version)]
526
+ rescue Exception => e
527
+ end
528
+ end
529
+
530
+ message = "Could not parse version constraint #{constraint}"
531
+ message << ": #{e.message}" if e
532
+ raise UnexpectedValueError, message
533
+ end
534
+
535
+ # Increment, decrement, or simply pad a version number.
536
+ # Support function for {@link parse_constraint()}
537
+ #
538
+ # Params:
539
+ # +matches+ Array with version parts in array indexes 1,2,3,4
540
+ # +position+ Integer 1,2,3,4 - which segment of the version to decrement
541
+ # +increment+ Integer
542
+ # +pad+ String The string to pad version parts after position
543
+ #
544
+ # Returns:
545
+ # string The new version
546
+ def manipulate_version_string(matches, position, increment = 0, pad = '0')
547
+ 4.downto(1).each do |i|
548
+ if i > position
549
+ matches[i] = pad
550
+ elsif i == position && increment
551
+ matches[i] = matches[i].to_i + increment
552
+ # If matches[i] was 0, carry the decrement
553
+ if matches[i] < 0
554
+ matches[i] = pad
555
+ position -= 1
556
+
557
+ # Return nil on a carry overflow
558
+ return nil if i == 1
559
+ end
560
+ end
561
+ end
562
+ "#{matches[1]}.#{matches[2]}.#{matches[3]}.#{matches[4]}"
563
+ end
564
+
565
+ def expand_stability(stability)
566
+ stability = stability.downcase
567
+ case stability
568
+ when 'a'
569
+ 'alpha'
570
+ when 'b'
571
+ 'beta'
572
+ when 'p', 'pl'
573
+ 'patch'
574
+ when 'rc'
575
+ 'RC'
576
+ else
577
+ stability
578
+ end
579
+ end
580
+ end
581
+ end
582
+ end
583
+ end