diff --git a/CHANGELOG.md b/CHANGELOG.md index 97468e6..5f8b35a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ +# Changelog + ## [Unreleased] +- Implement `psdk-use version`, `commit`, `mr`, and `latest` sub-commands. + ## [0.1.0] - 2025-11-12 - Initial release diff --git a/Gemfile.lock b/Gemfile.lock index b37d2fb..ea9a451 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -78,6 +78,7 @@ GEM PLATFORMS arm64-darwin-24 ruby + x64-mingw-ucrt DEPENDENCIES fileutils (~> 1.5.0) diff --git a/README.md b/README.md index 2c524e3..0847bc5 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ gem install psdk-cli ## Usage -In a terminal you can run `psdk-cli` it will list all the available commands. +In a terminal you can run `psdk-cli` it will list all the available commands. Please note that sub command (such as `psdk-cli plugin`) can be executed using the shortcut `psdk-` (eg. `psdk-plugin` for `psdk-cli plugin`). diff --git a/lib/psdk/cli/use.rb b/lib/psdk/cli/use.rb index 157ae86..6307927 100644 --- a/lib/psdk/cli/use.rb +++ b/lib/psdk/cli/use.rb @@ -17,32 +17,32 @@ def studio PSDK.unuse_local_pokemonsdk(delete: options[:delete]) end - desc 'version PSDK_VERSION', 'make the project use a specific PSDK version' + desc 'version [PSDK_VERSION]', 'make the project use a specific PSDK version' def version(psdk_version) ensure_project - # TODO: ensure pokemonsdk is in the project, checkout the specific version commit (if found) - puts psdk_version + require_relative '../helpers/psdk' + PSDK.use_version(psdk_version) end - desc 'commit SHA1', 'make the project use a specific PSDK commit' + desc 'commit [SHA1]', 'make the project use a specific PSDK commit' def commit(sha1) ensure_project - # TODO: ensure pokemonsdk is in the project, checkout the specific commit - puts sha1 + require_relative '../helpers/psdk' + PSDK.use_commit(sha1) end - desc 'mr URL', 'make the project use a specific MR' - def mr(url) + desc 'mr [ID]', 'make the project use a specific MR' + def mr(id) ensure_project - # TODO: ensure pokemonsdk is in the project, checkout the specific commit from the MR - # (ensuring remotes are configured) - puts url + require_relative '../helpers/psdk' + PSDK.use_mr(id) end desc 'latest', 'make the project use the latest PSDK commit from development' def latest ensure_project - # TODO: ensure pokemonsdk is in the project, checkout development and pull + require_relative '../helpers/psdk' + PSDK.use_latest end private diff --git a/lib/psdk/helpers/psdk.rb b/lib/psdk/helpers/psdk.rb index 7ad890c..3eed2b7 100644 --- a/lib/psdk/helpers/psdk.rb +++ b/lib/psdk/helpers/psdk.rb @@ -6,7 +6,7 @@ module Psdk module Cli # Module holding all the utility to interact with PSDK repository - module PSDK + module PSDK # rubocop:disable Metrics/ModuleLength # Default URL to the PSDK repository MAIN_REPOSITORY_URL = 'https://gitlab.com/pokemonsdk/pokemonsdk.git' @@ -29,6 +29,47 @@ def repository_path return File.join(Configuration::PATH, 'pokemonsdk') end + # Make the project use an official PSDK release + # @param version [String] release identifier (e.g., "26.58") + def use_version(version) + switch_project_repository("official release #{version}") do |path| + fetch_repository(path) + commit = run_git(path, 'log', '--format=%H', '--extended-regexp', + "--grep=^Release #{Regexp.escape(version)}$", '--max-count=1', 'origin/release') + raise "PSDK release #{version} was not found" if commit.empty? + + run_git(path, 'checkout', '--detach', commit) + end + end + + # Make the project use a specific PSDK commit + # @param commit [String] commit identifier (e.g., "deadcafe") + def use_commit(commit) + switch_project_repository("commit #{commit}") do |path| + fetch_repository(path) + resolved_commit = run_git(path, 'rev-parse', '--verify', '--end-of-options', "#{commit}^{commit}") + run_git(path, 'checkout', '--detach', resolved_commit) + end + end + + # Make the project use the head of a GitLab merge request + # @param id [String] merge request ID (e.g., "123") + def use_mr(id) + switch_project_repository("merge request !#{id}") do |path| + ref = "refs/remotes/origin/merge-requests/#{id}" + run_git(path, 'fetch', 'origin', "+refs/merge-requests/#{id}/head:#{ref}") + run_git(path, 'checkout', '-B', "mr-#{id}", ref) + end + end + + # Make the project use the latest development commit + def use_latest + switch_project_repository('latest development commit') do |path| + fetch_repository(path) + run_git(path, 'checkout', '-B', 'development', 'origin/development') + end + end + # Unuse the local pokemonsdk folder (meaning we want the project to fallback on Pokémon Studio's PSDK) # @param delete [Boolean] if the folder should be deleted def unuse_local_pokemonsdk(delete:) @@ -107,6 +148,67 @@ def rename_pokemonsdk_folder(psdk_path) File.rename(psdk_path, new_path) end end + + # Run a repository switch: resolve the project's pokemonsdk path, apply the given block, + # then refresh submodules and report the active target + # @param target [String] human-readable description of the switch target, used in the confirmation message + # @yieldparam path [String] path to the project's pokemonsdk repository + def switch_project_repository(target) + path = ensure_project_repository + yield(path) + run_git(path, 'submodule', 'update', '--init', '--recursive') + show_active_target(path, target) + rescue StandardError => e + show_switch_error(e) + end + + # Ensure the project's pokemonsdk repository exists, cloning it if necessary + # @return [String] path to the project's pokemonsdk repository + def ensure_project_repository + path = File.join(Configuration.project_path, 'pokemonsdk') + return path if File.exist?(File.join(path, '.git')) + + raise "#{path} exists but is not a Git repository" if Dir.exist?(path) + + success = system('git', 'clone', MAIN_REPOSITORY_URL, path) + raise "Failed to clone pokemonsdk into `#{path}`" unless success + + return path + end + + # Fetch the latest refs from origin + # @param path [String] path to the repository + def fetch_repository(path) + run_git(path, 'fetch', 'origin') + end + + # Run a git command in the given repository and return its output + # @param path [String] path to the repository + # @param arguments [Array] git subcommand and its arguments + # @return [String] the stripped stdout of the command + def run_git(path, *arguments) + output = IO.popen(['git', *arguments], chdir: path, &:read) + return output.strip if $?.success? # rubocop:disable Style/SpecialGlobalVars + + raise "git #{arguments.join(' ')} failed" + end + + # Show the confirmation message for the currently active PSDK target + # @param path [String] path to the repository + # @param target [String] human-readable description of the active target + def show_active_target(path, target) + commit = run_git(path, 'rev-parse', '--short', 'HEAD') + version = File.read(File.join(path, 'version.txt')).to_i + version_string = [version].pack('I>').unpack('C*').join('.').gsub(/^(0\.)+/, '') + puts "Active PSDK: #{target} (version #{version_string}, commit #{commit})" + end + + # Show the error message when a PSDK switch operation fails + # @param error [StandardError] the error that was raised + def show_switch_error(error) + puts "[Error] Failed to switch PSDK: #{error.message}" + exit(1) + end end end end diff --git a/spec/psdk/cli/use_spec.rb b/spec/psdk/cli/use_spec.rb index 6bc5aa0..6a11621 100644 --- a/spec/psdk/cli/use_spec.rb +++ b/spec/psdk/cli/use_spec.rb @@ -30,8 +30,10 @@ describe 'public methods' do before do allow(subject).to receive(:ensure_project) - # Suppress puts output for cleaner test runs - allow(subject).to receive(:puts) + allow(Psdk::Cli::PSDK).to receive(:use_version) + allow(Psdk::Cli::PSDK).to receive(:use_commit) + allow(Psdk::Cli::PSDK).to receive(:use_mr) + allow(Psdk::Cli::PSDK).to receive(:use_latest) end describe '#studio' do @@ -42,28 +44,32 @@ end describe '#version' do - it 'calls ensure_project' do + it 'targets the requested official release' do + expect(Psdk::Cli::PSDK).to receive(:use_version).with('24.15') subject.version('24.15') expect(subject).to have_received(:ensure_project) end end describe '#commit' do - it 'calls ensure_project' do - subject.commit('sha1') + it 'targets the requested commit' do + expect(Psdk::Cli::PSDK).to receive(:use_commit).with('deadcafe') + subject.commit('deadcafe') expect(subject).to have_received(:ensure_project) end end describe '#mr' do - it 'calls ensure_project' do - subject.mr('url') + it 'targets the requested merge request' do + expect(Psdk::Cli::PSDK).to receive(:use_mr).with('42') + subject.mr('42') expect(subject).to have_received(:ensure_project) end end describe '#latest' do - it 'calls ensure_project' do + it 'targets the latest development commit' do + expect(Psdk::Cli::PSDK).to receive(:use_latest) subject.latest expect(subject).to have_received(:ensure_project) end diff --git a/spec/psdk/helpers/psdk_spec.rb b/spec/psdk/helpers/psdk_spec.rb index c9db673..bbe810f 100644 --- a/spec/psdk/helpers/psdk_spec.rb +++ b/spec/psdk/helpers/psdk_spec.rb @@ -4,6 +4,74 @@ require 'psdk/helpers/psdk' RSpec.describe Psdk::Cli::PSDK do # rubocop:disable Metrics/BlockLength + describe 'project version switching' do # rubocop:disable Metrics/BlockLength + let(:psdk_path) { '/path/to/project/pokemonsdk' } + + before do + allow(described_class).to receive(:ensure_project_repository).and_return(psdk_path) + allow(described_class).to receive(:fetch_repository) + allow(described_class).to receive(:run_git) + allow(described_class).to receive(:show_active_target) + end + + it 'checks out the release commit and refreshes submodules' do + expect(described_class).to receive(:run_git).with( + psdk_path, 'log', '--format=%H', '--extended-regexp', '--grep=^Release 26\\.58$', + '--max-count=1', 'origin/release' + ).and_return('release-sha') + expect(described_class).to receive(:run_git).with(psdk_path, 'checkout', '--detach', 'release-sha') + expect(described_class).to receive(:run_git).with(psdk_path, 'submodule', 'update', '--init', '--recursive') + expect(described_class).to receive(:show_active_target).with(psdk_path, 'official release 26.58') + + described_class.use_version('26.58') + end + + it 'resolves and checks out a specific commit' do + expect(described_class).to receive(:run_git).with( + psdk_path, 'rev-parse', '--verify', '--end-of-options', 'deadcafe^{commit}' + ).and_return('full-sha') + expect(described_class).to receive(:run_git).with(psdk_path, 'checkout', '--detach', 'full-sha') + expect(described_class).to receive(:show_active_target).with(psdk_path, 'commit deadcafe') + + described_class.use_commit('deadcafe') + end + + it 'fetches an MR by ID and checks out its local branch' do + expect(described_class).to receive(:run_git).with( + psdk_path, 'fetch', 'origin', + '+refs/merge-requests/42/head:refs/remotes/origin/merge-requests/42' + ) + expect(described_class).to receive(:run_git).with( + psdk_path, 'checkout', '-B', 'mr-42', 'refs/remotes/origin/merge-requests/42' + ) + expect(described_class).to receive(:show_active_target).with(psdk_path, 'merge request !42') + + described_class.use_mr('42') + end + + it 'checks out the latest development commit' do + expect(described_class).to receive(:run_git).with( + psdk_path, 'checkout', '-B', 'development', 'origin/development' + ) + expect(described_class).to receive(:show_active_target).with(psdk_path, 'latest development commit') + + described_class.use_latest + end + + it 'confirms the active version and commit' do + allow(described_class).to receive(:show_active_target).and_call_original + allow(described_class).to receive(:run_git).with( + psdk_path, 'rev-parse', '--short', 'HEAD' + ).and_return('deadcafe') + allow(File).to receive(:read).with(File.join(psdk_path, 'version.txt')).and_return('6714') + expect(described_class).to receive(:puts).with( + 'Active PSDK: official release 26.58 (version 26.58, commit deadcafe)' + ) + + described_class.send(:show_active_target, psdk_path, 'official release 26.58') + end + end + describe '.unuse_local_pokemonsdk' do # rubocop:disable Metrics/BlockLength let(:project_path) { '/path/to/project' } let(:psdk_path) { File.join(project_path, 'pokemonsdk') }