# frozen_string_literal: true module Discourse VERSION_REGEXP ||= /\A\d+\.\d+\.\d+(\.beta\d+)?\z/ VERSION_COMPATIBILITY_FILENAME ||= ".discourse-compatibility" # work around reloader unless defined?(::Discourse::VERSION) module VERSION #:nodoc: STRING = "3.3.0.beta1-dev" PARTS = STRING.split(".") private_constant :PARTS MAJOR = PARTS[0].to_i MINOR = PARTS[1].to_i TINY = PARTS[2].to_i PRE = PARTS[3]&.split("-", 2)&.first DEV = PARTS[3]&.split("-", 2)&.second end end class InvalidVersionListError < StandardError end def self.has_needed_version?(current, needed) Gem::Version.new(current) >= Gem::Version.new(needed) end # lookup an external resource (theme/plugin)'s best compatible version # compatible resource files are YAML, in the format: # `discourse_version: plugin/theme git reference.` For example: # 2.5.0.beta6: c4a6c17 # 2.5.0.beta4: d1d2d3f # 2.5.0.beta2: bbffee # 2.4.4.beta6: some-other-branch-ref # 2.4.2.beta1: v1-tag def self.find_compatible_resource(version_list, target_version = ::Discourse::VERSION::STRING) return unless version_list.present? begin version_list = YAML.safe_load(version_list) rescue Psych::SyntaxError, Psych::DisallowedClass => e end raise InvalidVersionListError unless version_list.is_a?(Hash) version_list = version_list .transform_keys do |v| Gem::Requirement.parse(v) rescue Gem::Requirement::BadRequirementError => e raise InvalidVersionListError, "Invalid version specifier: #{v}" end .sort_by do |parsed_requirement, _| operator, version = parsed_requirement [version, operator == "<" ? 0 : 1] end parsed_target_version = Gem::Version.new(target_version) lowest_matching_entry = version_list.find do |parsed_requirement, target| req_operator, req_version = parsed_requirement req_operator = "<=" if req_operator == "=" if !%w[<= <].include?(req_operator) raise InvalidVersionListError, "Invalid version specifier operator for '#{req_operator} #{req_version}'. Operator must be one of <= or <" end resolved_requirement = Gem::Requirement.new("#{req_operator} #{req_version}") resolved_requirement.satisfied_by?(parsed_target_version) end return if lowest_matching_entry.nil? checkout_version = lowest_matching_entry[1] begin Discourse::Utils.execute_command "git", "check-ref-format", "--allow-onelevel", checkout_version rescue RuntimeError raise InvalidVersionListError, "Invalid ref name: #{checkout_version}" end checkout_version end # Find a compatible resource from a git repo def self.find_compatible_git_resource(path) return unless File.directory?("#{path}/.git") tree_info = Discourse::Utils.execute_command( "git", "-C", path, "ls-tree", "-l", "HEAD", Discourse::VERSION_COMPATIBILITY_FILENAME, ) blob_size = tree_info.split[3].to_i if blob_size > Discourse::MAX_METADATA_FILE_SIZE $stderr.puts "#{Discourse::VERSION_COMPATIBILITY_FILENAME} file in #{path} too big" return end compat_resource = Discourse::Utils.execute_command( "git", "-C", path, "show", "HEAD@{upstream}:#{Discourse::VERSION_COMPATIBILITY_FILENAME}", ) Discourse.find_compatible_resource(compat_resource) rescue InvalidVersionListError => e $stderr.puts "Invalid version list in #{path}" rescue Discourse::Utils::CommandError => e nil end end