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 +4 -4
- data/bin/geet +5 -1
- data/geet.gemspec +1 -1
- data/lib/geet/commandline/commands.rb +7 -0
- data/lib/geet/git/repository.rb +10 -6
- data/lib/geet/github/api_interface.rb +7 -4
- data/lib/geet/helpers/services_workflow_helper.rb +1 -5
- data/lib/geet/version.rb +1 -1
- data/spec/integration/open_pr_spec.rb +23 -0
- data/spec/unit/geet_launcher_spec.rb +41 -0
- data/spec/unit/git/repository_spec.rb +23 -0
- data/spec/unit/github/api_interface_spec.rb +51 -0
- data/spec/unit/github/pr_spec.rb +26 -0
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2903cf0f0d89e0f84d45e434545cf6958adcad59ec3ccbe7bad825bb987834a4
|
|
4
|
+
data.tar.gz: 255f580c0f8f555f6fb3e69ded39a785d062907db5da4d0d909cf03b12a85bf9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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(
|
|
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-
|
|
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
|
data/lib/geet/git/repository.rb
CHANGED
|
@@ -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]
|
|
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"]
|
|
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 =
|
|
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
|
@@ -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
|
data/spec/unit/github/pr_spec.rb
CHANGED
|
@@ -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.
|
|
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-
|
|
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.
|
|
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
|