geet 0.31.1 → 0.32.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bf5b6b46f33b00c38e4f383c186ca62d9a196f3516cee7e5569cabc087e028be
4
- data.tar.gz: eb3464d9583e1a9672bba43752ba56707cd1ab84931e9df830c74cadbebe266d
3
+ metadata.gz: 2903cf0f0d89e0f84d45e434545cf6958adcad59ec3ccbe7bad825bb987834a4
4
+ data.tar.gz: 255f580c0f8f555f6fb3e69ded39a785d062907db5da4d0d909cf03b12a85bf9
5
5
  SHA512:
6
- metadata.gz: 983a7f8a246f87ecacd86b2afd8dd0aa6e9deaff51b443ecc29697c91b2b8c3fcb9fee462ef295fe66a526e527111e0cf0bace6b7294c38353223481df5a3d06
7
- data.tar.gz: 6e55131d0d846d6d72c9d5e1312c40291bb996313c3106e33cc1e3069c82464d6b0119eacd4f037453e9dba89b6f620d0179a65cb6adbc0248c53dd959289943
6
+ metadata.gz: 195be7f2ca33e9fadcada902dcacff479d3dfa17ca4424fc876bfeea53e69cc56e2044f36f3e52cc4d73053f8015ce944bc047a60f7c9645759c95126644ce0f
7
+ data.tar.gz: d1b5c383990d18d0340e6d0b52ff60212f0d5788fd9c35c5598af125e6df20b72c5ad375817531345222c16676b96ae1dd7e9d69e6a85d7e81f3db002c9de9c7
data/bin/geet CHANGED
@@ -30,7 +30,11 @@ class GeetLauncher
30
30
  return
31
31
  end
32
32
 
33
- repository = Git::Repository.new(upstream: !!options[:upstream], protected_repositories: protected_repositories)
33
+ repository = Git::Repository.new(
34
+ upstream: !!options[:upstream],
35
+ protected_repositories: protected_repositories,
36
+ api_token_required: !TOKEN_OPTIONAL_COMMANDS.include?(command)
37
+ )
34
38
 
35
39
  case command
36
40
  when GIST_CREATE_COMMAND
data/geet.gemspec CHANGED
@@ -8,7 +8,7 @@ Gem::Specification.new do |s|
8
8
  s.platform = Gem::Platform::RUBY
9
9
  s.required_ruby_version = ">= 3.3.0"
10
10
  s.authors = ["Saverio Miroddi"]
11
- s.date = "2026-07-24"
11
+ s.date = "2026-10-14"
12
12
  s.email = ["saverio.pub2@gmail.com"]
13
13
  s.homepage = "https://github.com/saveriomiroddi/geet"
14
14
  s.summary = "Commandline interface for performing SCM host operations, eg. create a PR on GitHub"
@@ -20,6 +20,13 @@ module Geet
20
20
  REPO_ADD_UPSTREAM_COMMAND = T.let("repo.add_upstream", String)
21
21
  REPO_CREATE_COMMAND = T.let("repo.create", String)
22
22
  REPO_OPEN_COMMAND = T.let("repo.open", String)
23
+
24
+ TOKEN_OPTIONAL_COMMANDS = T.let([
25
+ LABEL_LIST_COMMAND,
26
+ MILESTONE_LIST_COMMAND,
27
+ PR_LIST_COMMAND,
28
+ PR_OPEN_COMMAND,
29
+ ].freeze, T::Array[String])
23
30
  end
24
31
  end
25
32
  end
@@ -23,14 +23,15 @@ module Geet
23
23
  upstream: T::Boolean,
24
24
  git_client: Utils::GitClient,
25
25
  warnings: T::Boolean, # disable all the warnings
26
- protected_repositories: T::Array[String] # warn when creating an issue/pr on these repos (format: `owner/repo`)
26
+ protected_repositories: T::Array[String], # warn when creating an issue/pr on these repos (format: `owner/repo`)
27
+ api_token_required: T::Boolean
27
28
  )
28
29
  .void
29
30
  }
30
- def initialize(upstream: false, git_client: DEFAULT_GIT_CLIENT, warnings: true, protected_repositories: [])
31
+ def initialize(upstream: false, git_client: DEFAULT_GIT_CLIENT, warnings: true, protected_repositories: [], api_token_required: true)
31
32
  @upstream = upstream
32
33
  @git_client = git_client
