Commit 1ff178e9 authored by Dorian Krefft's avatar Dorian Krefft

Automatically detecting the most relevant plugin update for an existing instance of the IntelliJ.

The plugin resource will now check the exact version of the IntelliJ and
then will check, which update supports this version.
parent e4f7cf58
Pipeline #65329278 passed with stage
in 1 minute and 43 seconds
......@@ -192,16 +192,17 @@ proper location, so the user will not have to do it manually.
| install | This action will download the plugin and then install it inside the proper directory. Default action. It will not update the plugin if it is already installed. |
| update | Same as the `:install` action but will always update the plugin (even if it is already installed). |
| Property | Type | Description |
| ---------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| name | String | Name of the plugin. Should be defined as the resource's name. Has to match *exactly* the name displayed inside the plugins repository, otherwise Chef will raise an error. |
| workspace | String | Path for the IntelliJ's workspace. It is used to define the proper path for the plugins installations. Can be also set up as the "global workspace" path. |
| source | String | If set, the cookbook download the plugins from the different source than the official repository. |
| update | Integer | Version of the plugin to be downloaded. If empty, the cookbook will download the latest version. |
| edition | Symbol | Some plugins are available only for one edition. To make sure that the correct plugin will be installed, you should pass the edition parameter. Can be `:community` or `:ultimate`. |
| repository_filename | String | .zip plugins not always have the same file name after extracting as the zip file. To avoid downloading the plugin every time, you can set this property and the resource will check if the plugin is already installed. |
| user | String | The user used to install the plugin. |
| group | String | The group used to install the plugin. |
| Property | Type | Description |
| ---------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| name | String | Name of the plugin. Should be defined as the resource's name. Has to match *exactly* the name displayed inside the plugins repository, otherwise Chef will raise an error. |
| workspace | String | Path for the IntelliJ's workspace. It is used to define the proper path for the plugins installations. Can be also set up as the "global workspace" path. |
| source | String | If set, the cookbook download the plugins from the different source than the official repository. |
| update | Integer | Version of the plugin to be downloaded. If empty, the cookbook will download the latest version. |
| edition | Symbol | Some plugins are available only for one edition. To make sure that the correct plugin will be installed, you should pass the edition parameter. Can be `:community` or `:ultimate`. |
| repository_filename | String | .zip plugins not always have the same file name after extracting as the zip file. To avoid downloading the plugin every time, you can set this property and the resource will check if the plugin is already installed. |
| user | String | The user used to install the plugin. |
| group | String | The group used to install the plugin. |
| intellij_path | String | The path where the IntelliJ is installed. If this setting is provided, the resource will automatically detect the build number and edition of the IntelliJ and will try to install the most relevant plugin (will check, which version of the plugin supports the IntelliJ). |
Example:
......@@ -213,7 +214,7 @@ intellij_plugin 'Chef integration' do
action :install
end
intellij_plugin 'Docker integration' do
intellij_plugin 'Docker' do
workspace '/home/myuser/.IdeaIC14'
respository_filename 'Docker'
edition :community
......@@ -227,6 +228,12 @@ intellij_plugin 'Dart' do
source 'https://downloads/Dart.zip'
action :update
end
intellij_plugin 'Docker' do
workspace '/home/myuser/.IdeaIC14e'
intellij_path '/home/myuser/JetBrains/IntelliJ'
action :install
end
```
#### SDK
......
......@@ -25,6 +25,13 @@ module IntelliJ
end
end
class IntelliJNotFound < StandardError
def initialize(path)
message = "The IntelliJ was not found in the path '#{path}'!"
super(message)
end
end
class IntelliJPluginVersionNotFound < StandardError
def initialize(name, version, updates)
message = "The version '#{version}' for the IntelliJ '#{name}' plugin could not be found."
......
......@@ -4,6 +4,10 @@ require 'pathname'
module IntelliJ
module Helpers
def self.included(base)
base.extend self
end
def detect_vcs(path)
vcs_type = nil
vcs_type = :Git if ::File.exist?("#{path}/.git")
......@@ -43,6 +47,11 @@ module IntelliJ
"#{Dir.tmpdir}/#{::File.basename(filename)}"
end
def edition_from_code(code)
editions = { IU: :ultimate, IC: :community }
editions[code.to_sym]
end
def edition_short_code(edition)
editions = { ultimate: 'IU', community: 'IC' }
editions[edition.to_sym]
......@@ -63,7 +72,7 @@ module IntelliJ
file = "#{path}/product-info.json"
if ::File.exist?(file)
info = JSON.parse(::File.read(file))
result = [info['version'], info['productCode']]
result = [info['version'], info['productCode'], info['buildNumber']]
end
result
end
......@@ -101,5 +110,29 @@ module IntelliJ
to
end
end
def version_earlier?(first, second)
first, second = normalize_versions(first, second)
Gem::Version.new(first) <= Gem::Version.new(second)
end
def normalize_versions(first, second)
first_versions = first.split('.')
second_versions = second.split('.')
first_id = first_versions.find_index('*')
second_id = second_versions.find_index('*')
minimal = first_id && second_id ? [first_id, second_id].min : first_id || second_id
first_versions = drop_last_elements(first_versions, first_versions.length - minimal) if minimal
second_versions = drop_last_elements(second_versions, second_versions.length - minimal) if minimal
[first_versions.join('.'), second_versions.join('.')]
end
def drop_last_elements(array, amount)
amount = array.length if amount > array.length
array.reverse.drop(amount).reverse
end
end
end
......@@ -7,6 +7,7 @@ module IntelliJ
uri = URI(url)
response = Net::HTTP.get_response(uri)
status = response.code.to_i
Chef::Log.debug("Response from #{url} (status: #{status}):\n#{response.body}")
raise IntelliJ::Exceptions::GeneralApiError, response if status >= 400
JSON.parse(response.body)
end
......@@ -59,7 +60,8 @@ module IntelliJ
response = request_json_response("https://plugins.jetbrains.com/api/plugins/#{id}/updates")
result = {}
response.each do |update|
result[update['id']] = "https://plugins.jetbrains.com/files/#{update['file']}?updateId=#{update['id']}&pluginId=#{id}&family=#{family}"
result[update['id']] = update.merge({})
result[update['id']]['source'] = "https://plugins.jetbrains.com/files/#{update['file']}?updateId=#{update['id']}&pluginId=#{id}&family=#{family}"
end
result
end
......
......@@ -6,9 +6,20 @@ module IntelliJ
base.extend self
end
def get_default_source(name, edition, update)
def plugin_data_from_repository(name, edition, update, intellij_path)
intellij_build = nil
if intellij_path
_, edition_code, build_number = local_intellij_info(intellij_path)
edition = edition_from_code(edition_code)
intellij_build = build_number
end
plugin = find_plugin(name, edition)
get_download_link(plugin, update)
update = get_download_link(plugin, update, intellij_build)
update['edition'] = edition
update['intellij_build'] = intellij_build
update
end
def find_plugin(name, edition)
......@@ -17,9 +28,10 @@ module IntelliJ
suggestions[name]
end
def get_download_link(plugin, update)
def get_download_link(plugin, update, intellij_build)
updates = plugin_updates(plugin['id'], plugin['family'])
update ||= updates.keys[0]
update ||= latest_plugin_for_build(updates, intellij_build, plugin['name'])
raise IntelliJ::Exceptions::IntelliJPluginVersionNotFound.new(plugin['name'], update, updates.keys) unless updates[update.to_i]
updates[update.to_i]
end
......@@ -28,5 +40,22 @@ module IntelliJ
repo_name = repository_filename || ::File.basename(URI.parse(source).path)
::File.exist?("#{repository}/#{repo_name}")
end
def latest_plugin_for_build(updates, build_number, plugin_name)
matching = []
unless build_number.to_s.empty?
Chef::Log.debug("Finding updates matching build: #{build_number}. Updates:\n#{updates}")
updates.each do |id, update|
is_valid = version_earlier?(update['since'], build_number) && version_earlier?(build_number, update['until'])
matching.push(id) if is_valid
end
Chef::Log.warn("Could not find any plugin '#{plugin_name}' update for the IntelliJ #{build_number}! Most likely IntelliJ will ask you to update the plugin after the start. Using the latest (#{updates.keys[0]}) update.") if matching.empty?
end
matching.max || updates.keys[0]
end
end
end
......@@ -3,7 +3,7 @@ include IntelliJ::Helpers
module IntelliJ
module Shortcut
def default_shortcut_name(intellij_path)
version, edition_code = local_intellij_info(intellij_path)
version, edition_code, = local_intellij_info(intellij_path)
if version
editions = { IC: 'Community', IU: 'Ultimate' }
......
......@@ -30,6 +30,7 @@ end
intellij_plugin 'Batch Scripts Support' do
workspace workspace_path
intellij_path intellij_path
edition :community
user user
group group
......@@ -169,6 +170,7 @@ end
intellij_plugin 'Docker' do
workspace workspace_path
intellij_path intellij_path
edition :community
user user
group group
......
......@@ -19,7 +19,7 @@ action :install do
properties = @new_resource.properties
package_file = get_tmp_path(source)
local_version, local_edition = local_intellij_info(path)
local_version, local_edition, = local_intellij_info(path)
edition_code = edition_short_code(edition)
directory path do
......@@ -78,7 +78,7 @@ action :update do
_, _, version = summarize_release(args)
path = args.path
edition = args.edition
local_version, local_edition = local_intellij_info(path)
local_version, local_edition, = local_intellij_info(path)
if local_version != version || local_edition != edition_short_code(edition)
log "Removing the previous IntelliJ #{local_version} #{local_edition} edition in #{path}" do
......@@ -111,7 +111,7 @@ action :delete do
args = @new_resource
Chef::Application.fatal!('You must pass either source or version parameter in order to remove IntelliJ!', 42) if !args.path && !args.version
local_version, local_edition = local_intellij_info(args.path)
local_version, local_edition, = local_intellij_info(args.path)
log "Removing the previous IntelliJ #{local_version} #{local_edition} edition in #{args.path}" do
level :info
......
......@@ -3,6 +3,7 @@ default_action :install
property :plugin_name, String, name_property: true
property :workspace, String, required: true
property :intellij_path, String
property :source, String
property :update, Integer
property :edition, Symbol, equal_to: [:ultimate, :community], default: :community
......@@ -12,10 +13,28 @@ property :group, String
action :install do
args = @new_resource
source = args.source || get_default_source(args.plugin_name, args.edition, args.update)
raise IntelliJ::Exceptions::IntelliJNotFound, args.intellij_path if args.intellij_path && !::File.exist?(args.intellij_path)
repo = "#{args.workspace}/config/plugins"
source = args.source
if source
log "Installing the plugin #{args.plugin_name} from specific source: #{args.source}" do
level :info
end
else
plugin_data = plugin_data_from_repository(args.plugin_name, args.edition, args.update, args.intellij_path)
intellij_version = "IntelliJ #{plugin_data['edition']} edition #{plugin_data['intellij_build']}".strip
log "Installing the plugin #{args.plugin_name} for #{intellij_version} from IntelliJ's repository" do
level :info
end
source = plugin_data['source']
end
path = "#{repo}/#{::File.basename(URI.parse(source).path)}"
already_installed = plugin_installed?(repo, source, args.repository_filename)
directory repo do
recursive true
......@@ -23,14 +42,13 @@ action :install do
group args.group
end
remote_file path do
source source
user args.user
group args.group
not_if { already_installed }
end
unless plugin_installed?(repo, source, args.repository_filename)
remote_file path do
source source
user args.user
group args.group
end
if !already_installed && ::File.extname(path) == '.zip'
zipfile path do
into repo
overwrite true
......@@ -40,18 +58,39 @@ action :install do
Chef::Log.info("Skipping unzipping the plugin since it is already installed in #{"#{repo}/#{contents.first}"}'") if check
end
end
only_if { ::File.extname(path) == '.zip' }
end
file path do
action :delete
only_if { ::File.extname(path) == '.zip' }
end
end
end
action :update do
args = @new_resource
source = args.source || get_default_source(args.plugin_name, args.edition, args.update)
raise IntelliJ::Exceptions::IntelliJNotFound, args.intellij_path if args.intellij_path && !::File.exist?(args.intellij_path)
repo = "#{args.workspace}/config/plugins"
source = args.source
if source
log "Installing the plugin #{args.plugin_name} from specific source: #{args.source}" do
level :info
end
else
plugin_data = plugin_data_from_repository(args.plugin_name, args.edition, args.update, args.intellij_path)
intellij_version = "IntelliJ #{plugin_data['edition']} edition #{plugin_data['intellij_build']}".strip
log "Installing the plugin #{args.plugin_name} for #{intellij_version} from IntelliJ's repository" do
level :info
end
source = plugin_data['source']
end
path = "#{repo}/#{::File.basename(URI.parse(source).path)}"
directory repo do
......
require 'spec_helper'
require_relative '../../../../intellij/libraries/.helpers'
require_relative '../../../../intellij/libraries/.jetbrains.api.rb'
describe 'Testing the helpers library' do
before do
class HelpersLibrary
include IntelliJ::Helpers
end
end
context 'testing dropping last elements of the array' do
it 'returns original array if the amount is 0' do
array = [0, 1, 2, 3, 4]
result = HelpersLibrary.drop_last_elements(array, 0)
expect(result).to eq(array)
end
it 'shortens the array if the amount is not 0' do
array = [0, 1, 2, 3, 4]
result = HelpersLibrary.drop_last_elements(array, 2)
expect(result).to eq([0, 1, 2])
end
it 'clears the array if the amount is greater than its length' do
array = [0, 1, 2, 3, 4]
result = HelpersLibrary.drop_last_elements(array, 10)
expect(result).to eq([])
end
end
context 'testing the versions normalisation' do
it 'does nothing if the versions are specified fully' do
first = '10.0.1'
second = '10.1.2'
f_result, s_result = HelpersLibrary.normalize_versions(first, second)
expect(f_result).to eq(first)
expect(s_result).to eq(second)
end
it 'normalises both versions to unified format if at least one is defined with *' do
first = '10.0.*'
second = '10.1.2'
f_result, s_result = HelpersLibrary.normalize_versions(first, second)
expect(f_result).to eq('10.0')
expect(s_result).to eq('10.1')
end
it 'normalises both versions to minimal format if the * are defined in different places' do
first = '10.0.*'
second = '10.*'
f_result, s_result = HelpersLibrary.normalize_versions(first, second)
expect(f_result).to eq('10')
expect(s_result).to eq('10')
end
it 'should be resistant to weird format with versions after the *' do
first = '10.0.*'
second = '10.1.*.11.*'
f_result, s_result = HelpersLibrary.normalize_versions(first, second)
expect(f_result).to eq('10.0')
expect(s_result).to eq('10.1')
end
it 'should return empty strings if the first field is *' do
first = '10.0.*'
second = '*'
f_result, s_result = HelpersLibrary.normalize_versions(first, second)
expect(f_result).to be_empty
expect(s_result).to be_empty
end
end
context 'testing the extended versions comparision' do
it 'detects that the version is smaller with normal format' do
first = '10.0.1'
second = '10.1.2'
result = HelpersLibrary.version_earlier?(first, second)
expect(result).to be true
end
it 'detects that the version is bigger with normal format' do
first = '10.1.2'
second = '10.0.1'
result = HelpersLibrary.version_earlier?(first, second)
expect(result).to be false
end
it 'correctly manages the extended format comparision' do
first = '10.0.*'
second = '10.0.1'
result = HelpersLibrary.version_earlier?(first, second)
expect(result).to be true
end
end
end
......@@ -8,6 +8,33 @@ describe 'intellij_plugin' do
step_into :intellij_plugin
platform 'ubuntu'
context 'installing the plugin from the IntelliJ repository without defining the IntelliJ path' do
before do
allow_any_instance_of(IntelliJ::Plugins).to receive(:plugin_data_from_repository).with('Docker', :community, nil, nil).and_return(
'update' => 001,
'source' => 'http://downloads/Docker.zip',
'edition' => :community
)
end
recipe do
intellij_plugin 'Docker' do
workspace '/home/user/myworkspace'
edition :community
user 'myuser'
group 'mygroup'
action :install
end
end
it { is_expected.to install_intellij_plugin('Docker') }
it { is_expected.to write_log('Installing the plugin Docker for IntelliJ community edition from IntelliJ\'s repository') }
it { is_expected.to create_directory('/home/user/myworkspace/config/plugins').with(user: 'myuser', group: 'mygroup') }
it { is_expected.to create_remote_file('/home/user/myworkspace/config/plugins/Docker.zip').with(user: 'myuser', group: 'mygroup') }
it { is_expected.to extract_zipfile('/home/user/myworkspace/config/plugins/Docker.zip') }
it { is_expected.to delete_file('/home/user/myworkspace/config/plugins/Docker.zip') }
end
context 'installing the non-installed .jar plugin' do
recipe do
intellij_plugin 'My fancy plugin' do
......@@ -21,6 +48,7 @@ describe 'intellij_plugin' do
end
it { is_expected.to install_intellij_plugin('My fancy plugin') }
it { is_expected.to write_log('Installing the plugin My fancy plugin from specific source: http://downloads/plugin.jar') }
it { is_expected.to create_directory('/home/user/myworkspace/config/plugins').with(user: 'myuser', group: 'mygroup') }
it { is_expected.to create_remote_file('/home/user/myworkspace/config/plugins/plugin.jar').with(user: 'myuser', group: 'mygroup') }
it { is_expected.to_not extract_zipfile('/home/user/myworkspace/config/plugins/plugin.jar') }
......@@ -40,6 +68,7 @@ describe 'intellij_plugin' do
end
it { is_expected.to install_intellij_plugin('My fancy plugin') }
it { is_expected.to write_log('Installing the plugin My fancy plugin from specific source: http://downloads/plugin.zip') }
it { is_expected.to create_directory('/home/user/myworkspace/config/plugins').with(user: 'myuser', group: 'mygroup') }
it { is_expected.to create_remote_file('/home/user/myworkspace/config/plugins/plugin.zip').with(user: 'myuser', group: 'mygroup') }
it { is_expected.to extract_zipfile('/home/user/myworkspace/config/plugins/plugin.zip') }
......@@ -65,6 +94,7 @@ describe 'intellij_plugin' do
end
it { is_expected.to install_intellij_plugin('My fancy plugin') }
it { is_expected.to write_log('Installing the plugin My fancy plugin from specific source: http://downloads/plugin.zip') }
it { is_expected.to create_directory('/home/user/myworkspace/config/plugins').with(user: 'myuser', group: 'mygroup') }
it { is_expected.to create_remote_file('/home/user/myworkspace/config/plugins/plugin.zip').with(user: 'myuser', group: 'mygroup') }
it { is_expected.to_not extract_zipfile('/home/user/myworkspace/config/plugins/plugin.zip') }
......@@ -90,6 +120,7 @@ describe 'intellij_plugin' do
end
it { is_expected.to update_intellij_plugin('My fancy plugin') }
it { is_expected.to write_log('Installing the plugin My fancy plugin from specific source: http://downloads/plugin.zip') }
it { is_expected.to create_directory('/home/user/myworkspace/config/plugins').with(user: 'myuser', group: 'mygroup') }
it { is_expected.to create_remote_file('/home/user/myworkspace/config/plugins/plugin.zip').with(user: 'myuser', group: 'mygroup') }
it { is_expected.to extract_zipfile('/home/user/myworkspace/config/plugins/plugin.zip') }
......@@ -115,6 +146,7 @@ describe 'intellij_plugin' do
end
it { is_expected.to install_intellij_plugin('My fancy plugin') }
it { is_expected.to write_log('Installing the plugin My fancy plugin from specific source: http://downloads/plugin.zip') }
it { is_expected.to create_directory('/home/user/myworkspace/config/plugins').with(user: 'myuser', group: 'mygroup') }
it { is_expected.to_not create_remote_file('/home/user/myworkspace/config/plugins/plugin.zip').with(user: 'myuser', group: 'mygroup') }
it { is_expected.to_not extract_zipfile('/home/user/myworkspace/config/plugins/plugin.zip') }
......@@ -145,5 +177,24 @@ describe 'intellij_plugin' do
it { expect { subject }.to raise_exception IntelliJ::Exceptions::IntelliJPluginVersionNotFound }
end
context 'installing a plugin for a missing IntelliJ instance' do
before do
allow(File).to receive(:exist?).with(anything).and_call_original
allow(File).to receive(:exist?).with('/some/not/existing/directory').and_return false
end
recipe do
intellij_plugin 'Docker' do
workspace '/home/user/myworkspace'
intellij_path '/some/not/existing/directory'
update 1234567890123
edition :community
action :install
end
end
it { expect { subject }.to raise_exception IntelliJ::Exceptions::IntelliJNotFound }
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment