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.
@@ -0,0 +1,111 @@
1
+ # Ruby 4.0.5 Upgrade Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Make Ruby 4.0.5 the project default while supporting Ruby 3.3 and newer.
6
+
7
+ **Architecture:** Keep the upgrade confined to the project's Ruby version declaration, gem compatibility metadata, CI matrix, and any dependency or code incompatibility demonstrated by the verification suite. Do not refresh dependencies or refactor code without a Ruby 4.0.5 failure that requires it.
8
+
9
+ **Tech Stack:** Ruby 4.0.5, Bundler 4.0.3, RSpec, Sorbet, RuboCop, GitHub Actions
10
+
11
+ ## Global Constraints
12
+
13
+ - The project development Ruby version is exactly `4.0.5`.
14
+ - The gem's minimum supported Ruby version is exactly `3.3.0`.
15
+ - CI covers Ruby head, 4.0, 3.4, and 3.3.
16
+ - Dependency and source changes require a demonstrated Ruby 4.0.5 incompatibility.
17
+
18
+ ---
19
+
20
+ ### Task 1: Ruby support declarations and compatibility verification
21
+
22
+ **Files:**
23
+ - Modify: `.ruby-version`
24
+ - Modify: `geet.gemspec`
25
+ - Modify: `.github/workflows/ci.yml`
26
+ - Modify if required by Bundler: `Gemfile.lock`
27
+ - Modify if required by a failing check: the source or dependency declaration responsible for that failure
28
+
29
+ **Interfaces:**
30
+ - Consumes: Ruby version selection by `.ruby-version`, RubyGems metadata from `geet.gemspec`, and the GitHub Actions Ruby matrix
31
+ - Produces: a Ruby 4.0.5 development environment and a tested Ruby 3.3-or-newer compatibility contract
32
+
33
+ - [ ] **Step 1: Confirm the existing declarations have the expected old values**
34
+
35
+ Run:
36
+
37
+ ```bash
38
+ test "$(< .ruby-version)" = "3.4.6" \
39
+ && rg -n 'required_ruby_version = ">= 3\.2\.0"' geet.gemspec \
40
+ && rg -n 'ruby-version: \[head, 4\.0, 3\.4, 3\.3, 3\.2\]' .github/workflows/ci.yml
41
+ ```
42
+
43
+ Expected: exit status 0 and matches for the gemspec and CI matrix.
44
+
45
+ - [ ] **Step 2: Update the Ruby support declarations**
46
+
47
+ Set `.ruby-version` to:
48
+
49
+ ```text
50
+ 4.0.5
51
+ ```
52
+
53
+ Set the gemspec declaration to:
54
+
55
+ ```ruby
56
+ s.required_ruby_version = ">= 3.3.0"
57
+ ```
58
+
59
+ Set the CI matrix to:
60
+
61
+ ```yaml
62
+ ruby-version: [head, 4.0, 3.4, 3.3]
63
+ ```
64
+
65
+ - [ ] **Step 3: Validate dependency resolution under Ruby 4.0.5**
66
+
67
+ Run:
68
+
69
+ ```bash
70
+ ruby --version \
71
+ && bundle check
72
+ ```
73
+
74
+ Expected: Ruby reports `ruby 4.0.5`; Bundler reports that all dependencies are satisfied. If `bundle check` reports missing dependencies, run `bundle install` and retain only lockfile changes required for Ruby 4.0.5.
75
+
76
+ - [ ] **Step 4: Run the project verification suite**
77
+
78
+ Run:
79
+
80
+ ```bash
81
+ bundle exec rspec \
82
+ && bundle exec srb typecheck \
83
+ && bundle exec rubocop \
84
+ && bundle exec rake build
85
+ ```
86
+
87
+ Expected: all specs pass, Sorbet reports no errors, RuboCop reports no offenses, and RubyGems builds the gem. If a command fails because of Ruby 4.0.5, make the smallest source or dependency change that resolves that specific failure, then rerun the entire command sequence.
88
+
89
+ - [ ] **Step 5: Check declarations and the resulting diff**
90
+
91
+ Run:
92
+
93
+ ```bash
94
+ test "$(< .ruby-version)" = "4.0.5" \
95
+ && rg -n 'required_ruby_version = ">= 3\.3\.0"' geet.gemspec \
96
+ && rg -n 'ruby-version: \[head, 4\.0, 3\.4, 3\.3\]' .github/workflows/ci.yml \
97
+ && ! rg -n '3\.4\.6|required_ruby_version = ">= 3\.2\.0"|ruby-version: \[head, 4\.0, 3\.4, 3\.3, 3\.2\]' . --glob '!docs/superpowers/**' --glob '!.git/**' \
98
+ && git diff --check \
99
+ && git status --short
100
+ ```
101
+
102
+ Expected: exit status 0, only intended files are modified, and the diff has no whitespace errors.
103
+
104
+ - [ ] **Step 6: Commit the upgrade**
105
+
106
+ ```bash
107
+ git add .ruby-version geet.gemspec .github/workflows/ci.yml Gemfile.lock \
108
+ && git commit -m "Upgrade development Ruby to 4.0.5"
109
+ ```
110
+
111
+ Omit `Gemfile.lock` from `git add` if it did not change. Add any verified compatibility fix files to the same command.
@@ -0,0 +1,15 @@
1
+ # Pre-existing Build and CI Fixes Design
2
+
3
+ ## Scope
4
+
5
+ Fix the two pre-existing project issues exposed during Ruby 4.0.5 verification: the CI test job supplies the wrong token variable, and Rake does not define Bundler's standard gem tasks.
6
+
7
+ ## Changes
8
+
9
+ - Rename the CI environment key from `GITHUB_API_TOKEN` to `GH_TOKEN`, matching the application, README, and test harness contract.
10
+ - Require `bundler/gem_tasks` from `Rakefile` so `rake build` and the standard Bundler gem tasks are available.
11
+ - Do not add token aliases, fallback credentials, dependency updates, or unrelated build changes.
12
+
13
+ ## Verification
14
+
15
+ Run the specs using the same `GH_TOKEN` environment contract as CI, then run Sorbet, RuboCop, the default Rake task, and `rake build`. Remove the generated gem artifact and confirm the diff contains only the CI workflow and Rakefile changes.
@@ -0,0 +1,43 @@
1
+ # Repository creation design
2
+
3
+ ## Goal
4
+
5
+ Add `geet repo create` to create a GitHub repository from the current local Git repository, configure remotes, push the current branch, and make that branch the GitHub default branch.
6
+
7
+ ## Command
8
+
9
+ ```text
10
+ geet repo create [-v|--visibility private|public] [-u|--upstream ADDRESS]
11
+ ```
12
+
13
+ The repository name is the basename of the current directory.
14
+
15
+ Without `--upstream`, `--visibility` selects a private or public repository. If it is omitted, Geet prompts the user to choose between `private` and `public`.
16
+
17
+ With `--upstream`, Geet creates a GitHub fork. GitHub determines the fork visibility, so Geet does not prompt for visibility and rejects an explicitly passed `--visibility`. `ADDRESS` may use any GitHub clone-address format Geet can resolve. Geet preserves the supplied address unchanged when configuring the local `upstream` remote.
18
+
19
+ ## Architecture
20
+
21
+ The command decoder registers `repo.create`, and the launcher dispatches it without constructing `Git::Repository`, which assumes an existing `origin`.
22
+
23
+ `Services::CreateRepo` coordinates the complete workflow. A GitHub repository API object performs authenticated-user lookup, repository or fork creation, fork availability checks, and default-branch updates. `Utils::GitClient` exposes the local Git operations required by the service.
24
+
25
+ ## Workflow
26
+
27
+ Before changing local or remote state, the service verifies that the current directory is a Git repository with a named branch and at least one commit. It fails if `origin` exists. In fork mode, it also fails if `upstream` exists. It validates visibility and the incompatibility between `--visibility` and `--upstream` during this preflight.
28
+
29
+ Normal mode creates a repository for the authenticated GitHub user with the selected visibility. Fork mode extracts the upstream repository path, requests a fork named after the current directory, and waits until GitHub makes the asynchronous fork available.
30
+
31
+ The service adds the created repository as `origin` using `git@github.com:OWNER/REPOSITORY.git`. In fork mode, it also adds the supplied address as `upstream`. It pushes the current branch to `origin` with upstream tracking, then updates the GitHub repository's default branch to the pushed branch.
32
+
33
+ On success, the command prints the created repository address and confirms the pushed default branch.
34
+
35
+ ## Failure handling
36
+
37
+ Preflight failures occur before GitHub repository creation. GitHub API and Git command errors retain their existing messages and stop the workflow immediately.
38
+
39
+ If a step after repository creation fails, Geet does not delete the GitHub repository or remove configured remotes. The retained state allows manual correction without destructive rollback.
40
+
41
+ ## Testing
42
+
43
+ Tests cover command decoding and dispatch, prompted and explicit visibility, invalid visibility, the `--upstream` and `--visibility` conflict, existing remotes, normal creation, asynchronous fork creation, GitHub address parsing, remote configuration, pushing with upstream tracking, default-branch updates, success output, and stopping after failures.
@@ -0,0 +1,17 @@
1
+ # Ruby 4.0.5 Upgrade Design
2
+
3
+ ## Scope
4
+
5
+ Use Ruby 4.0.5 as the project development version, raise the gem's minimum supported Ruby version from 3.2 to 3.3, and retain CI coverage for every supported Ruby minor version through 4.0 plus Ruby head.
6
+
7
+ ## Changes
8
+
9
+ - Set `.ruby-version` to `4.0.5`.
10
+ - Set `required_ruby_version` to `>= 3.3.0` in `geet.gemspec`.
11
+ - Remove Ruby 3.2 from the CI matrix while retaining Ruby head, 4.0, 3.4, and 3.3.
12
+ - Re-resolve dependencies only if Ruby 4.0.5 requires lockfile changes.
13
+ - Make code or dependency changes only for incompatibilities demonstrated by verification.
14
+
15
+ ## Verification
16
+
17
+ Under Ruby 4.0.5, install the locked bundle and run the RSpec suite, Sorbet typecheck, RuboCop, and gem build. Confirm the lockfile and repository contain no remaining declarations that incorrectly make Ruby 3.2 a supported version or Ruby 3.4.6 the development version.
data/geet.gemspec CHANGED
@@ -6,9 +6,9 @@ Gem::Specification.new do |s|
6
6
  s.name = "geet"