33
- @api_token = T.let(extract_env_api_token, String)
34
+ @api_token = T.let(extract_env_api_token(required: api_token_required), T.nilable(String))
34
35
  @warnings = warnings
35
36
  @protected_repositories = protected_repositories
36
37
  end
@@ -200,9 +201,12 @@ module Geet
200
201
  @api_interface ||= Github::ApiInterface.new(@api_token, repo_path: @git_client.path(upstream: @upstream), upstream: @upstream)
201
202
  end
202
203
 
203
- sig { returns(String) }
204
- def extract_env_api_token
205
- ENV["GH_TOKEN"] || raise("GH_TOKEN not set!")
204
+ sig { params(required: T::Boolean).returns(T.nilable(String)) }
205
+ def extract_env_api_token(required:)
206
+ api_token = ENV["GH_TOKEN"]
207
+ raise "GH_TOKEN not set!" if required && api_token.nil?
208
+
209
+ api_token
206
210
  end
207
211
  end
208
212
  end
@@ -13,6 +13,7 @@ module Geet
13
13
  API_AUTH_USER = T.let("", String) # We don't need the login, as the API key uniquely identifies the user
14
14
  API_BASE_URL = T.let("https://api.github.com", String)
15
15
  GRAPHQL_API_URL = T.let("https://api.github.com/graphql", String)
16
+ UNAUTHENTICATED_NOT_FOUND_HINT = T.let("GH_TOKEN is not set; private repositories require it", String)
16
17
 
17
18
  sig { returns(T.nilable(String)) }
18
19
  attr_reader :repository_path
@@ -22,13 +23,13 @@ module Geet
22
23
  #
23
24
  sig {
24
25
  params(
25
- api_token: String,
26
+ api_token: T.nilable(String),
26
27
  repo_path: T.nilable(String),
27
28
  upstream: T.nilable(T::Boolean)
28
29
  ).void
29
30
  }
30
31
  def initialize(api_token, repo_path: nil, upstream: nil)
31
- @api_token = T.let(api_token, String)
32
+ @api_token = T.let(api_token, T.nilable(String))
32
33
  @repository_path = T.let(repo_path, T.nilable(String))
33
34
  @upstream = T.let(upstream, T.nilable(T::Boolean))
34
35
  end
@@ -78,6 +79,8 @@ module Geet
78
79
 
79
80
  if error?(response)
80
81
  error_message = decode_and_format_error(T.cast(parsed_response, T::Hash[String, T.untyped]))
82
+ # GitHub hides private repositories from unauthenticated requests behind a 404.
83
+ error_message += " (#{UNAUTHENTICATED_NOT_FOUND_HINT})" if response.code == "404" && @api_token.nil?
81
84
  raise Geet::Shared::HttpError.new(error_message, response.code)
82
85
  end
83
86
 
@@ -110,7 +113,7 @@ module Geet
110
113
 
111
114
  Net::HTTP.start(uri.host, use_ssl: true) do |http|
112
115
  request = Net::HTTP::Post.new(uri).tap do
113
- it.basic_auth API_AUTH_USER, @api_token
116
+ it.basic_auth API_AUTH_USER, @api_token if @api_token
114
117
  it["Accept"] = "application/vnd.github.v3+json"
115
118
  it.body = {query:, variables:}.to_json
116
119
  end
@@ -169,7 +172,7 @@ module Geet
169
172
  Net::HTTP.start(uri.host, use_ssl: true) do |http|
170
173
  request = http_class.new(uri)
171
174
 
172
- request.basic_auth API_AUTH_USER, @api_token
175
+ request.basic_auth API_AUTH_USER, @api_token if @api_token
173
176
  request.body = data.to_json if data
174
177
  request["Accept"] = "application/vnd.github.v3+json"
175
178
 
@@ -26,11 +26,7 @@ module Geet
26
26
  #
27
27
  sig { returns(Github::PR) }
28
28
  def checked_find_branch_pr
29
- owner = if @repository.upstream?
30
- @repository.authenticated_user.username
31
- else
32
- @git_client.owner
33
- end
29
+ owner = @git_client.owner
34
30
 
35
31
  head = @git_client.current_branch
36
32
 
data/lib/geet/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # typed: strict
3
3
 
4
4
  module Geet
5
- VERSION = "0.31.1"
5
+ VERSION = "0.32.1"
6
6
  end
