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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +2 -2
- data/.ruby-version +1 -1
- data/README.md +17 -1
- data/Rakefile +1 -0
- data/bin/geet +11 -3
- data/docs/superpowers/plans/2026-07-12-pre-existing-build-and-ci-fixes.md +89 -0
- data/docs/superpowers/plans/2026-07-12-repo-create.md +435 -0
- data/docs/superpowers/plans/2026-07-12-ruby-4-upgrade.md +111 -0
- data/docs/superpowers/specs/2026-07-12-pre-existing-build-and-ci-fixes-design.md +15 -0
- data/docs/superpowers/specs/2026-07-12-repo-create-design.md +43 -0
- data/docs/superpowers/specs/2026-07-12-ruby-4-upgrade-design.md +17 -0
- data/geet.gemspec +4 -4
- data/lib/geet/commandline/commands.rb +1 -0
- data/lib/geet/commandline/configuration.rb +7 -0
- data/lib/geet/github/repository.rb +88 -0
- data/lib/geet/services/create_repo.rb +108 -0
- data/lib/geet/services/open_repo.rb +2 -4
- data/lib/geet/utils/git_client.rb +15 -4
- data/lib/geet/version.rb +1 -1
- data/sorbet/rbi/gems/{rake@12.3.3.rbi → rake@13.4.2.rbi} +724 -486
- data/spec/integration/open_repo_spec.rb +1 -2
- data/spec/unit/commandline/configuration_spec.rb +34 -0
- data/spec/unit/geet_launcher_spec.rb +38 -0
- data/spec/unit/github/repository_spec.rb +40 -0
- data/spec/unit/services/create_repo_spec.rb +187 -0
- data/spec/unit/services/merge_pr_spec.rb +21 -0
- data/spec/unit/utils/git_client_spec.rb +50 -0
- metadata +23 -9
|
@@ -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.
|
|
9
|
+
s.required_ruby_version = ">= 3.3.0"
|
|
10
10
|
s.authors = ["Saverio Miroddi"]
|
|
11
|
-
s.date = "2026-
|
|
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
|
|
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", "~>
|
|
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(
|
|
16
|
-
def initialize(
|
|
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
|
-
|
|
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