diff --git a/lib/gem_contribute/cli.rb b/lib/gem_contribute/cli.rb index 604b168..10a5cf2 100644 --- a/lib/gem_contribute/cli.rb +++ b/lib/gem_contribute/cli.rb @@ -23,6 +23,8 @@ module CLI GitHub project (e.g. `rubyevents/rubyevents`). Lands on the default branch. Flags: -e (editor), -a (AI tool). + open Open the gem's GitHub repo in your default browser. + The URL is also printed on stdout as a fallback. fix / Fork the gem's repo, clone the fork, branch from main. Flags: -e (editor), -a (AI tool), --no-comment. submit Push the current branch and open a pre-filled @@ -63,6 +65,7 @@ def dispatch(command, argv, stdout:, stderr:) "config" => ->(o, e) { Config.new(stdout: o, stderr: e) }, "auth" => ->(o, e) { Auth.new(stdout: o, stderr: e) }, "fork" => ->(o, e) { Fork.new(stdout: o, stderr: e, clone_root: GemContribute::Config.new.clone_root) }, + "open" => ->(o, e) { Open.new(stdout: o, stderr: e) }, "fix" => lambda { |o, e| config = GemContribute::Config.new Fix.new(stdout: o, stderr: e, clone_root: config.clone_root, config: config) diff --git a/lib/gem_contribute/cli/open.rb b/lib/gem_contribute/cli/open.rb new file mode 100644 index 0000000..a9573a0 --- /dev/null +++ b/lib/gem_contribute/cli/open.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module GemContribute + module CLI + # `gem-contribute open ` — print the gem's GitHub URL and try to open + # it in the default browser. + # + # Resolves `` the same way `issues` and `fix` do (RubyGems → Project, + # with the `gem-contribute` self short-circuit). The browser opener is the + # same platform-aware helper used by `auth login` and `submit`. As in those + # verbs, the opener is injected at construction so specs never shell out. + class Open + include PlatformTools + include Workflow + + USAGE = "Usage: gem-contribute open " + + def initialize(stdout: $stdout, stderr: $stderr, output: nil, + resolver: Resolver.new, + browser_opener: nil) + @output = output || Output::Standard.new(out: stdout, err: stderr) + @resolver = resolver + @browser_opener = browser_opener || method(:default_browser_opener) + end + + def run(argv) + target = argv.shift + if target.nil? + @output.error(USAGE) + return 2 + end + + project = resolve_target(target, verb: "open") + return 1 if project.nil? + + url = "https://github.com/#{project.owner}/#{project.repo}" + opened = @browser_opener.call(url) + @output.info(opened ? "Opened browser to:" : "Open this URL in your browser:") + @output.info(" #{url}") + 0 + end + end + end +end diff --git a/spec/gem_contribute/cli/open_spec.rb b/spec/gem_contribute/cli/open_spec.rb new file mode 100644 index 0000000..40d6ecd --- /dev/null +++ b/spec/gem_contribute/cli/open_spec.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +require "stringio" + +RSpec.describe GemContribute::CLI::Open do + let(:stdout) { StringIO.new } + let(:stderr) { StringIO.new } + let(:resolver) { instance_double(GemContribute::Resolver) } + let(:opened_urls) { [] } + let(:browser_opener) do + lambda do |url| + opened_urls << url + true + end + end + let(:cli) do + described_class.new(stdout: stdout, stderr: stderr, + resolver: resolver, browser_opener: browser_opener) + end + + let(:project) do + GemContribute::Project.new( + gem_name: "rubocop", host: "github.com", + owner: "rubocop", repo: "rubocop", metadata: {} + ) + end + + it "exits 2 with a usage message when no gem name is given" do + expect(cli.run([])).to eq(2) + expect(stderr.string).to include("Usage: gem-contribute open ") + end + + it "opens the resolved repo URL in the browser and prints it" do + allow(resolver).to receive(:resolve).and_return(project) + + expect(cli.run(["rubocop"])).to eq(0) + expect(opened_urls).to eq(["https://github.com/rubocop/rubocop"]) + out = stdout.string + expect(out).to include("Opened browser to:") + expect(out).to include("https://github.com/rubocop/rubocop") + end + + it "still prints the URL when the browser opener returns false" do + allow(resolver).to receive(:resolve).and_return(project) + failing_opener = ->(_url) { false } + cli = described_class.new(stdout: stdout, stderr: stderr, + resolver: resolver, browser_opener: failing_opener) + + expect(cli.run(["rubocop"])).to eq(0) + out = stdout.string + expect(out).to include("Open this URL in your browser:") + expect(out).to include("https://github.com/rubocop/rubocop") + end + + it "exits 1 with a clear message when the gem resolves to a non-github.com host" do + other = GemContribute::Project.new( + gem_name: "internal", host: :unknown, owner: nil, repo: nil, metadata: {} + ) + allow(resolver).to receive(:resolve).and_return(other) + + expect(cli.run(["internal"])).to eq(1) + expect(stderr.string).to include("only github.com is supported") + expect(opened_urls).to be_empty + end + + it "self-resolves `gem-contribute` without hitting the resolver" do + allow(resolver).to receive(:resolve) + + expect(cli.run(["gem-contribute"])).to eq(0) + expect(resolver).not_to have_received(:resolve) + expect(opened_urls).to eq([ + "https://github.com/#{GemContribute::SELF_PROJECT.owner}/" \ + "#{GemContribute::SELF_PROJECT.repo}" + ]) + end +end