@@ -39,4 +39,27 @@ describe Geet::Services::OpenPr do
39
39
  end
40
40
 
41
41
  end # context 'with github.com'
42
+
43
+ context "when opening an upstream PR" do
44
+ let(:repository) { instance_double(Geet::Git::Repository, upstream?: true) }
45
+
46
+ it "uses the origin owner without requesting the authenticated user" do
47
+ pr = instance_double(
48
+ Geet::Github::PR,
49
+ link: "https://github.com/upstream/repository/pull/1"
50
+ )
51
+
52
+ allow(git_client).to receive(:owner).and_return(owner)
53
+ allow(git_client).to receive(:current_branch).and_return(branch)
54
+ allow(repository).to receive(:prs).with(owner:, head: branch).and_return([pr])
55
+ expect(repository).not_to receive(:authenticated_user)
56
+
57
+ expect {
58
+ service_instance = described_class.new(repository, git_client: git_client)
59
+ allow(service_instance).to receive(:open_file_with_default_application)
60
+
61
+ expect(service_instance.execute).to equal(pr)
62
+ }.to output("Finding PR with head (#{owner}:#{branch})...\n").to_stdout
63
+ end
64
+ end
42
65
  end
@@ -45,5 +45,46 @@ describe GeetLauncher do
45
45
  ensure
46
46
  ENV["GH_TOKEN"] = without_token if without_token
47
47
  end
48
+
49
+ {
50
+ Geet::Commandline::Commands::LABEL_LIST_COMMAND => Geet::Services::ListLabels,
51
+ Geet::Commandline::Commands::MILESTONE_LIST_COMMAND => Geet::Services::ListMilestones,
52
+ Geet::Commandline::Commands::PR_LIST_COMMAND => Geet::Services::ListPrs,
53
+ Geet::Commandline::Commands::PR_OPEN_COMMAND => Geet::Services::OpenPr,
54
+ }.each do |command, service_class|
55
+ it "dispatches #{command} with an optional API token" do
56
+ launcher = described_class.new
57
+ repository = instance_double(Geet::Git::Repository)
58
+ service = instance_double(service_class)
59
+
60
+ allow_any_instance_of(Geet::Commandline::Configuration).to receive(:decode_argv).and_return([command, {}])
61
+ allow(launcher).to receive(:protected_repositories).and_return([])
62
+ expect(Geet::Git::Repository).to receive(:new).with(
63
+ upstream: false,
64
+ protected_repositories: [],
65
+ api_token_required: false
66
+ ).and_return(repository)
67
+ expect(service_class).to receive(:new).with(repository).and_return(service)
68
+ expect(service).to receive(:execute).with(no_args)
69
+
70
+ launcher.launch
71
+ end
72
+ end
73
+
74
+ it "requires an API token for issue.list" do
75
+ launcher = described_class.new
76
+
77
+ allow_any_instance_of(Geet::Commandline::Configuration).to receive(:decode_argv).and_return([
78
+ Geet::Commandline::Commands::ISSUE_LIST_COMMAND,
79
+ {},
80
+ ])
81
+ allow(launcher).to receive(:protected_repositories).and_return([])
82
+ expect(Geet::Services::ListIssues).not_to receive(:new)
83
+
84
+ api_token = ENV.delete("GH_TOKEN")
85
+ expect { launcher.launch }.to raise_error("GH_TOKEN not set!")
86
+ ensure
87
+ ENV["GH_TOKEN"] = api_token if api_token
88
+ end
48
89
  end