7
7
  s.version = Geet::VERSION
8
8
  s.platform = Gem::Platform::RUBY
9
- s.required_ruby_version = ">= 3.2.0"
9
+ s.required_ruby_version = ">= 3.3.0"
10
10
  s.authors = ["Saverio Miroddi"]
11
- s.date = "2026-05-12"
11
+ s.date = "2026-07-24"
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"
@@ -17,12 +17,12 @@ Gem::Specification.new do |s|
17
17
 
18
18
  s.add_runtime_dependency "base64", "~> 0.3.0"
19
19
  s.add_runtime_dependency "ostruct", "~> 0.6.3"
20
- s.add_runtime_dependency "simple_scripting", "~> 0.14.0"
20
+ s.add_runtime_dependency "simple_scripting", "~> 0.14"
21
21
  s.add_runtime_dependency "sorbet-runtime", "= 0.6.12883"
22
22
  s.add_runtime_dependency "tty-prompt", "~> 0.23.1"
23
23
  s.add_runtime_dependency "zeitwerk", "~> 2.7"
24
24
 
25
- s.add_development_dependency "rake", "~> 12.3"
25
+ s.add_development_dependency "rake", "~> 13.3"
26
26
 
27
27
  s.files = `git ls-files`.split("\n")
28
28
  s.executables << "geet"
