tractive 1.0.5 → 1.0.9

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: db1fec86bca93ac81ac4ce8c4f1e6fb99f0983743c55fc3275a95188c8178e0e
4
- data.tar.gz: f2907430c9a64b4cd5e0004050c1d7eddf11a18f94a4a007bf5315706a73e23b
3
+ metadata.gz: 5ebcddc1621be979b25f4041cc4eac2429e6a6a08b05611154c230d49d9de86c
4
+ data.tar.gz: f61bd5af4741266ae56d81c277c7aaec723a49358f81dcb6eebdc3dbaae2e37c
5
5
  SHA512:
6
- metadata.gz: 1828a51469e9418188cd72195b51b245bb47921b7fbc883287bd089f9b2da58d18fbf08411c9f5cc33ed4a4a92259e365931afe281e1cf810a16c97a0632fa1a
7
- data.tar.gz: f898f265b58c41ca3ae59ee20cf52ec50ced92045a1595f23ea50e66bb4b0bd911a8695cfdc34b3a41a248a8b11bcb3a20be70eb8193aba8a7867c3f9ba4cc1e
6
+ metadata.gz: b29d97cc67c43cc4d9ee72d88563e01dc6d7cdc7b988ed3b987c73a3cbc9a2fc8746597c3d3b1e175ebca83073fd6e4b19f8b51f60c9792849893a6816483f5d
7
+ data.tar.gz: d07192c1b845a3a4ca266e881ead6a7583ba5575313d68372ffa9df6785c0f4e8856be09cafdf736bb279e5e651a4d3eb1e6f294b0754b0dd4c3a5dfded9ac03
data/.gitignore CHANGED
@@ -9,3 +9,4 @@
9
9
 
10
10
  # rspec failure tracking
11
11
  .rspec_status
12
+ Gemfile.lock
data/.rubocop.yml CHANGED
@@ -25,12 +25,18 @@ Style/Documentation:
25
25
  Style/GlobalVars:
26
26
  Enabled: false
27
27
 
28
+ Style/HashTransformKeys:
29
+ Enabled: false
30
+
28
31
  Metrics/AbcSize:
29
32
  Enabled: false
30
33
 
31
34
  Metrics/MethodLength:
32
35
  Enabled: false
33
36
 
37
+ Metrics/ModuleLength:
38
+ Max: 150
39
+
34
40
  Security/Open:
35
41
  Enabled: false
36
42
 
data/README.adoc CHANGED
@@ -297,6 +297,8 @@ trac:
297
297
  ticketbaseurl: https://example.org/trac/foobar/ticket
298
298
  ----
299
299
 
300
+ `ticketbaseurl:`::: The Trac Url which will be added in Github issues. A link will be added in the footer of Github issue to link it to Trac ticket.
301
+
300
302
  [[config-github]]
301
303
  ==== GitHub configuration
302
304
 
@@ -680,6 +682,7 @@ The following options are allowed (at least one necessary):
680
682
  * `column-name`
681
683
  * `operator`
682
684
  * `column-value`
685
+ * `include-null`
683
686
 
684
687
  | Boolean
685
688
 
@@ -695,6 +698,10 @@ The following options are allowed (at least one necessary):
695
698
  | Value of the column to filter.
696
699
  | String
697
700
 
701
+ | `--include-null`
702
+ | Include rows having null value in filtered column.
703
+ | Boolean
704
+
698
705
  | `-h`, `help`
699
706
  | Display the Tractive help message, or you can provide a command to know more
700
707
  about a single command via `tractive help {command}`.
data/db/trac-test.db CHANGED
Binary file
data/exe/tractive CHANGED
@@ -30,6 +30,8 @@ class TractiveCommand < Thor
30
30
  desc: "Operator for filter."
31
31
  method_option "columnvalue", type: :string, aliases: ["--column-value"],
32
32
  desc: "Value of the column to filter."
33
+ method_option "includenull", type: :boolean, aliases: ["--include-null"],
34
+ desc: "Flag for including null values in the filter result."
33
35
 
34
36
  method_option "importfromfile", type: :string, aliases: ["-I", "--import-from-file"],