49
90
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ describe Geet::Git::Repository do
6
+ describe "API token handling" do
7
+ it "allows the token to be omitted when authentication is not required" do
8
+ api_token = ENV.delete("GH_TOKEN")
9
+
10
+ expect { described_class.new(api_token_required: false) }.not_to raise_error
11
+ ensure
12
+ ENV["GH_TOKEN"] = api_token if api_token
13
+ end
14
+
15
+ it "requires the token by default" do
16
+ api_token = ENV.delete("GH_TOKEN")
17
+
18
+ expect { described_class.new }.to raise_error("GH_TOKEN not set!")
19
+ ensure
20
+ ENV["GH_TOKEN"] = api_token if api_token
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "webmock/rspec"
5
+
6
+ describe Geet::Github::ApiInterface do
7
+ describe "authentication" do
8
+ let(:request_url) { "https://api.github.com/repos/owner/repository/issues" }
9
+
10
+ it "sends an unauthenticated request when no token is available" do
11
+ request = stub_request(:get, request_url)
12
+ .with { |actual_request| !actual_request.headers.key?("Authorization") }
13
+ .to_return(status: 200, body: "[]")
14
+
15
+ described_class.new(nil, repo_path: "owner/repository").send_request("issues")
16
+
17
+ expect(request).to have_been_requested.once
18
+ end
19
+
20
+ it "authenticates the request when a token is available" do
21
+ authorization = "Basic #{Base64.strict_encode64(":token")}"
22
+ request = stub_request(:get, request_url)
23
+ .with(headers: {"Authorization" => authorization})
24
+ .to_return(status: 200, body: "[]")
25
+
26
+ described_class.new("token", repo_path: "owner/repository").send_request("issues")
27
+
28
+ expect(request).to have_been_requested.once
29
+ end
30
+
31
+ context "when the response is Not Found" do
32
+ before do
33
+ stub_request(:get, request_url).to_return(status: 404, body: {message: "Not Found"}.to_json)
34
+ end
35
+
36
+ it "hints at the missing token when no token is available" do
37
+ expect {
38
+ described_class.new(nil, repo_path: "owner/repository").send_request("issues")
39
+ }.to raise_error(Geet::Shared::HttpError, "Not Found (GH_TOKEN is not set; private repositories require it)") { |error|
40
+ expect(error.code).to eq(404)
41
+ }
42
+ end
43
+
44
+ it "doesn't hint at the token when one is available" do
45
+ expect {
46
+ described_class.new("token", repo_path: "owner/repository").send_request("issues")
47
+ }.to raise_error(Geet::Shared::HttpError, "Not Found")
48
+ end
49
+ end
50
+ end
51
+ end
@@ -7,6 +7,32 @@ require_relative "../../../lib/geet/github/pr"
7
7
  require_relative "../../../lib/geet/github/api_interface"
8
8
 
9
9
  describe Geet::Github::PR do
10
+ describe ".list" do
11
+ it "filters upstream PRs by the supplied origin owner and branch" do
12
+ api_interface = instance_double(Geet::Github::ApiInterface, upstream?: true)
13
+ matching_pr = {
14
+ "number" => 1,
15
+ "title" => "Matching PR",
16
+ "html_url" => "https://github.com/upstream/repository/pull/1",
17
+ "head" => {"label" => "origin-owner:topic"},
18
+ }
19
+ other_pr = {
20
+ "number" => 2,
21
+ "title" => "Other PR",
22
+ "html_url" => "https://github.com/upstream/repository/pull/2",
23
+ "head" => {"label" => "other-owner:topic"},
24
+ }
25
+
26
+ expect(api_interface).to receive(:send_request)
27
+ .with("pulls", multipage: true)
28
+ .and_return([matching_pr, other_pr])
29
+
30
+ prs = described_class.list(api_interface, owner: "origin-owner", head: "topic")
31
+
32
+ expect(prs.map(&:number)).to eq([1])
33
+ end
34
+ end
35
+
10
36
  describe "#fetch_available_merge_method" do
11
37
  let(:api_interface) { instance_double(Geet::Github::ApiInterface) }
12
38
  let(:pr_number) { 123 }
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: geet
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.31.1
4
+ version: 0.32.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Saverio Miroddi
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-07-24 00:00:00.000000000 Z
10
+ date: 2026-10-14 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: base64
@@ -260,6 +260,8 @@ files:
260
260
  - spec/spec_helper.rb
261
261
  - spec/unit/commandline/configuration_spec.rb
262
262
  - spec/unit/geet_launcher_spec.rb
263
+ - spec/unit/git/repository_spec.rb
264
+ - spec/unit/github/api_interface_spec.rb
263
265
  - spec/unit/github/pr_spec.rb
264
266
  - spec/unit/github/repository_spec.rb
265
267
  - spec/unit/services/create_repo_spec.rb
@@ -310,7 +312,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
310
312
  - !ruby/object:Gem::Version
311
313
  version: '0'
312
314
  requirements: []
313
- rubygems_version: 4.0.10
315
+ rubygems_version: 4.0.20
314
316
  specification_version: 4
315
317
  summary: Commandline interface for performing SCM host operations, eg. create a PR
316
318
  on GitHub