@@ -18,6 +18,7 @@ module Geet
18
18
  PR_MERGE_COMMAND = T.let("pr.merge", String)
19
19
  PR_OPEN_COMMAND = T.let("pr.open", String)
20
20
  REPO_ADD_UPSTREAM_COMMAND = T.let("repo.add_upstream", String)
21
+ REPO_CREATE_COMMAND = T.let("repo.create", String)
21
22
  REPO_OPEN_COMMAND = T.let("repo.open", String)
22
23
  end
23
24
  end
@@ -111,6 +111,12 @@ module Geet
111
111
  long_help: "Add the upstream repository to the current repository (configuration).",
112
112
  ], T::Array[T.any(T::Hash[T.untyped, T.untyped], T::Array[String])])
113
113
 
114
+ REPO_CREATE_OPTIONS = T.let([
115
+ ["-v", "--visibility private|public", "Repository visibility"],
116
+ ["-u", "--upstream address", "Create a fork of this repository"],
117
+ long_help: "Create a GitHub repository from the current directory.",
118
+ ], T::Array[T.any(T::Hash[T.untyped, T.untyped], T::Array[String])])
119
+
114
120
  REPO_OPEN_OPTIONS = T.let([
115
121
  ["-u", "--upstream", "Open the upstream repository"],
116
122
  long_help: "Open the current repository in the browser",
@@ -144,6 +150,7 @@ module Geet
144
150
  },
145
151
  "repo" => {
146
152
  "add_upstream" => REPO_ADD_UPSTREAM_OPTIONS,
153
+ "create" => REPO_CREATE_OPTIONS,
147
154
  "open" => REPO_OPEN_OPTIONS,
148
155
  },
149
156
  }, T::Hash[String, T::Array[T.untyped]])
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ module Geet
5
+ module Github
6
+ class Repository
7
+ extend T::Sig
8
+
9
+ sig { returns(String) }
10
+ attr_reader :full_name
11
+
12
+ sig { returns(String) }
13
+ attr_reader :html_url
14
+
15
+ sig { returns(String) }
16
+ attr_reader :ssh_url
17
+
18
+ sig {
19
+ params(
20
+ name: String,
21
+ visibility: String,
22
+ api_interface: Geet::Github::ApiInterface
23
+ ).returns(Geet::Github::Repository)
24
+ }
25
+ def self.create(name, visibility, api_interface)
26
+ response = api_interface.send_request("/user/repos", data: {name:, private: visibility == "private"})
27
+ from_response(response, api_interface)
28
+ end
29
+
30
+ sig {
31
+ params(
32
+ upstream_path: String,
33
+ name: String,
34
+ api_interface: Geet::Github::ApiInterface
35
+ ).returns(Geet::Github::Repository)
36
+ }
37
+ def self.fork(upstream_path, name, api_interface)
38
+ response = api_interface.send_request("/repos/#{upstream_path}/forks", data: {name:})
39
+ from_response(response, api_interface)
40
+ end
41
+
42
+ sig {
43
+ params(
44
+ path: String,
45
+ api_interface: Geet::Github::ApiInterface
46
+ ).returns(Geet::Github::Repository)
47
+ }
48
+ def self.fetch(path, api_interface)
49
+ response = api_interface.send_request("/repos/#{path}")
50
+ from_response(response, api_interface)
51
+ end
52
+
53
+ sig {
54
+ params(
55
+ response: T::Hash[String, T.untyped],
56
+ api_interface: Geet::Github::ApiInterface
57
+ ).void
58
+ }
59
+ def initialize(response, api_interface)
60
+ @full_name = T.let(T.cast(response.fetch("full_name"), String), String)
61
+ @html_url = T.let(T.cast(response.fetch("html_url"), String), String)
62
+ @ssh_url = T.let(T.cast(response.fetch("ssh_url"), String), String)
63
+ @api_interface = T.let(api_interface, Geet::Github::ApiInterface)
64
+ end
65
+
66
+ sig { params(branch: String).void }
67
+ def update_default_branch(branch)
68
+ @api_interface.send_request("/repos/#{@full_name}", data: {default_branch: branch}, http_method: :patch)
69
+ end
70
+
71
+ class << self
72
+ extend T::Sig
73
+
74
+ private
75
+
76
+ sig {
77
+ params(
78
+ response: T.untyped,
79
+ api_interface: Geet::Github::ApiInterface
80
+ ).returns(Geet::Github::Repository)
81
+ }
82
+ def from_response(response, api_interface)
83
+ new(T.cast(response, T::Hash[String, T.untyped]), api_interface)
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ require "stringio"
5
+ require "tty-prompt"
6
+
7
+ module Geet
8
+ module Services
9
+ class CreateRepo
10
+ extend T::Sig
11
+
12
+ VISIBILITIES = T.let(["private", "public"].freeze, T::Array[String])
13
+ FORK_ATTEMPTS = 30
14
+ FORK_RETRY_DELAY = 1
15
+ DEFAULT_SLEEPER = T.let(->(delay) { Kernel.sleep(delay) }, T.proc.params(delay: Integer).void)
16
+
17
+ sig {
18
+ params(
19
+ directory: String,
20
+ out: T.any(IO, StringIO),
21
+ git_client: Utils::GitClient,
22
+ api_interface: T.nilable(Github::ApiInterface),
23
+ prompt: TTY::Prompt,
24
+ sleeper: T.proc.params(delay: Integer).void,
25
+ fork_attempts: Integer
26
+ ).void
27
+ }
28
+ def initialize(directory: Dir.pwd, out: $stdout, git_client: Utils::GitClient.new, api_interface: nil, prompt: TTY::Prompt.new, sleeper: DEFAULT_SLEEPER, fork_attempts: FORK_ATTEMPTS)
29
+ @directory = directory
30
+ @out = out
31
+ @git_client = git_client
32
+ @api_interface = T.let(
33
+ api_interface || Github::ApiInterface.new(ENV["GH_TOKEN"] || raise("GH_TOKEN not set!")),
34
+ Github::ApiInterface
35
+ )
36
+ @prompt = prompt
37
+ @sleeper = sleeper
38
+ @fork_attempts = fork_attempts
39
+ end
40
+
41
+ sig { params(visibility: T.nilable(String), upstream: T.nilable(String)).returns(Github::Repository) }
42
+ def execute(visibility: nil, upstream: nil)
43
+ preflight(visibility:, upstream:)
44
+
45
+ name = File.basename(@directory)
46
+ branch = @git_client.current_branch
47
+ selected_visibility = T.let(nil, T.nilable(String))
48
+ selected_visibility = visibility || T.cast(@prompt.select("Visibility:", VISIBILITIES), String) if !upstream
49
+ repository = upstream ? create_fork(name, upstream) : Github::Repository.create(name, selected_visibility, @api_interface)
50
+
51
+ @git_client.add_remote(Utils::GitClient::ORIGIN_NAME, repository.ssh_url)
52
+ @git_client.add_remote(Utils::GitClient::UPSTREAM_NAME, upstream) if upstream
53
+ @git_client.push(remote_branch: branch)
54
+ repository.update_default_branch(branch)
55
+ @out.puts "Repository address: #{repository.html_url}"
56
+ @out.puts "Default branch: #{branch}"
57
+ repository
58
+ end
59
+
60
+ private
61
+
62
+ sig { params(name: String, upstream: String).returns(Github::Repository) }
63
+ def create_fork(name, upstream)
64
+ upstream_path = @git_client.remote_path(upstream)
65
+ provisional_repository = Github::Repository.fork(upstream_path, name, @api_interface)
66
+ wait_for_fork(provisional_repository.full_name)
67
+ end
68
+
69
+ sig { params(full_name: String).returns(Github::Repository) }
70
+ def wait_for_fork(full_name)
71
+ @fork_attempts.times do |attempt|
72
+ begin
73
+ return Github::Repository.fetch(full_name, @api_interface)
74
+ rescue Shared::HttpError => error
75
+ raise if error.code != 404
76
+ @sleeper.call(FORK_RETRY_DELAY) if attempt < @fork_attempts - 1
77
+ end
78
+ end
79
+
80
+ raise "Timed out waiting for fork #{full_name}"
81
+ end
82
+
83
+ sig { params(visibility: T.nilable(String), upstream: T.nilable(String)).void }
84
+ def preflight(visibility:, upstream:)
85
+ validate_commits
86
+ validate_remotes(upstream)
87
+ validate_visibility(visibility, upstream)
88
+ end
89
+
90
+ sig { void }
91
+ def validate_commits
92
+ raise "The repository has no commits!" if !@git_client.head_commit?
93
+ end
94
+
95
+ sig { params(upstream: T.nilable(String)).void }
96
+ def validate_remotes(upstream)
97
+ raise 'Remote "origin" already exists!' if @git_client.remote_defined?(Utils::GitClient::ORIGIN_NAME)
98
+ raise 'Remote "upstream" already exists!' if upstream && @git_client.remote_defined?(Utils::GitClient::UPSTREAM_NAME)
99
+ end
100
+
101
+ sig { params(visibility: T.nilable(String), upstream: T.nilable(String)).void }
102
+ def validate_visibility(visibility, upstream)
103
+ raise "Visibility can't be specified with upstream!" if visibility && upstream
104
+ raise "Visibility must be private or public!" if visibility && !VISIBILITIES.include?(visibility)
105
+ end
106
+ end
107
+ end
108
+ end
@@ -12,10 +12,8 @@ module Geet
12
12
 
