diff --git a/lib/releaseme/project.rb b/lib/releaseme/project.rb index ccc38cc..dc1314e 100644 --- a/lib/releaseme/project.rb +++ b/lib/releaseme/project.rb @@ -1,231 +1,228 @@ #-- # Copyright (C) 2014-2020 Harald Sitter # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation; either version 2 of # the License or (at your option) version 3 or any later version # accepted by the membership of KDE e.V. (or its successor approved # by the membership of KDE e.V.), which shall act as a proxy # defined in Section 14 of version 3 of the license. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . #++ require 'net/http' require 'yaml' require_relative 'git' require_relative 'logable' require_relative 'projects_api' module ReleaseMe class Project prepend Logable @@configdir = "#{File.dirname(File.dirname(File.expand_path(__dir__)))}/projects/" # Project identifer found. nil if not resolved. attr_reader :identifier # VCS to use for this project attr_reader :vcs # Branch used for i18n trunk attr_reader :i18n_trunk # Branch used for i18n stable attr_reader :i18n_stable # Branch used for i18n lts (same as stable except for Plasma) attr_reader :i18n_lts # Path used for i18n. attr_reader :i18n_path # Creates a new Project. Nothing may be nil except for i18n_lts! def initialize(identifier:, vcs:, i18n_trunk:, i18n_stable:, i18n_path:, i18n_lts: nil) @identifier = identifier @vcs = vcs @i18n_trunk = i18n_trunk @i18n_stable = i18n_stable @i18n_lts = i18n_lts @i18n_path = i18n_path end def from_data(api_project) # FIXME: not defined in remote QQ id = File.basename(api_project.path) # Resolve git url. vcs = invent_or_git_vcs(api_project.repo) # FIXME: hack to get readonly. should be RO by default and # frontend scripts should opt-into RW by setting a property # on us if ENV.include?('RELEASEME_READONLY') vcs.repository = "https://anongit.kde.org/#{api_project.repo}" end i18n_trunk = api_project.i18n.trunk_kf5 i18n_stable = api_project.i18n.stable_kf5 # Figure out which i18n path to use. i18n_path = api_project.i18n.component return false if !i18n_path || i18n_path.empty? # LTS branch only used for Plasma so unless it's set in a config file # just use stable branch i18n_lts = i18n_path == 'kde-workspace' ? plasma_lts : i18n_stable Project.new(identifier: id, vcs: vcs, i18n_trunk: i18n_trunk, i18n_stable: i18n_stable, i18n_lts: i18n_lts, i18n_path: i18n_path) end def plasma_lts self.class.plasma_lts end class << self def plasma_lts ymlfile = "#{@@configdir}/plasma.yml" unless File.exist?(ymlfile) raise "Project file for Plasma not found [#{ymlfile}]." end data = YAML.load_file(ymlfile) data['i18n_lts'] end # Constructs a Project instance from the definition placed in # projects/project_name.yml # @param project_name name of the yml file to look for. This is not # reflected in the actual Project.identifier # @return Project never empty, raises exceptions when something goes wrong # @raise RuntimeError on every occasion ever. Unless something goes wrong # deep inside. def from_config(project_name) ymlfile = "#{@@configdir}/#{project_name}.yml" unless File.exist?(ymlfile) raise "Project file for #{project_name} not found [#{ymlfile}]." end data = YAML.load(File.read(ymlfile)) data = data.inject({}) do |tmphsh, (key, value)| key = key.downcase.to_sym if key == :vcs raise 'Vcs configuration has no type key.' unless value.key?('type') begin vcs_type = value.delete('type') require_relative vcs_type.downcase.to_s value = ReleaseMe.const_get(vcs_type).from_hash(value) rescue LoadError, RuntimeError => e raise "Failed to resolve the Vcs values #{value} -->\n #{e}" end end tmphsh[key] = value next tmphsh end Project.new(**data) end def from_data(api_project) # FIXME: not defined in remote QQ id = File.basename(api_project.path) # Resolve git url. vcs = invent_or_git_vcs(api_project.repo) # FIXME: hack to get readonly. should be RO by default and # frontend scripts should opt-into RW by setting a property # on us if ENV.include?('RELEASEME_READONLY') - vcs.repository = "https://anongit.kde.org/#{api_project.repo}" + vcs.repository = "https://invent.kde.org/#{api_project.repo}" end i18n_trunk = api_project.i18n.trunk_kf5 i18n_stable = api_project.i18n.stable_kf5 # Figure out which i18n path to use. i18n_path = api_project.i18n.component return false if !i18n_path || i18n_path.empty? # LTS branch only used for Plasma so unless it's set in a config file # just use stable branch i18n_lts = i18n_path == 'kde-workspace' ? plasma_lts : i18n_stable Project.new(identifier: id, vcs: vcs, i18n_trunk: i18n_trunk, i18n_stable: i18n_stable, i18n_lts: i18n_lts, i18n_path: i18n_path) end def from_xpath(id) # By default assume id is the name of a project and nothing else. # This means we'll get the project if there is a module AND a project # of the same name. More importantly this means listing recursively # is no longer a thing so releasing a "module" as a use case is not # supported. Also ids then need to be unique and from_find asserts that. # https://bugs.kde.org/show_bug.cgi?id=420501 warn 'from_xpath is deprecated; use from_find instead' from_find(id) end def from_find(id) ret = ProjectsAPI.find(id: id).collect do |path| from_data(ProjectsAPI.get(path)) end # Ensure project names are in fact unique. raise "Unexpectedly found multiple matches for #{id}" if ret.size > 1 ret rescue OpenURI::HTTPError => e return [] if e.io.status[0] == '404' # [0] is code, [1] msg raise e end # @param url [String] find all Projects associated with this repo url. # @return [Array] can be empty def from_repo_url(url) # Git URIs are all over the place so much so that standard URI cannot # accurately parse them, so bypass URI entirely and do a super nasty # split run to get the path. without_scheme = url.split('//', 2)[-1] repo = without_scheme.split('/', 2)[-1] repo = repo.gsub(/\.git$/, '') api_project = ProjectsAPI.get_by_repo(repo) [from_data(api_project)] rescue OpenURI::HTTPError => e return [] if e.io.status[0] == '404' # Not a thing raise e # Otherwise raise, the error was unexpected on an API level. end private def invent_or_git_vcs(repo) # Repos that have migrated to invent will respond to ls-remote, # repos that have not will not. See if the writable invent repo exists # if not, drop to git.kde.org. If the user doesn't have push access # to invent that will also trip up this check and they'll default # to git.kde.org. This is a bit unfortunate :| vcs = Git.new - vcs.repository = "git@invent.kde.org:kde/#{repo}" + vcs.repository = "git@invent.kde.org:#{repo}" return vcs if vcs.exist? - log_info 'Repo not writable on invent.kde.org. Defaulting to git.kde.org' - vcs = Git.new - vcs.repository = "git@git.kde.org:#{repo}" - vcs + raise 'Repo not writable on invent.kde.org. Something is wrong with url resolution' end end end end diff --git a/test/project_test.rb b/test/project_test.rb index 42bee51..e935257 100644 --- a/test/project_test.rb +++ b/test/project_test.rb @@ -1,332 +1,332 @@ #-- # Copyright (C) 2014-2015 Harald Sitter # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation; either version 2 of # the License or (at your option) version 3 or any later version # accepted by the membership of KDE e.V. (or its successor approved # by the membership of KDE e.V.), which shall act as a proxy # defined in Section 14 of version 3 of the license. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . #++ require 'fileutils' require_relative 'lib/testme' require_relative '../lib/releaseme/project' require_relative '../lib/releaseme/vcs' def j(*args) JSON.generate(*args) end def default_i18n { stable: nil, stableKF5: nil, trunk: nil, trunkKF5: 'master', component: 'default' } end def stub_projects_single(url) path = url.gsub('https://projects.kde.org/api/v1/projects/', '') stub_request(:get, url).to_return(body: j([path])) end def git_stubs # Do not let us hit live repos. - # We do repo progint to determine whether to use invent.kde.org or git.kde.org + # We do repo ls-remote to check the repo exists on invent.kde.org ReleaseMe::Git.any_instance.expects(:run).never # Pretend everything exists on invent success_status = mock() success_status.responds_like_instance_of(Process::Status) success_status.stubs(:success?).returns(true) ReleaseMe::Git.any_instance.stubs(:run).with do |args| next false unless args.include?('ls-remote') true end.returns(['', success_status]) end # FIXME: this should go somewhere central or into a class. having a meth in global scope sucks def stub_api git_stubs stub_request(:get, 'https://projects.kde.org/api/v1/projects/extragear/utils') .to_return(body: JSON.generate(%w[extragear/utils/yakuake extragear/utils/krusader extragear/utils/krecipes])) # Invalid. stub_request(:get, 'https://projects.kde.org/api/v1/projects/kitten') .to_return(status: 404) stub_request(:get, 'https://projects.kde.org/api/v1/find?id=kitten') .to_return(status: 404) stub_request(:get, 'https://projects.kde.org/api/v1/projects/utils') .to_return(status: 404) stub_request(:get, 'https://projects.kde.org/api/v1/find?id=utils') .to_return(status: 404) stub_request(:get, 'https://projects.kde.org/api/v1/projects/yakuake') .to_return(status: 404) stub_request(:get, 'https://projects.kde.org/api/v1/find?id=yakuake') .to_return(body: j(%w[extragear/utils/yakuake])) stub_request(:get, 'https://projects.kde.org/api/v1/project/networkmanager-qt') .to_return(status: 404) stub_request(:get, 'https://projects.kde.org/api/v1/find?id=networkmanager-qt') .to_return(body: j(%w[frameworks/networkmanager-qt])) stub_request(:get, 'https://projects.kde.org/api/v1/projects/ktp-contact-runner') .to_return(status: 404) stub_request(:get, 'https://projects.kde.org/api/v1/find?id=ktp-contact-runner') .to_return(body: j(%w[kde/kdenetwork/ktp-contact-runner])) stub_request(:get, 'https://projects.kde.org/api/v1/projects/extragear') .to_return(body: j(%w[extragear/utils/yakuake extragear/utils/krusader extragear/utils/krecipes extragear/network/telepathy/ktp1 extragear/network/telepathy/ktp2])) stub_request(:get, 'https://projects.kde.org/api/v1/projects/extragear/network/telepathy') .to_return(body: j(%w[extragear/network/telepathy/ktp1 extragear/network/telepathy/ktp2])) stub_projects_single('https://projects.kde.org/api/v1/projects/kde/kdegraphics/libs/libksane') stub_projects_single('https://projects.kde.org/api/v1/projects/extragear/network/telepathy/ktp1') stub_projects_single('https://projects.kde.org/api/v1/projects/extragear/network/telepathy/ktp2') stub_projects_single('https://projects.kde.org/api/v1/projects/networkmanager-qt') stub_projects_single('https://projects.kde.org/api/v1/projects/extragear/utils/yakuake') # By Project Path stub_request(:get, 'https://projects.kde.org/api/v1/project/extragear/utils/yakuake') .to_return(body: j(path: 'extragear/utils/yakuake', repo: 'yakuake', i18n: { stable: nil, stableKF5: 'notmaster', trunk: nil, trunkKF5: 'master', component: 'extragear-utils' })) stub_request(:get, 'https://projects.kde.org/api/v1/project/extragear/utils/krusader') .to_return(body: j(path: 'extragear/utils/krusader', repo: 'krusader', i18n: default_i18n)) stub_request(:get, 'https://projects.kde.org/api/v1/project/extragear/utils/krecipes') .to_return(body: j(path: 'extragear/utils/krecipes', repo: 'krecipes', i18n: default_i18n)) stub_request(:get, 'https://projects.kde.org/api/v1/project/extragear/network/telepathy/ktp1') .to_return(body: j(path: 'extragear/network/telepathy/ktp1', repo: 'ktp1', i18n: default_i18n)) stub_request(:get, 'https://projects.kde.org/api/v1/project/extragear/network/telepathy/ktp2') .to_return(body: j(path: 'extragear/network/telepathy/ktp2', repo: 'ktp2', i18n: default_i18n)) stub_request(:get, 'https://projects.kde.org/api/v1/project/kde/kdenetwork/ktp-contact-runner') .to_return(body: j(path: 'kde/kdenetwork/ktp-contact-runner', repo: 'ktp-contact-runner', i18n: default_i18n)) stub_request(:get, 'https://projects.kde.org/api/v1/project/frameworks/networkmanager-qt') .to_return(body: j(path: 'frameworks/networkmanager-qt', repo: 'networkmanager-qt', i18n: default_i18n)) # By Repo stub_request(:get, 'https://projects.kde.org/api/v1/repo/kfilemetadata') .to_return(body: j(path: 'frameworks/kfilemetadata', repo: 'kfilemetadata', i18n: default_i18n)) end class TestProjectResolver < Testme def setup stub_api end def assert_valid_project(project_array, expected_identifier) refute_nil(project_array) assert_equal(project_array.size, 1) assert_equal(project_array[0].identifier, expected_identifier) end def test_real_project pr = ReleaseMe::Project.from_find('yakuake') assert_valid_project(pr, 'yakuake') end def test_xpath # deprecated not used elsewhere pr = ReleaseMe::Project.from_xpath('yakuake') assert_valid_project(pr, 'yakuake') end def test_module_as_project pr = ReleaseMe::Project.from_find('networkmanager-qt') assert_valid_project(pr, 'networkmanager-qt') end #### def assert_valid_array(project_array, matches) refute_nil(project_array) assert_equal(matches.size, project_array.size) project_array.each do |project| matches.delete(project.identifier) end assert(matches.empty?, "One or more sub-projects did not get resolved correctly: #{matches}") end end class TestProjectConfig < Testme def setup stub_api end def test_invalid_name name = 'kittens' assert_raises do ReleaseMe::Project.from_config(name) end end def test_construction_git ReleaseMe::Project.class_variable_set(:@@configdir, data('projects/')) name = 'valid' pr = ReleaseMe::Project.from_config(name) refute_nil(pr) assert_equal('yakuake', pr.identifier) assert_equal('git://anongit.kde.org/yakuake', pr.vcs.repository) assert_equal('master', pr.i18n_trunk) assert_equal('notmaster', pr.i18n_stable) assert_nil(pr.i18n_lts) assert_equal('extragear-utils', pr.i18n_path) end def test_valid_svn ReleaseMe::Project.class_variable_set(:@@configdir, data('projects/')) name = 'valid-svn' pr = ReleaseMe::Project.from_config(name) refute_nil(pr) assert_equal('svn://anonsvn.kde.org/home', pr.vcs.repository) end def test_invalid_vcs ReleaseMe::Project.class_variable_set(:@@configdir, data('projects/')) name = 'invalid-vcs' assert_raises NoMethodError do ReleaseMe::Project.from_config(name) end end def test_invalid_vcs_type ReleaseMe::Project.class_variable_set(:@@configdir, data('projects/')) name = 'invalid-vcs-type' assert_raises RuntimeError do ReleaseMe::Project.from_config(name) end end def test_plasma_lts ReleaseMe::Project.class_variable_set(:@@configdir, data('projects/')) projects = ReleaseMe::Project.from_find('yakuake') assert_equal(projects.size, 1) pr = projects.shift assert_equal(pr.plasma_lts, 'Plasma/5.8') end end class TestProject < Testme def setup stub_api end def teardown end def test_manual_construction_fail assert_raises do # Refuse to new because we need all arguments. ReleaseMe::Project.new(identifier: 'a', vcs: nil) end end def test_manual_construction_success data = { :identifier => 'yakuake', :vcs => ReleaseMe::Vcs.new, :i18n_trunk => 'master', :i18n_stable => 'master', :i18n_path => 'extragear-utils' } pr = ReleaseMe::Project.new(**data) refute_nil(pr) assert_equal(pr.identifier, data[:identifier]) assert_equal(pr.vcs, data[:vcs]) assert_equal(pr.i18n_trunk, data[:i18n_trunk]) assert_equal(pr.i18n_stable, data[:i18n_stable]) assert_equal(pr.i18n_path, data[:i18n_path]) end def test_resolve_valid projects = ReleaseMe::Project.from_find('yakuake') assert_equal(projects.size, 1) pr = projects.shift assert_equal('yakuake', pr.identifier) assert_equal('master', pr.i18n_trunk) assert_equal('notmaster', pr.i18n_stable) assert_equal('extragear-utils', pr.i18n_path) end def test_resolve_valid_i18n_path_with_sub_project # ktp things are in extragear/network/telepathy/ktp*, yet their # translation path is component-module. Make sure that we get the correct # path for this. # Other example would be extragear/graphics/libs/kdiagram. projects = ReleaseMe::Project.from_find('ktp-contact-runner') assert_equal(1, projects.size) pr = projects.shift assert_equal('ktp-contact-runner', pr.identifier) end def test_resolve_invalid projects = ReleaseMe::Project.from_find('kitten') assert_equal(projects, []) end def test_vcs projects = ReleaseMe::Project.from_find('yakuake') assert_equal(projects.size, 1) pr = projects.shift vcs = pr.vcs - assert_equal('git@invent.kde.org:kde/yakuake', vcs.repository) + assert_equal('git@invent.kde.org:yakuake', vcs.repository) assert_nil(vcs.branch) # project on its own should not set a branch end def test_from_repo_url # Mock Git internals to make this repo default to git.kde.org instead # of invent.kde.org by pretending the ls-remote fails. fail_status = mock() fail_status.responds_like_instance_of(Process::Status) fail_status.stubs(:success?).returns(false) ReleaseMe::Git.any_instance.stubs(:run).with do |args| next false unless args.include?('ls-remote') next false unless args.include?('git@invent.kde.org:kde/kfilemetadata') true end.returns(['', fail_status]) projects = ReleaseMe::Project.from_repo_url('git://anongit.kde.org/kfilemetadata') assert_equal(1, projects.size) pr = projects.shift assert_equal('kfilemetadata', pr.identifier) - assert_equal('git@git.kde.org:kfilemetadata', pr.vcs.repository) + assert_equal('git@invent.kde.org:kfilemetadata', pr.vcs.repository) end end