35
37
  desc: "Import issues from a json file"
data/lib/tractive/info.rb CHANGED
@@ -34,8 +34,8 @@ module Tractive
34
34
  "milestones" => milestones,
35
35
  "labels" => {
36
36
  "type" => Utilities.make_hash("type_", types),
37
- "component" => Utilities.make_hash("component_", components),
38
37
  "resolution" => Utilities.make_hash("resolution_", resolutions),
38
+ "component" => Utilities.make_each_hash(components, %w[name color], "component: "),
39
39
  "severity" => Utilities.make_each_hash(severity, %w[name color]),
40
40
  "priority" => Utilities.make_each_hash(priorities, %w[name color]),
41
41
  "tracstate" => Utilities.make_each_hash(tracstates, %w[name color])
data/lib/tractive/main.rb CHANGED
@@ -8,7 +8,7 @@ module Tractive
8
8
  @opts = opts
9
9
  @cfg = YAML.load_file(@opts[:config])
10
10
 
11
- Tractive::Utilities.setup_logger(output_stream: @opts[:log_file] || $stderr, verbose: @opts[:verbose])
11
+ Tractive::Utilities.setup_logger(output_stream: @opts[:logfile] || $stderr, verbose: @opts[:verbose])
12
12
  @db = Tractive::Utilities.setup_db!(@cfg["trac"]["database"])
13
13
  rescue Sequel::DatabaseConnectionError, Sequel::AdapterNotFound, URI::InvalidURIError, Sequel::DatabaseError => e
14
14
  $logger.error e.message
@@ -6,7 +6,7 @@ module Migrator
6
6
  def initialize(args)
7
7
  @tracticketbaseurl = args[:cfg]["trac"]["ticketbaseurl"]
8
8
  @attachurl = args[:opts][:attachurl] || args[:cfg].dig("attachments", "url")
9
- @changeset_base_url = args[:cfg]["trac"]["changeset_base_url"]
9
+ @changeset_base_url = args[:cfg]["trac"]["changeset_base_url"] || ""
10
10
  @singlepost = args[:opts][:singlepost]
11
11
  @labels_cfg = args[:cfg]["labels"].transform_values(&:to_h)
12
12
  @milestonesfromtrac = args[:cfg]["milestones"]
@@ -15,14 +15,17 @@ module Migrator
15
15
  @repo = args[:cfg]["github"]["repo"]
16
16
  @client = GithubApi::Client.new(access_token: args[:cfg]["github"]["token"])
17
17
  @wiki_attachments_url = args[:cfg]["trac"]["wiki_attachments_url"]
18
+ @revmap_file_path = args[:opts][:revmapfile] || args[:cfg]["revmap_path"]
19
+ @attachment_options = { hashed: args[:cfg].dig("attachments", "hashed") }
18
20
 
19
21
  load_milestone_map
20
22
  create_labels_on_github(@labels_cfg["severity"].values)
21
23
  create_labels_on_github(@labels_cfg["priority"].values)
22
24
  create_labels_on_github(@labels_cfg["tracstate"].values)
25
+ create_labels_on_github(@labels_cfg["component"].values)
23
26
 
24
27
  @uri_parser = URI::Parser.new
25
- @twf_to_markdown = Migrator::Converter::TwfToMarkdown.new(@tracticketbaseurl, @attachurl, @changeset_base_url, @wiki_attachments_url)
28
+ @twf_to_markdown = Migrator::Converter::TwfToMarkdown.new(@tracticketbaseurl, @attachurl, @changeset_base_url, @wiki_attachments_url, @revmap_file_path)
26
29
  end
27
30
 
28
31
  def compose(ticket)
@@ -77,7 +80,6 @@ module Migrator
77
80
 
78
81
  badges = Set[]
79
82
 
80
- badges.add(@labels_cfg.fetch("component", {})[ticket[:component]])
81
83
  badges.add(@labels_cfg.fetch("type", {})[ticket[:type]])
82
84
  badges.add(@labels_cfg.fetch("resolution", {})[ticket[:resolution]])
83
85
  badges.add(@labels_cfg.fetch("version", {})[ticket[:version]])
@@ -85,6 +87,8 @@ module Migrator
85
87
  labels.add(@labels_cfg.fetch("severity", {})[ticket[:severity]])
86
88
  labels.add(@labels_cfg.fetch("priority", {})[ticket[:priority]])
87
89
  labels.add(@labels_cfg.fetch("tracstate", {})[ticket[:status]])
90
+ labels.add(@labels_cfg.fetch("component", {})[ticket[:component]])
91
+
88
92
  labels.delete(nil)
89
93
 
90
94
  keywords = ticket[:keywords]
@@ -100,7 +104,7 @@ module Migrator
100
104
  milestone = @milestonemap[ticket[:milestone]]
101
105
 
102
106
  # compute footer
103
- footer = "_Issue migrated from trac:#{ticket[:id]} at #{Time.now}_"
107
+ footer = "_Issue migrated from #{trac_ticket_link(ticket)} at #{Time.now}_"
104
108
 
105
109
  # compute badgetabe
106
110
  #
@@ -201,8 +205,17 @@ module Migrator
201
205
  def create_labels_on_github(labels)
202
206
  return if labels.nil? || labels.empty?
203
207
 
204
- existing_labels = @client.labels(@repo, per_page: 100).map { |label| label["name"] }
205
- new_labels = labels.reject { |label| existing_labels.include?(label["name"]) }
208
+ page = 1
209
+ existing_labels = []
210
+ result = @client.labels(@repo, per_page: 100, page: page).map { |label| label["name"] }
211
+
212
+ until result.empty?
213
+ existing_labels += result
214
+ page += 1
215
+ result = @client.labels(@repo, per_page: 100, page: page).map { |label| label["name"] }
216
+ end
217
+
218
+ new_labels = labels.reject { |label| existing_labels.include?(label["name"]&.strip) }
206
219
 
207
220
  new_labels.each do |label|
208
221
  params = { name: label["name"] }
@@ -262,7 +275,7 @@ module Migrator
262
275
  changeset = body.match(/In \[changeset:"(\d+)/).to_a[1]
263
276
  text += if changeset
264
277
  # changesethash = @revmap[changeset]
265
- "_committed #{Tractive::Utilities.map_changeset(changeset)}_"
278
+ "_committed #{Tractive::Utilities.map_changeset(changeset, @revmap, @changeset_base_url)}_"
266
279
  else
267
280
  "_commented_\n\n"
268
281
  end
@@ -275,7 +288,7 @@ module Migrator
275
288
  name = meta[:filename]
276
289
  body = meta[:description]
277
290
  if @attachurl
278
- url = @uri_parser.escape("#{@attachurl}/#{meta[:id]}/#{name}")
291
+ url = @uri_parser.escape("#{@attachurl}/#{attachment_path(meta[:id], name, @attachment_options)}")
279
292
  text += "[`#{name}`](#{url})"
280
293
  body += "\n![#{name}](#{url})" if [".png", ".jpg", ".gif"].include? File.extname(name).downcase
281
294
  else
@@ -321,6 +334,23 @@ module Migrator
321
334
  !(%w[keywords cc reporter version].include?(kind) ||
322
335
  (kind == "comment" && (newvalue.nil? || newvalue.lstrip.empty?)))
323
336
  end
337
+
338
+ def trac_ticket_link(ticket)
339
+ return "trac:#{ticket[:id]}" unless @tracticketbaseurl
340
+
341
+ "[trac:#{ticket[:id]}](#{@tracticketbaseurl}/#{ticket[:id]})"
342
+ end
343
+
344
+ def attachment_path(id, filename, options = {})
345
+ return "#{id}/#{filename}" unless options[:hashed]
346
+
347
+ folder_name = Digest::SHA1.hexdigest(id)
348
+ parent_folder_name = folder_name[0..2]
349
+ hashed_filename = Digest::SHA1.hexdigest(filename)
350
+ file_extension = File.extname(filename)
351
+
352
+ "#{parent_folder_name}/#{folder_name}/#{hashed_filename}#{file_extension}"
353
+ end
324
354
  end
325
355
  end
326
356
  end
@@ -4,11 +4,12 @@ module Migrator
4
4
  module Converter
5
5
  # twf => Trac wiki format
6
6
  class TwfToMarkdown
7
- def initialize(base_url, attach_url, changeset_base_url, wiki_attachments_url)
7
+ def initialize(base_url, attach_url, changeset_base_url, wiki_attachments_url, revmap_file_path)
8
8
  @base_url = base_url
9
9
  @attach_url = attach_url
10
10
  @changeset_base_url = changeset_base_url
11
11
  @wiki_attachments_url = wiki_attachments_url
12
+ @revmap = load_revmap_file(revmap_file_path)
12
13
  end
13
14
 
14
15
  def convert(str)
@@ -26,6 +27,24 @@ module Migrator
26
27
 
27
28
  private
28
29
 
30
+ def load_revmap_file(revmapfile)
31
+ # load revision mapping file and convert it to a hash.
32
+ # This revmap file allows to map between SVN revisions (rXXXX)
33
+ # and git commit sha1 hashes.
34
+ revmap = nil
35
+ if revmapfile
36
+ File.open(revmapfile, "r:UTF-8") do |f|
37
+ $logger.info("loading revision map #{revmapfile}")
38
+
39
+ revmap = f.each_line
40
+ .map { |line| line.split(/\s+\|\s+/) }
41
+ .map { |rev, sha| [rev.gsub(/^r/, ""), sha] }.to_h # remove leading "r" if present
42
+ end
43
+ end
44
+
45
+ revmap
46
+ end
47
+
29
48
  # CommitTicketReference
30
49
  def convert_ticket_reference(str)
31
50
  str.gsub!(/\{\{\{\n(#!CommitTicketReference .+?)\}\}\}/m, '\1')
@@ -72,9 +91,11 @@ module Migrator
72
91
  str.gsub!(%r{#{Regexp.quote(changeset_base_url)}/(\d+)/?}, '[changeset:\1]') if changeset_base_url
73
92
  str.gsub!(/\[changeset:"r(\d+)".*\]/, '[changeset:\1]')
74
93
  str.gsub!(/\[changeset:r(\d+)\]/, '[changeset:\1]')
75
- str.gsub!(/\br(\d+)\b/) { Tractive::Utilities.map_changeset(Regexp.last_match[1]) }
76
- str.gsub!(/\[changeset:"(\d+)".*\]/) { Tractive::Utilities.map_changeset(Regexp.last_match[1]) }
77
- str.gsub!(/\[changeset:"(\d+).*\]/) { Tractive::Utilities.map_changeset(Regexp.last_match[1]) }
94
+ str.gsub!(/\br(\d+)\b/) { Tractive::Utilities.map_changeset(Regexp.last_match[1], @revmap, changeset_base_url) }
95
+ str.gsub!(/\[changeset:"(\d+)".*\]/) { Tractive::Utilities.map_changeset(Regexp.last_match[1], @revmap, changeset_base_url) }
96
+ str.gsub!(/\[changeset:(\d+).*\]/) { Tractive::Utilities.map_changeset(Regexp.last_match[1], @revmap, changeset_base_url) }
97
+ str.gsub!(/\[(\d+)\]/) { Tractive::Utilities.map_changeset(Regexp.last_match[1], @revmap, changeset_base_url) }
98
+ str.gsub!(%r{\[(\d+)/.*\]}) { Tractive::Utilities.map_changeset(Regexp.last_match[1], @revmap, changeset_base_url) }
78
99
  end
79
100
 
80
101
  # Font styles
@@ -27,7 +27,7 @@ module Migrator
27
27
  input_file_name = args[:opts][:importfromfile]
28
28
 
29
29
  @filter_applied = args[:opts][:filter]
30
- @filter_options = { column_name: args[:opts][:columnname], operator: args[:opts][:operator], column_value: args[:opts][:columnvalue] }
30
+ @filter_options = { column_name: args[:opts][:columnname], operator: args[:opts][:operator], column_value: args[:opts][:columnvalue], include_null: args[:opts][:includenull] }
31
31
 
32
32
  @trac = Tractive::Trac.new(db)
33
33
  @repo = github["repo"]
@@ -87,7 +87,7 @@ module Migrator
87
87
 
88
88
  revmap = f.each_line
89
89
  .map { |line| line.split(/\s+\|\s+/) }
90
- .map { |rev, sha, _| [rev.gsub(/^r/, ""), sha] }.to_h # remove leading "r" if present
90
+ .map { |rev, sha| [rev.gsub(/^r/, ""), sha] }.to_h # remove leading "r" if present
91
91
  end
92
92
  end
93
93
 
@@ -19,14 +19,18 @@ module Tractive
19
19
  def filter_column(options)
20
20
  return self if options.nil? || options.values.compact.empty?
21
21
 
22
- case options[:operator].downcase
23
- when "like"
24
- where { Sequel.like(options[:column_name].to_sym, options[:column_value]) }
25
- when "not like"
26
- where { ~Sequel.like(options[:column_name].to_sym, options[:column_value]) }
27
- else
28
- where { Sequel.lit("#{options[:column_name]} #{options[:operator]} '#{options[:column_value]}'") }
29
- end
22
+ query = case options[:operator].downcase
23
+ when "like"
24
+ Sequel.like(options[:column_name].to_sym, options[:column_value])
25
+ when "not like"
26
+ ~Sequel.like(options[:column_name].to_sym, options[:column_value])
27
+ else
28
+ Sequel.lit("#{options[:column_name]} #{options[:operator]} '#{options[:column_value]}'")
29
+ end
30
+
31
+ query = Sequel.|(query, { options[:column_name].to_sym => nil }) if options[:include_null]
32
+
33
+ where { query }
30
34
  end
31
35
  end
32
36
 
@@ -7,10 +7,10 @@ module Tractive
7
7
  array.map { |i| [i, "#{prefix}#{i}"] }.to_h
8
8
  end
9
9
 
10
- def make_each_hash(values, keys)
10
+ def make_each_hash(values, keys, prefix = "")
11
11
  values.map do |value|
12
12
  value = [value] unless value.is_a?(Array)
13
- [value[0], keys.zip(value).to_h]
13
+ [value[0], keys.zip(value.map { |v| "#{prefix}#{v}" }).to_h]
14
14
  end.to_h
15
15
  end
16
16
 
@@ -45,11 +45,13 @@ module Tractive
45
45
  end
46
46
 
47
47
  # returns the git commit hash for a specified revision (using revmap hash)
48
- def map_changeset(str)
49
- if @revmap&.key?(str)
50
- "[r#{str}](../commit/#{@revmap[str]}) #{@revmap[str]}"
48
+ def map_changeset(str, revmap, changeset_base_url = "")
49
+ if revmap&.key?(str)
50
+ base_url = changeset_base_url
51
+ base_url += "/" if base_url[-1] && base_url[-1] != "/"
52
+ "#{base_url}#{revmap[str].strip}"
51
53
  else
52
- str
54
+ "[#{str}]"
53
55
  end
54
56
  end
55
57
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tractive
4
- VERSION = "1.0.5"
4
+ VERSION = "1.0.9"
5
5
  end
data/tractive.gemspec CHANGED
@@ -16,6 +16,7 @@ Gem::Specification.new do |spec|
16
16
  spec.metadata["homepage_uri"] = spec.homepage
17
17
  spec.metadata["source_code_uri"] = "https://github.com/ietf-ribose/tractive"
18
18
  spec.metadata["changelog_uri"] = "https://github.com/ietf-ribose/tractive"
19
+ spec.metadata["rubygems_mfa_required"] = "true"
19
20
 
20
21
  # Specify which files should be added to the gem when it is released.
21
22
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tractive
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.5
4
+ version: 1.0.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-10-21 00:00:00.000000000 Z
11
+ date: 2021-12-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mysql2
@@ -124,7 +124,6 @@ files:
124
124
  - ".ruby-version"
125
125
  - CODE_OF_CONDUCT.md
126
126
  - Gemfile
127
- - Gemfile.lock
128
127
  - LICENSE.md
129
128
  - README.adoc
130
129
  - Rakefile
@@ -171,6 +170,7 @@ metadata:
171
170
  homepage_uri: https://github.com/ietf-ribose/tractive
172
171
  source_code_uri: https://github.com/ietf-ribose/tractive
173
172
  changelog_uri: https://github.com/ietf-ribose/tractive
173
+ rubygems_mfa_required: 'true'
174
174
  post_install_message:
175
175
  rdoc_options: []
176
176
  require_paths:
data/Gemfile.lock DELETED
@@ -1,100 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- tractive (1.0.5)
5
- mysql2
6
- ox
7
- rest-client
8
- sequel
9
- sqlite3
10
- thor
11
-
12
- GEM
13
- remote: https://rubygems.org/
14
- specs:
15
- addressable (2.8.0)
16
- public_suffix (>= 2.0.2, < 5.0)
17
- ast (2.4.2)
18
- coderay (1.1.3)
19
- crack (0.4.5)
20
- rexml
21
- diff-lcs (1.4.4)
22
- domain_name (0.5.20190701)
23
- unf (>= 0.0.5, < 1.0.0)
24
- hashdiff (1.0.1)
25
- http-accept (1.7.0)
26
- http-cookie (1.0.4)
27
- domain_name (~> 0.5)
28
- method_source (1.0.0)
29
- mime-types (3.3.1)
30
- mime-types-data (~> 3.2015)
31
- mime-types-data (3.2021.0901)
32
- mysql2 (0.5.3)
33
- netrc (0.11.0)
34
- ox (2.14.5)
35
- parallel (1.21.0)
36
- parser (3.0.2.0)
37
- ast (~> 2.4.1)
38
- pry (0.14.1)
39
- coderay (~> 1.1)
40
- method_source (~> 1.0)
41
- public_suffix (4.0.6)
42
- rainbow (3.0.0)
43
- rake (13.0.6)
44
- regexp_parser (2.1.1)
45
- rest-client (2.1.0)
46
- http-accept (>= 1.7.0, < 2.0)
47
- http-cookie (>= 1.0.2, < 2.0)
48
- mime-types (>= 1.16, < 4.0)
49
- netrc (~> 0.8)
50
- rexml (3.2.5)
51
- rspec (3.10.0)
52
- rspec-core (~> 3.10.0)
53
- rspec-expectations (~> 3.10.0)
54
- rspec-mocks (~> 3.10.0)
55
- rspec-core (3.10.1)
56
- rspec-support (~> 3.10.0)
57
- rspec-expectations (3.10.1)
58
- diff-lcs (>= 1.2.0, < 2.0)
59
- rspec-support (~> 3.10.0)
60
- rspec-mocks (3.10.2)
61
- diff-lcs (>= 1.2.0, < 2.0)
62
- rspec-support (~> 3.10.0)
63
- rspec-support (3.10.2)
64
- rubocop (1.22.1)
65
- parallel (~> 1.10)
66
- parser (>= 3.0.0.0)
67
- rainbow (>= 2.2.2, < 4.0)
68
- regexp_parser (>= 1.8, < 3.0)
69
- rexml
70
- rubocop-ast (>= 1.12.0, < 2.0)
71
- ruby-progressbar (~> 1.7)
72
- unicode-display_width (>= 1.4.0, < 3.0)
73
- rubocop-ast (1.12.0)
74
- parser (>= 3.0.1.1)
75
- ruby-progressbar (1.11.0)
76
- sequel (5.49.0)
77
- sqlite3 (1.4.2)
78
- thor (1.1.0)
79
- unf (0.1.4)
80
- unf_ext
81
- unf_ext (0.0.8)
82
- unicode-display_width (2.1.0)
83
- webmock (3.14.0)
84
- addressable (>= 2.8.0)
85
- crack (>= 0.3.2)
86
- hashdiff (>= 0.4.0, < 2.0.0)
87
-
88
- PLATFORMS
89
- ruby
90
-
91
- DEPENDENCIES
92
- pry
93
- rake (~> 13.0)
94
- rspec (~> 3.0)
95
- rubocop (~> 1.7)
96
- tractive!
97
- webmock (~> 3.14)
98
-
99
- BUNDLED WITH
100
- 2.1.2