13
13
  DEFAULT_GIT_CLIENT = Utils::GitClient.new
14
14
 
15
- sig { params(repository: Git::Repository, out: IO, git_client: Utils::GitClient).void }
16
- def initialize(repository, out: $stdout, git_client: DEFAULT_GIT_CLIENT)
17
- @repository = repository
18
- @out = out
15
+ sig { params(git_client: Utils::GitClient).void }
16
+ def initialize(git_client: DEFAULT_GIT_CLIENT)
19
17
  @git_client = git_client
20
18
  end
21
19
 
@@ -182,6 +182,11 @@ module Geet
182
182
  # REPOSITORY/REMOTE QUERYING APIS
183
183
  ##########################################################################
184
184
 
185
+ sig { returns(T::Boolean) }
186
+ def head_commit?
187
+ execute_git_command("rev-parse --verify HEAD", allow_error: true, silent_stderr: true) != ""
188
+ end
189
+
185
190
  # Return the components of the remote, according to REMOTE_URL_REGEX; doesn't include the full
186
191
  # match.
187
192
  #
@@ -201,7 +206,14 @@ module Geet
201
206
  def path(upstream: false)
202
207
  remote_name_option = upstream ? {name: UPSTREAM_NAME} : {}
203
208
 
204
- T.must(remote(**remote_name_option)[REMOTE_URL_REGEX, 4])
209
+ remote_path(remote(**remote_name_option))
210
+ end
211
+
212
+ sig { params(remote_url: String).returns(String) }
213
+ def remote_path(remote_url)
214
+ match = remote_url.match(REMOTE_URL_REGEX)
215
+ raise "Unexpected remote reference format: #{remote_url.inspect}" if !match
216
+ T.must(match[4])
205
217
  end
206
218
 
207
219
  sig { returns(String) }
@@ -225,10 +237,9 @@ module Geet
225
237
 
226
238
  if !remote_defined?(name)
227
239
  raise "Remote #{name.inspect} not found!"
228
- elsif remote_url !~ REMOTE_URL_REGEX
229
- raise "Unexpected remote reference format: #{remote_url.inspect}"
230
240
  end
231
241
 
242
+ remote_path(remote_url)
232
243
  remote_url
233
244
  end
234
245
 
@@ -287,7 +298,7 @@ module Geet
287
298
 
288
299
  sig { params(name: String, url: String).returns(String) }
289
300
  def add_remote(name, url)
290
- execute_git_command("remote add #{name.shellescape} #{url}")
301
+ execute_git_command("remote add #{name.shellescape} #{url.shellescape}")
291
302
  end
292
303
 
293
304
  ##########################################################################
data/lib/geet/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # typed: strict
3
3
 
4
4
  module Geet
5
- VERSION = "0.30.0"
5
+ VERSION = "0.31.0"
6
6
  end