makit 0.0.162 → 0.0.164
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 +4 -4
- data/lib/makit/dotnet/cli.rb +155 -0
- data/lib/makit/dotnet.rb +8 -0
- data/lib/makit/nuget.rb +211 -0
- data/lib/makit/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5aa415801dae9c081791596f59c00947878f6ee7d76d3bb7515f6ea8e2a8e958
|
|
4
|
+
data.tar.gz: 12983cf08ef21f336c22bf3903a0e336acc0860cc201f55c5c742814c4e1c8e7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 79310ac260f739a81bb2a2fec375133a6e61635a0b936b27fc1f3d223755c1024e76f6bca5e5134b008adf700cf2808bf54672647f7fdaede6fb9b4094ed7dee
|
|
7
|
+
data.tar.gz: 3677481dfedddfbd200f5adc024477ea74fc8044c9caf0f643d913e0b868a633c4ad90438469b11f7e2dcc5c9a1fc8112aaa442457f1a95019ec6c1165103f61
|
data/lib/makit/dotnet/cli.rb
CHANGED
|
@@ -30,6 +30,161 @@ module Makit
|
|
|
30
30
|
end
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
# Installs a .NET global tool if it's not already installed
|
|
34
|
+
#
|
|
35
|
+
# @param tool_name [String] The name of the tool to install (e.g., "docfx", "dotnet-outdated-tool")
|
|
36
|
+
# @return [Boolean] true if the tool was installed or already exists, false if installation failed
|
|
37
|
+
def self.install_global_tool(tool_name)
|
|
38
|
+
raise ArgumentError, "tool_name is required" if tool_name.nil? || tool_name.strip.empty?
|
|
39
|
+
|
|
40
|
+
tool_name = tool_name.strip
|
|
41
|
+
|
|
42
|
+
# Check if tool is already installed
|
|
43
|
+
# Parse the output to find exact tool name matches
|
|
44
|
+
list_output = `dotnet tool list --global 2>&1`
|
|
45
|
+
if $CHILD_STATUS.success?
|
|
46
|
+
# The output format is typically:
|
|
47
|
+
# Package Id Version Commands
|
|
48
|
+
# ----------------------------------------
|
|
49
|
+
# docfx 2.x.x docfx
|
|
50
|
+
# Parse lines to find exact tool name matches
|
|
51
|
+
installed = list_output.lines.any? do |line|
|
|
52
|
+
# Match tool name at the start of a line (after optional whitespace)
|
|
53
|
+
# or as a word boundary to avoid substring matches
|
|
54
|
+
line.strip.split(/\s+/).first == tool_name
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
if installed
|
|
58
|
+
Makit::Logging.default_logger.info("Global tool '#{tool_name}' is already installed")
|
|
59
|
+
return true
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Install the tool
|
|
64
|
+
Makit::Logging.default_logger.info("Installing global tool '#{tool_name}'...")
|
|
65
|
+
"dotnet tool install --global #{tool_name}".run
|
|
66
|
+
$CHILD_STATUS.success?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Gets the currently installed version of a global tool
|
|
70
|
+
#
|
|
71
|
+
# @param tool_name [String] The name of the tool
|
|
72
|
+
# @return [String, nil] The installed version, or nil if not installed
|
|
73
|
+
def self.get_installed_tool_version(tool_name)
|
|
74
|
+
list_output = `dotnet tool list --global 2>&1`
|
|
75
|
+
return nil unless $CHILD_STATUS.success?
|
|
76
|
+
|
|
77
|
+
# Skip header lines
|
|
78
|
+
list_output.lines.each do |line|
|
|
79
|
+
line_stripped = line.strip
|
|
80
|
+
next if line_stripped.empty?
|
|
81
|
+
next if line_stripped.start_with?("Package") || line_stripped.start_with?("-")
|
|
82
|
+
|
|
83
|
+
# Split by whitespace - columns are: Package Id, Version, Commands
|
|
84
|
+
parts = line_stripped.split(/\s+/)
|
|
85
|
+
next if parts.empty?
|
|
86
|
+
|
|
87
|
+
# Match tool name at the start of the line
|
|
88
|
+
if parts.first == tool_name && parts.length >= 2
|
|
89
|
+
# Version is the second column
|
|
90
|
+
return parts[1]
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
nil
|
|
95
|
+
end
|
|
96
|
+
private_class_method :get_installed_tool_version
|
|
97
|
+
|
|
98
|
+
# Gets the latest available version of a tool from NuGet
|
|
99
|
+
#
|
|
100
|
+
# @param tool_name [String] The name of the tool
|
|
101
|
+
# @return [String, nil] The latest version, or nil if not found
|
|
102
|
+
def self.get_latest_tool_version(tool_name)
|
|
103
|
+
search_output = `dotnet tool search #{tool_name} --take 1 2>&1`
|
|
104
|
+
return nil unless $CHILD_STATUS.success?
|
|
105
|
+
|
|
106
|
+
# Parse output format:
|
|
107
|
+
# Package ID Latest Version Authors Downloads Verified
|
|
108
|
+
# -------------------------------------------------------------------------------------------------
|
|
109
|
+
# docfx 2.78.4 .NET Foundation and Contributors 4091347 x
|
|
110
|
+
search_output.lines.each do |line|
|
|
111
|
+
line_stripped = line.strip
|
|
112
|
+
next if line_stripped.empty?
|
|
113
|
+
next if line_stripped.start_with?("Package") || line_stripped.start_with?("-")
|
|
114
|
+
|
|
115
|
+
# Split by whitespace - columns are: Package ID, Latest Version, Authors, Downloads, Verified
|
|
116
|
+
parts = line_stripped.split(/\s+/)
|
|
117
|
+
next if parts.empty?
|
|
118
|
+
|
|
119
|
+
# Match tool name at the start of the line
|
|
120
|
+
if parts.first == tool_name && parts.length >= 2
|
|
121
|
+
# Latest Version is the second column
|
|
122
|
+
return parts[1]
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
nil
|
|
127
|
+
end
|
|
128
|
+
private_class_method :get_latest_tool_version
|
|
129
|
+
|
|
130
|
+
# Updates a .NET global tool to the latest version if a newer version is available
|
|
131
|
+
#
|
|
132
|
+
# If a newer version is available, the tool will be uninstalled and then reinstalled
|
|
133
|
+
# to ensure the latest version is installed.
|
|
134
|
+
#
|
|
135
|
+
# @param tool_name [String] The name of the tool to update (e.g., "docfx", "dotnet-outdated-tool")
|
|
136
|
+
# @return [Boolean] true if the tool was updated or already at latest version, false if update failed
|
|
137
|
+
def self.update_global_tool(tool_name)
|
|
138
|
+
raise ArgumentError, "tool_name is required" if tool_name.nil? || tool_name.strip.empty?
|
|
139
|
+
|
|
140
|
+
tool_name = tool_name.strip
|
|
141
|
+
|
|
142
|
+
# Get currently installed version
|
|
143
|
+
installed_version = get_installed_tool_version(tool_name)
|
|
144
|
+
|
|
145
|
+
if installed_version.nil?
|
|
146
|
+
Makit::Logging.default_logger.info("Global tool '#{tool_name}' is not installed, installing...")
|
|
147
|
+
return install_global_tool(tool_name)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Get latest available version
|
|
151
|
+
latest_version = get_latest_tool_version(tool_name)
|
|
152
|
+
|
|
153
|
+
if latest_version.nil?
|
|
154
|
+
Makit::Logging.default_logger.warn("Could not determine latest version for '#{tool_name}', skipping update")
|
|
155
|
+
return false
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Compare versions
|
|
159
|
+
begin
|
|
160
|
+
versions_to_compare = [installed_version, latest_version]
|
|
161
|
+
highest_version = Makit::Version.get_highest_version(versions_to_compare)
|
|
162
|
+
|
|
163
|
+
if highest_version == installed_version
|
|
164
|
+
Makit::Logging.default_logger.info("Global tool '#{tool_name}' is already at latest version (#{installed_version})")
|
|
165
|
+
return true
|
|
166
|
+
end
|
|
167
|
+
rescue => e
|
|
168
|
+
Makit::Logging.default_logger.warn("Could not compare versions for '#{tool_name}': #{e.message}, attempting update anyway")
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Newer version available - uninstall and reinstall
|
|
172
|
+
Makit::Logging.default_logger.info("Updating global tool '#{tool_name}' from #{installed_version} to #{latest_version}...")
|
|
173
|
+
|
|
174
|
+
# Uninstall the tool
|
|
175
|
+
Makit::Logging.default_logger.info("Uninstalling global tool '#{tool_name}'...")
|
|
176
|
+
"dotnet tool uninstall --global #{tool_name}".run
|
|
177
|
+
unless $CHILD_STATUS.success?
|
|
178
|
+
Makit::Logging.default_logger.error("Failed to uninstall global tool '#{tool_name}'")
|
|
179
|
+
return false
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Install the latest version
|
|
183
|
+
Makit::Logging.default_logger.info("Installing global tool '#{tool_name}' version #{latest_version}...")
|
|
184
|
+
"dotnet tool install --global #{tool_name}".run
|
|
185
|
+
$CHILD_STATUS.success?
|
|
186
|
+
end
|
|
187
|
+
|
|
33
188
|
#
|
|
34
189
|
# generate docs/DotNet.md
|
|
35
190
|
#
|
data/lib/makit/dotnet.rb
CHANGED
|
@@ -98,5 +98,13 @@ module Makit
|
|
|
98
98
|
def self.generate_docs
|
|
99
99
|
CLI.generate_docs
|
|
100
100
|
end
|
|
101
|
+
|
|
102
|
+
def self.install_global_tool(tool_name)
|
|
103
|
+
CLI.install_global_tool(tool_name)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def self.update_global_tool(tool_name)
|
|
107
|
+
CLI.update_global_tool(tool_name)
|
|
108
|
+
end
|
|
101
109
|
end
|
|
102
110
|
end
|
data/lib/makit/nuget.rb
CHANGED
|
@@ -239,5 +239,216 @@ module Makit
|
|
|
239
239
|
# Add the source
|
|
240
240
|
add_source(name, url, username: username, password: password, store_password_in_clear_text: store_password_in_clear_text)
|
|
241
241
|
end
|
|
242
|
+
|
|
243
|
+
# Migrates packages from one NuGet source to another, matching a pattern
|
|
244
|
+
#
|
|
245
|
+
# For local directory feeds, packages are found by scanning the directory structure:
|
|
246
|
+
# {source_path}/{package_name}/{version}/{package_name}.{version}.nupkg
|
|
247
|
+
#
|
|
248
|
+
# @param source_name [String] The name of the source to migrate from
|
|
249
|
+
# @param package_pattern [String] The package name pattern to match (e.g., "rep*", "MyPackage.*")
|
|
250
|
+
# @param destination_name [String] The name of the destination source
|
|
251
|
+
# @return [Integer] The number of packages migrated, or -1 if an error occurred
|
|
252
|
+
def self.migrate_packages(source_name, package_pattern, destination_name)
|
|
253
|
+
raise ArgumentError, "source_name is required" if source_name.nil? || source_name.strip.empty?
|
|
254
|
+
raise ArgumentError, "package_pattern is required" if package_pattern.nil? || package_pattern.strip.empty?
|
|
255
|
+
raise ArgumentError, "destination_name is required" if destination_name.nil? || destination_name.strip.empty?
|
|
256
|
+
|
|
257
|
+
source_name = source_name.strip
|
|
258
|
+
package_pattern = package_pattern.strip
|
|
259
|
+
destination_name = destination_name.strip
|
|
260
|
+
|
|
261
|
+
# Get source information
|
|
262
|
+
sources = list_sources
|
|
263
|
+
source_info = sources.find { |s| s[:name] == source_name }
|
|
264
|
+
destination_info = sources.find { |s| s[:name] == destination_name }
|
|
265
|
+
|
|
266
|
+
unless source_info
|
|
267
|
+
Makit::Logging.default_logger.error("Source '#{source_name}' not found")
|
|
268
|
+
return -1
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
unless destination_info
|
|
272
|
+
Makit::Logging.default_logger.error("Destination source '#{destination_name}' not found")
|
|
273
|
+
return -1
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
source_path = source_info[:url]
|
|
277
|
+
destination_path = destination_info[:url]
|
|
278
|
+
|
|
279
|
+
# Source must be a local directory feed (not a URL)
|
|
280
|
+
unless source_path && !source_path.match?(/^https?:\/\//)
|
|
281
|
+
Makit::Logging.default_logger.error("Source '#{source_name}' is not a local directory feed (URL: #{source_path})")
|
|
282
|
+
return -1
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Expand and normalize the source path
|
|
286
|
+
source_path = File.expand_path(source_path)
|
|
287
|
+
|
|
288
|
+
# Destination can be either local directory feed or remote feed (Azure DevOps, NuGet.org, etc.)
|
|
289
|
+
is_remote_destination = destination_path && destination_path.match?(/^https?:\/\//)
|
|
290
|
+
|
|
291
|
+
# Ensure source directory exists
|
|
292
|
+
unless Dir.exist?(source_path)
|
|
293
|
+
Makit::Logging.default_logger.error("Source directory does not exist: #{source_path}")
|
|
294
|
+
return -1
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
Makit::Logging.default_logger.info("Searching for packages matching '#{package_pattern}' in source '#{source_name}' (#{source_path})")
|
|
298
|
+
Makit::Logging.default_logger.info("Destination: #{destination_name} (#{destination_path}) - #{is_remote_destination ? 'Remote feed' : 'Local directory feed'}")
|
|
299
|
+
|
|
300
|
+
# Ensure destination directory exists (only for local feeds)
|
|
301
|
+
unless is_remote_destination
|
|
302
|
+
FileUtils.mkdir_p(destination_path) unless Dir.exist?(destination_path)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Find all .nupkg files matching the pattern
|
|
306
|
+
# Simply search recursively for all .nupkg files and match package names
|
|
307
|
+
matching_packages = []
|
|
308
|
+
|
|
309
|
+
# Glob for all .nupkg files recursively in the source directory
|
|
310
|
+
Dir.glob(File.join(source_path, "**", "*.nupkg")) do |nupkg_file|
|
|
311
|
+
# Extract package name from the file path
|
|
312
|
+
# Path structure is typically: {source_path}/{package_name}/{version}/{package_name}.{version}.nupkg
|
|
313
|
+
relative_path = nupkg_file.sub(/^#{Regexp.escape(source_path)}[\/\\]?/, "")
|
|
314
|
+
path_parts = relative_path.split(/[\/\\]/)
|
|
315
|
+
|
|
316
|
+
# Get package name from directory structure (first directory after source_path)
|
|
317
|
+
package_name = path_parts.length >= 1 ? path_parts[0] : nil
|
|
318
|
+
|
|
319
|
+
# If we can't get it from path, try to extract from filename
|
|
320
|
+
if package_name.nil? || package_name.empty?
|
|
321
|
+
filename = File.basename(nupkg_file, ".nupkg")
|
|
322
|
+
# Try to extract package name by removing version suffix (e.g., "1.2.3" or "1.2.3-beta")
|
|
323
|
+
if filename.match?(/^(.+?)\.(\d+\.\d+.*)$/)
|
|
324
|
+
package_name = $1
|
|
325
|
+
else
|
|
326
|
+
package_name = filename
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Check if package name matches pattern
|
|
331
|
+
if package_name && File.fnmatch?(package_pattern, package_name, File::FNM_DOTMATCH)
|
|
332
|
+
# Extract version from path (second directory) or from filename
|
|
333
|
+
version = path_parts.length >= 2 ? path_parts[1] : nil
|
|
334
|
+
|
|
335
|
+
# If we couldn't extract version from path, try from filename
|
|
336
|
+
if version.nil? || version.empty?
|
|
337
|
+
filename = File.basename(nupkg_file, ".nupkg")
|
|
338
|
+
if filename.match?(/^.+?\.(\d+\.\d+.*)$/)
|
|
339
|
+
version = $1
|
|
340
|
+
else
|
|
341
|
+
version = "unknown"
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
matching_packages << {
|
|
346
|
+
package_name: package_name,
|
|
347
|
+
version: version,
|
|
348
|
+
nupkg_path: nupkg_file
|
|
349
|
+
}
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
if matching_packages.empty?
|
|
354
|
+
Makit::Logging.default_logger.info("No packages found matching pattern '#{package_pattern}' in source '#{source_name}'")
|
|
355
|
+
return 0
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
Makit::Logging.default_logger.info("Found #{matching_packages.length} package(s) matching pattern '#{package_pattern}' in source '#{source_name}'")
|
|
359
|
+
|
|
360
|
+
# Migrate each package
|
|
361
|
+
migrated_count = 0
|
|
362
|
+
failed_count = 0
|
|
363
|
+
|
|
364
|
+
matching_packages.each do |package_info|
|
|
365
|
+
package_name = package_info[:package_name]
|
|
366
|
+
version = package_info[:version]
|
|
367
|
+
nupkg_path = package_info[:nupkg_path]
|
|
368
|
+
|
|
369
|
+
Makit::Logging.default_logger.info("Migrating #{package_name}.#{version} from '#{source_name}' to '#{destination_name}'...")
|
|
370
|
+
|
|
371
|
+
if is_remote_destination
|
|
372
|
+
# For remote feeds (Azure DevOps, NuGet.org, etc.), use dotnet nuget push
|
|
373
|
+
# Azure DevOps requires --api-key az (placeholder) and --skip-duplicate
|
|
374
|
+
Makit::Logging.default_logger.info(" Pushing #{package_name}.#{version} to remote feed...")
|
|
375
|
+
|
|
376
|
+
args = ["dotnet", "nuget", "push", nupkg_path, "--source", destination_path, "--skip-duplicate"]
|
|
377
|
+
|
|
378
|
+
# For Azure DevOps feeds, add --api-key az (required placeholder)
|
|
379
|
+
if destination_path.include?("dev.azure.com") || destination_path.include?("pkgs.dev.azure.com")
|
|
380
|
+
args << "--api-key" << "az"
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
stdout, stderr, status = Open3.capture3(*args)
|
|
384
|
+
|
|
385
|
+
if status.success?
|
|
386
|
+
Makit::Logging.default_logger.info(" Successfully migrated #{package_name}.#{version} to remote feed")
|
|
387
|
+
migrated_count += 1
|
|
388
|
+
else
|
|
389
|
+
# Check if it's a duplicate (which is okay with --skip-duplicate)
|
|
390
|
+
if stderr.include?("already exists") || stderr.include?("duplicate")
|
|
391
|
+
Makit::Logging.default_logger.info(" Package already exists in destination, skipping: #{package_name}.#{version}")
|
|
392
|
+
migrated_count += 1
|
|
393
|
+
else
|
|
394
|
+
Makit::Logging.default_logger.error(" Failed to migrate #{package_name}.#{version}: #{stderr}")
|
|
395
|
+
failed_count += 1
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
else
|
|
399
|
+
# For local directory feeds, use 'nuget add' which automatically organizes packages
|
|
400
|
+
# into the hierarchical structure: {packageID}/{version}/packageID.version.nupkg
|
|
401
|
+
target_package_dir = File.join(destination_path, package_name.downcase, version)
|
|
402
|
+
target_package_path = File.join(target_package_dir, File.basename(nupkg_path).downcase)
|
|
403
|
+
|
|
404
|
+
if File.exist?(target_package_path)
|
|
405
|
+
Makit::Logging.default_logger.info(" Package already exists in destination, skipping: #{package_name}.#{version}")
|
|
406
|
+
migrated_count += 1
|
|
407
|
+
next
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
stdout, stderr, status = Open3.capture3("nuget", "add", nupkg_path, "-Source", destination_path)
|
|
411
|
+
|
|
412
|
+
if status.success?
|
|
413
|
+
Makit::Logging.default_logger.info(" Successfully migrated and organized #{package_name}.#{version}")
|
|
414
|
+
migrated_count += 1
|
|
415
|
+
else
|
|
416
|
+
# Fallback to dotnet nuget push if nuget.exe is not available
|
|
417
|
+
nuget_exe = Makit::Environment.which("nuget")
|
|
418
|
+
if nuget_exe.nil? || nuget_exe.empty?
|
|
419
|
+
Makit::Logging.default_logger.info(" nuget.exe not found, falling back to dotnet nuget push")
|
|
420
|
+
|
|
421
|
+
# Manually organize package into proper directory structure
|
|
422
|
+
Makit::Logging.default_logger.info(" Organizing package into directory structure: #{target_package_dir}")
|
|
423
|
+
FileUtils.mkdir_p(target_package_dir)
|
|
424
|
+
FileUtils.cp(nupkg_path, target_package_path)
|
|
425
|
+
|
|
426
|
+
# Push using dotnet nuget push from the organized location
|
|
427
|
+
stdout, stderr, status = Open3.capture3("dotnet", "nuget", "push", target_package_path, "--source", destination_path)
|
|
428
|
+
|
|
429
|
+
if status.success?
|
|
430
|
+
Makit::Logging.default_logger.info(" Successfully migrated and organized #{package_name}.#{version}")
|
|
431
|
+
migrated_count += 1
|
|
432
|
+
else
|
|
433
|
+
Makit::Logging.default_logger.warn(" Package copied to organized structure but dotnet nuget push failed: #{stderr}")
|
|
434
|
+
Makit::Logging.default_logger.warn(" Package is available at #{target_package_path} but may need manual indexing")
|
|
435
|
+
migrated_count += 1
|
|
436
|
+
end
|
|
437
|
+
else
|
|
438
|
+
Makit::Logging.default_logger.error(" Failed to migrate #{package_name}.#{version}: #{stderr}")
|
|
439
|
+
failed_count += 1
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
if failed_count > 0
|
|
446
|
+
Makit::Logging.default_logger.warn("Migration completed with #{failed_count} failure(s). #{migrated_count} package(s) migrated successfully.")
|
|
447
|
+
else
|
|
448
|
+
Makit::Logging.default_logger.info("Successfully migrated #{migrated_count} package(s) from '#{source_name}' to '#{destination_name}'")
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
migrated_count
|
|
452
|
+
end
|
|
242
453
|
end
|
|
243
454
|
end
|
data/lib/makit/version.rb
CHANGED