geet 0.30.0 → 0.31.0

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.
@@ -8,7 +8,6 @@ require_relative "../../lib/geet/services/open_repo"
8
8
  module Geet
9
9
  describe Services::OpenRepo do
10
10
  let(:git_client) { Utils::GitClient.new }
11
- let(:repository) { Git::Repository.new(git_client: git_client) }
12
11
 
13
12
  OWNER = "donaldduck"
14
13
  REPOSITORY_NAME = "testrepo"
@@ -27,7 +26,7 @@ module Geet
27
26
  expected_output = ""
28
27
 
29
28
  expect {
30
- service_instance = described_class.new(repository, git_client: git_client)
29
+ service_instance = described_class.new(git_client: git_client)
31
30
 
32
31
  expect(service_instance).to receive(:open_file_with_default_application).with(expected_url) do
33
32
  # do nothing; just don't open the browser
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+ # typed: false
3
+
4
+ require "spec_helper"
5
+
6
+ describe Geet::Commandline::Configuration do
7
+ describe "#decode_argv" do
8
+ it "decodes long repository creation options" do
9
+ stub_const("ARGV", [
10
+ "repo", "create",
11
+ "--visibility", "private",
12
+ "--upstream", "git@github.com:owner/project.git",
13
+ ])
14
+
15
+ expect(described_class.new.decode_argv).to eq([
16
+ "repo.create",
17
+ {visibility: "private", upstream: "git@github.com:owner/project.git"},
18
+ ])
19
+ end
20
+
21
+ it "decodes short repository creation options" do
22
+ stub_const("ARGV", [
23
+ "repo", "create",
24
+ "-v", "public",
25
+ "-u", "https://github.com/owner/project.git",
26
+ ])
27
+
28
+ expect(described_class.new.decode_argv).to eq([
29
+ "repo.create",
30
+ {visibility: "public", upstream: "https://github.com/owner/project.git"},
31
+ ])
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+ # typed: false
3
+
4
+ require "spec_helper"
5
+ load File.expand_path("../../bin/geet", __dir__)
6
+
7
+ describe GeetLauncher do
8
+ describe "#launch" do
9
+ it "dispatches repository creation before constructing an existing repository" do
10
+ service = instance_double(Geet::Services::CreateRepo)
11
+ allow_any_instance_of(Geet::Commandline::Configuration).to receive(:decode_argv).and_return([
12
+ Geet::Commandline::Commands::REPO_CREATE_COMMAND,
13
+ {visibility: "private"},
14
+ ])
15
+ expect(Geet::Services::CreateRepo).to receive(:new).and_return(service)
16
+ expect(service).to receive(:execute).with(visibility: "private")
17
+ expect(Geet::Git::Repository).not_to receive(:new)
18
+
19
+ described_class.new.launch
20
+ end
21
+
22
+ it "dispatches repository opening without constructing an authenticated repository" do
23
+ service = instance_double(Geet::Services::OpenRepo)
24
+ allow_any_instance_of(Geet::Commandline::Configuration).to receive(:decode_argv).and_return([
25
+ Geet::Commandline::Commands::REPO_OPEN_COMMAND,
26
+ {upstream: true},
27
+ ])
28
+ expect(Geet::Services::OpenRepo).to receive(:new).and_return(service)
29
+ expect(service).to receive(:execute).with(upstream: true)
30
+ expect(Geet::Git::Repository).not_to receive(:new)
31
+
32
+ without_token = ENV.delete("GH_TOKEN")
33
+ described_class.new.launch
34
+ ensure
35
+ ENV["GH_TOKEN"] = without_token if without_token
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../spec_helper"
4
+
5
+ RSpec.describe Geet::Github::Repository do
6
+ let(:api_interface) { instance_double(Geet::Github::ApiInterface) }
7
+ let(:response) do
8
+ {
9
+ "full_name" => "donald/geet",
10
+ "html_url" => "https://github.com/donald/geet",
11
+ "ssh_url" => "git@github.com:donald/geet.git",
12
+ }
13
+ end
14
+
15
+ it "creates a private repository for the authenticated user" do
16
+ expect(api_interface).to receive(:send_request).with("/user/repos", data: {name: "geet", private: true}).and_return(response)
17
+ repository = described_class.create("geet", "private", api_interface)
18
+ expect([repository.full_name, repository.html_url, repository.ssh_url]).to eq(["donald/geet", "https://github.com/donald/geet", "git@github.com:donald/geet.git"])
19
+ end
20
+
21
+ it "creates a public repository for the authenticated user" do
22
+ expect(api_interface).to receive(:send_request).with("/user/repos", data: {name: "geet", private: false}).and_return(response)
23
+ described_class.create("geet", "public", api_interface)
24
+ end
25
+
26
+ it "requests a named fork" do
27
+ expect(api_interface).to receive(:send_request).with("/repos/upstream/project/forks", data: {name: "geet"}).and_return(response)
28
+ described_class.fork("upstream/project", "geet", api_interface)
29
+ end
30
+
31
+ it "fetches a repository" do
32
+ expect(api_interface).to receive(:send_request).with("/repos/donald/geet").and_return(response)
33
+ expect(described_class.fetch("donald/geet", api_interface).full_name).to eq("donald/geet")
34
+ end
35
+
36
+ it "updates the default branch" do
37
+ expect(api_interface).to receive(:send_request).with("/repos/donald/geet", data: {default_branch: "topic"}, http_method: :patch)
38
+ described_class.new(response, api_interface).update_default_branch("topic")
39
+ end
40
+ end
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+ # typed: false
3
+
4
+ require "spec_helper"
5
+
6
+ describe Geet::Services::CreateRepo do
7
+ describe "#execute" do
8
+ let(:git_client) { instance_double(Geet::Utils::GitClient) }
9
+ let(:api_interface) { instance_double(Geet::Github::ApiInterface) }
10
+ let(:prompt) { instance_double(TTY::Prompt) }
11
+ let(:out) { StringIO.new }
12
+
13
+ subject(:service) do
14
+ described_class.new(
15
+ directory: "/tmp/geet",
16
+ out:,
17
+ git_client:,
18
+ api_interface:,
19
+ prompt:,
20
+ sleeper: ->(_) {},
21
+ fork_attempts: 3
22
+ )
23
+ end
24
+
25
+ before do
26
+ allow(git_client).to receive(:head_commit?).and_return(true)
27
+ allow(git_client).to receive(:current_branch).and_return("topic")
28
+ allow(git_client).to receive(:remote_defined?).with("origin").and_return(false)
29
+ end
30
+
31
+ it "rejects a repository without commits before making an API request" do
32
+ allow(git_client).to receive(:head_commit?).and_return(false)
33
+
34
+ expect(Geet::Github::Repository).not_to receive(:create)
35
+ expect { service.execute(visibility: "private") }.to raise_error("The repository has no commits!")
36
+ end
37
+
38
+ it "rejects a detached HEAD before prompting or making an API request" do
39
+ allow(git_client).to receive(:current_branch).and_raise("Couldn't find current branch")
40
+
41
+ expect(prompt).not_to receive(:select)
42
+ expect(Geet::Github::Repository).not_to receive(:create)
43
+ expect { service.execute }.to raise_error("Couldn't find current branch")
44
+ end
45
+
46
+ it "rejects an existing origin before making an API request" do
47
+ allow(git_client).to receive(:remote_defined?).with("origin").and_return(true)
48
+
49
+ expect(Geet::Github::Repository).not_to receive(:create)
50
+ expect { service.execute(visibility: "private") }.to raise_error('Remote "origin" already exists!')
51
+ end
52
+
53
+ it "rejects an existing upstream before making an API request" do
54
+ allow(git_client).to receive(:remote_defined?).with("upstream").and_return(true)
55
+
56
+ expect(Geet::Github::Repository).not_to receive(:fork)
57
+ expect { service.execute(upstream: "https://github.com/upstream/project.git") }
58
+ .to raise_error('Remote "upstream" already exists!')
59
+ end
60
+
61
+ it "rejects invalid visibility before making an API request" do
62
+ expect(Geet::Github::Repository).not_to receive(:create)
63
+ expect { service.execute(visibility: "internal") }.to raise_error("Visibility must be private or public!")
64
+ end
65
+
66
+ it "rejects visibility in fork mode before making an API request" do
67
+ allow(git_client).to receive(:remote_defined?).with("upstream").and_return(false)
68
+
69
+ expect(Geet::Github::Repository).not_to receive(:fork)
70
+ expect { service.execute(visibility: "private", upstream: "https://github.com/upstream/project.git") }
71
+ .to raise_error("Visibility can't be specified with upstream!")
72
+ end
73
+
74
+ it "prompts for visibility when it is omitted" do
75
+ repository = instance_double(Geet::Github::Repository, ssh_url: "git@github.com:donald/geet.git", html_url: "https://github.com/donald/geet")
76
+ allow(prompt).to receive(:select).with("Visibility:", ["private", "public"]).and_return("private")
77
+ allow(Geet::Github::Repository).to receive(:create).with("geet", "private", api_interface).and_return(repository)
78
+ allow(git_client).to receive(:add_remote)
79
+ allow(git_client).to receive(:push)
80
+ allow(repository).to receive(:update_default_branch)
81
+
82
+ service.execute
83
+ end
84
+
85
+ it "does not prompt when public visibility is explicit" do
86
+ repository = instance_double(Geet::Github::Repository, ssh_url: "git@github.com:donald/geet.git", html_url: "https://github.com/donald/geet")
87
+ expect(prompt).not_to receive(:select)
88
+ allow(Geet::Github::Repository).to receive(:create).with("geet", "public", api_interface).and_return(repository)
89
+ allow(git_client).to receive(:add_remote)
90
+ allow(git_client).to receive(:push)
91
+ allow(repository).to receive(:update_default_branch)
92
+
93
+ service.execute(visibility: "public")
94
+ end
95
+
96
+ it "creates a repository, configures origin, pushes, and updates the default branch in order" do
97
+ repository = instance_double(
98
+ Geet::Github::Repository,
99
+ full_name: "donald/geet",
100
+ ssh_url: "git@github.com:donald/geet.git",
101
+ html_url: "https://github.com/donald/geet"
102
+ )
103
+ allow(prompt).to receive(:select).and_return("private")
104
+ expect(Geet::Github::Repository).to receive(:create)
105
+ .with("geet", "private", api_interface).ordered.and_return(repository)
106
+ expect(git_client).to receive(:add_remote)
107
+ .with("origin", "git@github.com:donald/geet.git").ordered
108
+ expect(git_client).to receive(:push).with(remote_branch: "topic").ordered
109
+ expect(repository).to receive(:update_default_branch).with("topic").ordered
110
+
111
+ expect(service.execute).to eq(repository)
112
+ expect(out.string).to include("Repository address: https://github.com/donald/geet")
113
+ expect(out.string).to include("Default branch: topic")
114
+ end
115
+
116
+ it "waits for a fork, configures both remotes, pushes, and updates the default branch" do
117
+ upstream = "https://github.com/upstream/project.git"
118
+ provisional_repository = instance_double(Geet::Github::Repository, full_name: "donald/geet")
119
+ repository = instance_double(
120
+ Geet::Github::Repository,
121
+ ssh_url: "git@github.com:donald/geet.git",
122
+ html_url: "https://github.com/donald/geet"
123
+ )
124
+ sleeper = proc {}
125
+ service = described_class.new(
126
+ directory: "/tmp/geet", out:, git_client:, api_interface:, prompt:,
127
+ sleeper:, fork_attempts: 3
128
+ )
129
+ allow(git_client).to receive(:remote_defined?).with("upstream").and_return(false)
130
+ expect(git_client).to receive(:remote_path).with(upstream).ordered.and_return("upstream/project")
131
+ expect(Geet::Github::Repository).to receive(:fork)
132
+ .with("upstream/project", "geet", api_interface).ordered.and_return(provisional_repository)
133
+ expect(Geet::Github::Repository).to receive(:fetch)
134
+ .with("donald/geet", api_interface).ordered.and_raise(Geet::Shared::HttpError.new("Not Found", 404))
135
+ expect(sleeper).to receive(:call).with(1).ordered
136
+ expect(Geet::Github::Repository).to receive(:fetch)
137
+ .with("donald/geet", api_interface).ordered.and_return(repository)
138
+ expect(git_client).to receive(:add_remote)
139
+ .with("origin", "git@github.com:donald/geet.git").ordered
140
+ expect(git_client).to receive(:add_remote).with("upstream", upstream).ordered
141
+ expect(git_client).to receive(:push).with(remote_branch: "topic").ordered
142
+ expect(repository).to receive(:update_default_branch).with("topic").ordered
143
+
144
+ expect(service.execute(upstream:)).to eq(repository)
145
+ expect(out.string).to include("Repository address: https://github.com/donald/geet")
146
+ expect(out.string).to include("Default branch: topic")
147
+ end
148
+
149
+ it "times out after all fork fetch attempts return 404" do
150
+ upstream = "https://github.com/upstream/project.git"
151
+ provisional_repository = instance_double(Geet::Github::Repository, full_name: "donald/geet")
152
+ allow(git_client).to receive(:remote_defined?).with("upstream").and_return(false)
153
+ allow(git_client).to receive(:remote_path).with(upstream).and_return("upstream/project")
154
+ allow(Geet::Github::Repository).to receive(:fork).and_return(provisional_repository)
155
+ allow(Geet::Github::Repository).to receive(:fetch)
156
+ .and_raise(Geet::Shared::HttpError.new("Not Found", 404))
157
+
158
+ expect { service.execute(upstream:) }.to raise_error("Timed out waiting for fork donald/geet")
159
+ end
160
+
161
+ it "immediately propagates a non-404 fork fetch error" do
162
+ upstream = "https://github.com/upstream/project.git"
163
+ provisional_repository = instance_double(Geet::Github::Repository, full_name: "donald/geet")
164
+ error = Geet::Shared::HttpError.new("Server Error", 500)
165
+ allow(git_client).to receive(:remote_defined?).with("upstream").and_return(false)
166
+ allow(git_client).to receive(:remote_path).with(upstream).and_return("upstream/project")
167
+ allow(Geet::Github::Repository).to receive(:fork).and_return(provisional_repository)
168
+ allow(Geet::Github::Repository).to receive(:fetch).and_raise(error)
169
+
170
+ expect { service.execute(upstream:) }.to raise_error(error)
171
+ end
172
+
173
+ it "stops before pushing or updating the default branch when adding a remote fails" do
174
+ repository = instance_double(
175
+ Geet::Github::Repository,
176
+ ssh_url: "git@github.com:donald/geet.git",
177
+ html_url: "https://github.com/donald/geet"
178
+ )
179
+ allow(Geet::Github::Repository).to receive(:create).and_return(repository)
180
+ allow(git_client).to receive(:add_remote).and_raise("remote failure")
181
+ expect(git_client).not_to receive(:push)
182
+ expect(repository).not_to receive(:update_default_branch)
183
+
184
+ expect { service.execute(visibility: "private") }.to raise_error("remote failure")
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+ # typed: false
3
+
4
+ require "spec_helper"
5
+
6
+ require_relative "../../../lib/geet/git/repository"
7
+ require_relative "../../../lib/geet/services/merge_pr"
8
+
9
+ describe Geet::Services::MergePr do
10
+ describe "#execute" do
11
+ let(:repository) { instance_double(Geet::Git::Repository) }
12
+ let(:git_client) { instance_double(Geet::Utils::GitClient) }
13
+
14
+ subject { described_class.new(repository, out: StringIO.new, git_client: git_client) }
15
+
16
+ it "raises when both --rebase and --squash are passed" do
17
+ expect { subject.execute(rebase: true, squash: true) }
18
+ .to raise_error("Can't specify both --rebase and --squash")
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "tmpdir"
5
+
6
+ describe Geet::Utils::GitClient do
7
+ subject { described_class.new }
8
+
9
+ describe "#head_commit?" do
10
+ it "returns true when HEAD resolves" do
11
+ allow(subject).to receive(:execute_git_command).with("rev-parse --verify HEAD", allow_error: true, silent_stderr: true).and_return("abc123")
12
+ expect(subject.head_commit?).to be(true)
13
+ end
14
+
15
+ it "returns false for an unborn HEAD" do
16
+ allow(subject).to receive(:execute_git_command).with("rev-parse --verify HEAD", allow_error: true, silent_stderr: true).and_return("")
17
+ expect(subject.head_commit?).to be(false)
18
+ end
19
+ end
20
+
21
+ describe "#remote_path" do
22
+ it "extracts an SSH repository path" do
23
+ expect(subject.remote_path("git@github.com:owner/project.git")).to eq("owner/project")
24
+ end
25
+
26
+ it "extracts an HTTPS repository path" do
27
+ expect(subject.remote_path("https://github.com/owner/project.git")).to eq("owner/project")
28
+ end
29
+
30
+ it "rejects an unsupported address" do
31
+ expect { subject.remote_path("owner/project") }.to raise_error(RuntimeError, 'Unexpected remote reference format: "owner/project"')
32
+ end
33
+ end
34
+
35
+ describe "#add_remote" do
36
+ it "passes a URL containing shell metacharacters as a single argument" do
37
+ Dir.mktmpdir do |directory|
38
+ marker = File.join(directory, "injected")
39
+ url = "https://example.com/owner/project.git; touch #{marker}"
40
+ git_client = described_class.new(location: directory)
41
+ system("git", "init", "--quiet", directory, exception: true)
42
+
43
+ git_client.add_remote("origin", url)
44
+
45
+ expect(File).not_to exist(marker)
46
+ expect(`git -C #{directory.shellescape} remote get-url origin`.strip).to eq(url)
47
+ end
48
+ end
49
+ end
50
+ end
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.30.0
4
+ version: 0.31.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Saverio Miroddi
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-05-12 00:00:00.000000000 Z
10
+ date: 2026-07-24 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: base64
@@ -43,14 +43,14 @@ dependencies:
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: 0.14.0
46
+ version: '0.14'
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
- version: 0.14.0
53
+ version: '0.14'
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: sorbet-runtime
56
56
  requirement: !ruby/object:Gem::Requirement
@@ -99,14 +99,14 @@ dependencies:
99
99
  requirements:
100
100
  - - "~>"
101
101
  - !ruby/object:Gem::Version
102
- version: '12.3'
102
+ version: '13.3'
103
103
  type: :development
104
104
  prerelease: false
105
105
  version_requirements: !ruby/object:Gem::Requirement
106
106
  requirements:
107
107
  - - "~>"
108
108
  - !ruby/object:Gem::Version
109
- version: '12.3'
109
+ version: '13.3'
110
110
  description: Commandline interface for performing SCM host operations, eg. create
111
111
  a PR on GitHub, with support for multiple hosts.
112
112
  email:
@@ -128,6 +128,12 @@ files:
128
128
  - Rakefile
129
129
  - bin/geet
130
130
  - bin/tapioca
131
+ - docs/superpowers/plans/2026-07-12-pre-existing-build-and-ci-fixes.md
132
+ - docs/superpowers/plans/2026-07-12-repo-create.md
133
+ - docs/superpowers/plans/2026-07-12-ruby-4-upgrade.md
134
+ - docs/superpowers/specs/2026-07-12-pre-existing-build-and-ci-fixes-design.md
135
+ - docs/superpowers/specs/2026-07-12-repo-create-design.md
136
+ - docs/superpowers/specs/2026-07-12-ruby-4-upgrade-design.md
131
137
  - extra/anonymize_vcr_data
132
138
  - extra/issue_editing.png
133
139
  - extra/pr_editing.png
@@ -145,6 +151,7 @@ files:
145
151
  - lib/geet/github/label.rb
146
152
  - lib/geet/github/milestone.rb
147
153
  - lib/geet/github/pr.rb
154
+ - lib/geet/github/repository.rb
148
155
  - lib/geet/github/user.rb
149
156
  - lib/geet/helpers/json_helper.rb
150
157
  - lib/geet/helpers/os_helper.rb
@@ -159,6 +166,7 @@ files:
159
166
  - lib/geet/services/create_label.rb
160
167
  - lib/geet/services/create_milestone.rb
161
168
  - lib/geet/services/create_pr.rb
169
+ - lib/geet/services/create_repo.rb
162
170
  - lib/geet/services/list_issues.rb
163
171
  - lib/geet/services/list_labels.rb
164
172
  - lib/geet/services/list_milestones.rb
@@ -201,7 +209,7 @@ files:
201
209
  - sorbet/rbi/gems/public_suffix@6.0.1.rbi
202
210
  - sorbet/rbi/gems/racc@1.8.1.rbi
203
211
  - sorbet/rbi/gems/rainbow@3.1.1.rbi
204
- - sorbet/rbi/gems/rake@12.3.3.rbi
212
+ - sorbet/rbi/gems/rake@13.4.2.rbi
205
213
  - sorbet/rbi/gems/rbi@0.3.7.rbi
206
214
  - sorbet/rbi/gems/rbs@4.0.0.dev.5.rbi
207
215
  - sorbet/rbi/gems/regexp_parser@2.11.2.rbi
@@ -250,7 +258,13 @@ files:
250
258
  - spec/integration/open_pr_spec.rb
251
259
  - spec/integration/open_repo_spec.rb
252
260
  - spec/spec_helper.rb
261
+ - spec/unit/commandline/configuration_spec.rb
262
+ - spec/unit/geet_launcher_spec.rb
253
263
  - spec/unit/github/pr_spec.rb
264
+ - spec/unit/github/repository_spec.rb
265
+ - spec/unit/services/create_repo_spec.rb
266
+ - spec/unit/services/merge_pr_spec.rb
267
+ - spec/unit/utils/git_client_spec.rb
254
268
  - spec/vcr_cassettes/create_gist_private.yml
255
269
  - spec/vcr_cassettes/create_gist_public.yml
256
270
  - spec/vcr_cassettes/create_issue.yml
@@ -289,14 +303,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
289
303
  requirements:
290
304
  - - ">="
291
305
  - !ruby/object:Gem::Version
292
- version: 3.2.0
306
+ version: 3.3.0
293
307
  required_rubygems_version: !ruby/object:Gem::Requirement
294
308
  requirements:
295
309
  - - ">="
296
310
  - !ruby/object:Gem::Version
297
311
  version: '0'
298
312
  requirements: []
299
- rubygems_version: 4.0.7
313
+ rubygems_version: 4.0.10
300
314
  specification_version: 4
301
315
  summary: Commandline interface for performing SCM host operations, eg. create a PR
302
316
  on GitHub