gem-contribute 0.2.0 → 0.3.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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.github/PULL_REQUEST_TEMPLATE.md +14 -8
  3. data/.github/workflows/ci.yml +26 -0
  4. data/.github/workflows/pr-template-check.yml +100 -0
  5. data/.github/workflows/release.yml +71 -0
  6. data/CHANGELOG.md +38 -0
  7. data/CLAUDE.md +1 -1
  8. data/CONTRIBUTING.md +10 -4
  9. data/MAINTAINER.md +119 -2
  10. data/README.md +13 -1
  11. data/docs/OPEN_QUESTIONS.md +167 -0
  12. data/docs/ROADMAP.md +266 -0
  13. data/docs/adr/0006-standalone-gem-not-plugin.md +1 -1
  14. data/docs/adr/0008-rooibos-tui-framework.md +3 -3
  15. data/docs/adr/0010-charm-ruby-tui-framework.md +2 -2
  16. data/docs/adr/0011-host-adapter-owns-host-verbs.md +58 -0
  17. data/docs/adr/0012-output-free-service-objects-three-interface-architecture.md +79 -0
  18. data/docs/adr/0013-revert-to-rooibos.md +71 -0
  19. data/docs/adr/0014-ship-bundler-and-rubygems-plugins.md +75 -0
  20. data/docs/adr/README.md +7 -3
  21. data/docs/design-interface-layer.md +295 -0
  22. data/docs/design.md +31 -8
  23. data/docs/index.md +1 -1
  24. data/docs/prep-plan.md +6 -6
  25. data/lib/gem_contribute/cli/auth.rb +22 -44
  26. data/lib/gem_contribute/cli/config.rb +29 -15
  27. data/lib/gem_contribute/cli/fix.rb +122 -0
  28. data/lib/gem_contribute/cli/fork.rb +145 -0
  29. data/lib/gem_contribute/cli/init.rb +19 -24
  30. data/lib/gem_contribute/cli/issue_announcer.rb +42 -0
  31. data/lib/gem_contribute/cli/issues.rb +36 -47
  32. data/lib/gem_contribute/cli/platform_tools.rb +33 -0
  33. data/lib/gem_contribute/cli/post_clone_hooks.rb +50 -0
  34. data/lib/gem_contribute/cli/rate_limit_footer.rb +5 -3
  35. data/lib/gem_contribute/cli/scan.rb +20 -16
  36. data/lib/gem_contribute/cli/submit.rb +60 -64
  37. data/lib/gem_contribute/cli/workflow.rb +63 -0
  38. data/lib/gem_contribute/cli.rb +9 -16
  39. data/lib/gem_contribute/config.rb +27 -1
  40. data/lib/gem_contribute/git.rb +49 -0
  41. data/lib/gem_contribute/host_adapter.rb +52 -5
  42. data/lib/gem_contribute/host_adapters/github_adapter.rb +126 -37
  43. data/lib/gem_contribute/operations/announce.rb +52 -0
  44. data/lib/gem_contribute/operations/branch.rb +35 -0
  45. data/lib/gem_contribute/operations/clone.rb +41 -0
  46. data/lib/gem_contribute/operations/fix_pipeline.rb +70 -0
  47. data/lib/gem_contribute/operations/fork.rb +35 -0
  48. data/lib/gem_contribute/output/null.rb +20 -0
  49. data/lib/gem_contribute/output/standard.rb +71 -0
  50. data/lib/gem_contribute/version.rb +1 -1
  51. data/lib/gem_contribute.rb +10 -18
  52. metadata +115 -5
  53. data/lib/gem_contribute/cli/fork_clone_branch.rb +0 -204
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gem-contribute
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Hagmann
8
+ autorequire:
8
9
  bindir: exe
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-05-04 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: bundler
@@ -23,10 +24,94 @@ dependencies:
23
24
  - - ">="
24
25
  - !ruby/object:Gem::Version
25
26
  version: '2.4'
27
+ - !ruby/object:Gem::Dependency
28
+ name: dry-initializer
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: dry-monads
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.10'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.10'
55
+ - !ruby/object:Gem::Dependency
56
+ name: dry-operation
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.1'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.1'
69
+ - !ruby/object:Gem::Dependency
70
+ name: tty-prompt
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.23'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.23'
83
+ - !ruby/object:Gem::Dependency
84
+ name: tty-spinner
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.9'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.9'
97
+ - !ruby/object:Gem::Dependency
98
+ name: zeitwerk
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '2.6'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '2.6'
26
111
  description: |
27
112
  gem-contribute reads a project's Gemfile.lock, resolves each gem's source
28
113
  repository via the RubyGems API, surfaces open contributable issues from
29
- those repositories, and offers a one-keystroke fork-clone-branch flow so a
114
+ those repositories, and offers a one-keystroke fix flow so a
30
115
  developer can go from "I noticed an issue" to "I have a working branch" in
31
116
  seconds. v0.1 supports GitHub-hosted gems with OAuth device-flow auth.
32
117
  email:
@@ -40,6 +125,9 @@ files:
40
125
  - ".github/ISSUE_TEMPLATE/workshop-issue.md"
41
126
  - ".github/PULL_REQUEST_TEMPLATE.md"
42
127
  - ".github/workflows/auto-merge-kicked-tires.yml"
128
+ - ".github/workflows/ci.yml"
129
+ - ".github/workflows/pr-template-check.yml"
130
+ - ".github/workflows/release.yml"
43
131
  - CHANGELOG.md
44
132
  - CLAUDE.md
45
133
  - CODE_OF_CONDUCT.md
@@ -49,6 +137,8 @@ files:
49
137
  - MAINTAINER.md
50
138
  - README.md
51
139
  - Rakefile
140
+ - docs/OPEN_QUESTIONS.md
141
+ - docs/ROADMAP.md
52
142
  - docs/_config.yml
53
143
  - docs/adr/0001-just-in-time-auth.md
54
144
  - docs/adr/0002-bundler-lockfile-parser.md
@@ -60,8 +150,13 @@ files:
60
150
  - docs/adr/0008-rooibos-tui-framework.md
61
151
  - docs/adr/0009-top-level-namespace.md
62
152
  - docs/adr/0010-charm-ruby-tui-framework.md
153
+ - docs/adr/0011-host-adapter-owns-host-verbs.md
154
+ - docs/adr/0012-output-free-service-objects-three-interface-architecture.md
155
+ - docs/adr/0013-revert-to-rooibos.md
156
+ - docs/adr/0014-ship-bundler-and-rubygems-plugins.md
63
157
  - docs/adr/README.md
64
158
  - docs/claude-code-prompt.md
159
+ - docs/design-interface-layer.md
65
160
  - docs/design.md
66
161
  - docs/ideas.md
67
162
  - docs/index.md
@@ -78,18 +173,31 @@ files:
78
173
  - lib/gem_contribute/cli.rb
79
174
  - lib/gem_contribute/cli/auth.rb
80
175
  - lib/gem_contribute/cli/config.rb
81
- - lib/gem_contribute/cli/fork_clone_branch.rb
176
+ - lib/gem_contribute/cli/fix.rb
177
+ - lib/gem_contribute/cli/fork.rb
82
178
  - lib/gem_contribute/cli/init.rb
179
+ - lib/gem_contribute/cli/issue_announcer.rb
83
180
  - lib/gem_contribute/cli/issues.rb
181
+ - lib/gem_contribute/cli/platform_tools.rb
182
+ - lib/gem_contribute/cli/post_clone_hooks.rb
84
183
  - lib/gem_contribute/cli/rate_limit_footer.rb
85
184
  - lib/gem_contribute/cli/scan.rb
86
185
  - lib/gem_contribute/cli/submit.rb
186
+ - lib/gem_contribute/cli/workflow.rb
87
187
  - lib/gem_contribute/config.rb
88
188
  - lib/gem_contribute/errors.rb
189
+ - lib/gem_contribute/git.rb
89
190
  - lib/gem_contribute/host_adapter.rb
90
191
  - lib/gem_contribute/host_adapters/github_adapter.rb
91
192
  - lib/gem_contribute/locked_gem.rb
92
193
  - lib/gem_contribute/lockfile_parser.rb
194
+ - lib/gem_contribute/operations/announce.rb
195
+ - lib/gem_contribute/operations/branch.rb
196
+ - lib/gem_contribute/operations/clone.rb
197
+ - lib/gem_contribute/operations/fix_pipeline.rb
198
+ - lib/gem_contribute/operations/fork.rb
199
+ - lib/gem_contribute/output/null.rb
200
+ - lib/gem_contribute/output/standard.rb
93
201
  - lib/gem_contribute/project.rb
94
202
  - lib/gem_contribute/resolver.rb
95
203
  - lib/gem_contribute/token_store.rb
@@ -105,6 +213,7 @@ metadata:
105
213
  changelog_uri: https://github.com/cdhagmann/gem-contribute/blob/main/CHANGELOG.md
106
214
  documentation_uri: https://cdhagmann.com/gem-contribute/
107
215
  rubygems_mfa_required: 'true'
216
+ post_install_message:
108
217
  rdoc_options: []
109
218
  require_paths:
110
219
  - lib
@@ -119,7 +228,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
119
228
  - !ruby/object:Gem::Version
120
229
  version: '0'
121
230
  requirements: []
122
- rubygems_version: 4.0.10
231
+ rubygems_version: 3.4.19
232
+ signing_key:
123
233
  specification_version: 4
124
234
  summary: Find and contribute to the open-source Ruby gems your project depends on.
125
235
  test_files: []
@@ -1,204 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "open3"
4
-
5
- module GemContribute
6
- module CLI
7
- # `gem-contribute fork-clone-branch <gem>/<issue#>`
8
- #
9
- # Performs the full sequence the TUI's `f` keybinding will trigger in
10
- # Stage 3:
11
- #
12
- # 1. Resolve <gem> via the RubyGems Resolver (no lockfile required;
13
- # the lockfile is for discovery via `scan`, not gating here).
14
- # 2. Read the cached GitHub token; raise AuthRequired with a clear
15
- # `auth login` hint if missing.
16
- # 3. Look up the viewer's login.
17
- # 4. If they don't already have a fork, fork the upstream repo.
18
- # 5. Poll until the fork is reachable (forks return 202 immediately
19
- # but the resource may 404 for a few seconds).
20
- # 6. `git clone` the fork to `<clone_root>/<owner>/<repo>`.
21
- # 7. `git checkout -b gem-contribute/issue-<N>` from the default branch.
22
- # 8. Print the local path on stdout.
23
- #
24
- # The shell-outs use Open3 with explicit args (not strings) to avoid any
25
- # shell-injection surface.
26
- class ForkCloneBranch
27
- DEFAULT_CLONE_ROOT = File.expand_path("~/code/oss")
28
- BRANCH_PREFIX = "gem-contribute/issue-"
29
- FORK_READINESS_RETRIES = 12 # 12 × 5s = 60s ceiling
30
- FORK_READINESS_INTERVAL = 5
31
-
32
- def initialize(stdout: $stdout,
33
- stderr: $stderr,
34
- resolver: Resolver.new,
35
- store: TokenStore.new,
36
- adapter_factory: ->(token:) { HostAdapters::GitHubAdapter.new(token: token) },
37
- git: Git.new,
38
- clone_root: DEFAULT_CLONE_ROOT,
39
- sleeper: ->(s) { Kernel.sleep(s) })
40
- @stdout = stdout
41
- @stderr = stderr
42
- @resolver = resolver
43
- @store = store
44
- @adapter_factory = adapter_factory
45
- @git = git
46
- @clone_root = clone_root
47
- @sleeper = sleeper
48
- end
49
-
50
- def run(argv)
51
- return missing_clone_root if @clone_root.nil?
52
-
53
- target = argv.shift
54
- return print_usage_error if target.nil? || !target.include?("/")
55
-
56
- gem_name, issue = target.split("/", 2)
57
- adapter = build_adapter
58
- return 1 if adapter.nil?
59
-
60
- project = resolve_or_fail(gem_name)
61
- return 1 if project.nil?
62
-
63
- execute(adapter, project, issue)
64
- rescue AuthRequired
65
- @stderr.puts "Not authenticated. Run `gem-contribute auth login` first."
66
- 1
67
- rescue AdapterError => e
68
- @stderr.puts "fork-clone-branch failed: #{e.message}"
69
- 1
70
- end
71
-
72
- private
73
-
74
- def missing_clone_root
75
- @stderr.puts "clone_root is not configured. Run `gem-contribute init` first."
76
- 1
77
- end
78
-
79
- def print_usage_error
80
- @stderr.puts "Usage: gem-contribute fork-clone-branch <gem>/<issue#>"
81
- 2
82
- end
83
-
84
- def build_adapter
85
- token = @store.token_for("github.com")
86
- if token.nil?
87
- @stderr.puts "Not authenticated. Run `gem-contribute auth login` first."
88
- return nil
89
- end
90
- @adapter_factory.call(token: token)
91
- end
92
-
93
- def resolve_or_fail(gem_name)
94
- return GemContribute::SELF_PROJECT if gem_name == GemContribute::SELF_PROJECT.gem_name
95
-
96
- gem = LockedGem.new(name: gem_name, version: "*", source_type: :rubygems, source_uri: "https://rubygems.org/")
97
- project = @resolver.resolve(gem)
98
-
99
- if project.host != "github.com"
100
- @stderr.puts "Cannot fork-clone-branch: #{gem_name} resolves to #{project.host} " \
101
- "(only github.com is supported at v0.1)"
102
- return nil
103
- end
104
-
105
- project
106
- end
107
-
108
- def execute(adapter, project, issue)
109
- viewer = adapter.viewer_login
110
- clone_url = ensure_fork(adapter, project, viewer)
111
- local_path = clone_into_root(project, clone_url)
112
- branch_name = "#{BRANCH_PREFIX}#{issue}"
113
- @git.checkout_branch(local_path, branch_name)
114
- # `submit` needs to know the canonical project to point the PR at.
115
- # Naming it `upstream` follows the standard fork workflow convention.
116
- @git.add_remote(local_path, "upstream",
117
- "https://github.com/#{project.owner}/#{project.repo}.git")
118
-
119
- @stdout.puts "Forked, cloned, and branched."
120
- @stdout.puts " path: #{local_path}"
121
- @stdout.puts " branch: #{branch_name}"
122
- @stdout.puts " upstream: https://github.com/#{project.owner}/#{project.repo}"
123
- @stdout.puts " fork: https://github.com/#{viewer}/#{project.repo}"
124
- @stdout.puts
125
- @stdout.puts "Next: cd #{local_path} && make your changes, then `gem-contribute submit`."
126
- 0
127
- end
128
-
129
- def ensure_fork(adapter, project, viewer)
130
- if adapter.already_forked?(project)
131
- @stdout.puts "You already have a fork at #{viewer}/#{project.repo}. Skipping fork."
132
- return "https://github.com/#{viewer}/#{project.repo}.git"
133
- end
134
-
135
- @stdout.puts "Forking #{project.owner}/#{project.repo} → #{viewer}/#{project.repo}..."
136
- body = adapter.fork(project)
137
- wait_until_ready(adapter, viewer, project.repo)
138
- body.fetch("clone_url")
139
- end
140
-
141
- def wait_until_ready(adapter, viewer, name)
142
- ready = FORK_READINESS_RETRIES.times.any? do |i|
143
- break true if adapter.fork_ready?(viewer, name)
144
-
145
- @sleeper.call(FORK_READINESS_INTERVAL) unless i == FORK_READINESS_RETRIES - 1
146
- false
147
- end
148
- return if ready
149
-
150
- raise AdapterError, "fork not reachable after #{FORK_READINESS_RETRIES * FORK_READINESS_INTERVAL}s"
151
- end
152
-
153
- def clone_into_root(project, clone_url)
154
- target = File.join(@clone_root, project.owner, project.repo)
155
- if File.directory?(File.join(target, ".git"))
156
- @stdout.puts "Reusing existing clone at #{target}."
157
- return target
158
- end
159
-
160
- FileUtils.mkdir_p(File.dirname(target))
161
- @stdout.puts "Cloning into #{target}..."
162
- @git.clone(clone_url, target)
163
- target
164
- end
165
- end
166
-
167
- # Thin wrapper around git so the spec can swap in a fake without shelling
168
- # out. The real implementation uses Open3 with arg-list invocation — no
169
- # shell, so no injection surface.
170
- class Git
171
- def clone(url, target)
172
- run!(["git", "clone", url, target])
173
- end
174
-
175
- def checkout_branch(path, branch)
176
- run!(["git", "-C", path, "checkout", "-b", branch])
177
- end
178
-
179
- def add_remote(path, name, url)
180
- # Idempotent: if the remote already exists (e.g. reusing a clone)
181
- # we silently succeed rather than fail the whole flow.
182
- return if remote_exists?(path, name)
183
-
184
- run!(["git", "-C", path, "remote", "add", name, url])
185
- end
186
-
187
- def push(path, remote, branch)
188
- run!(["git", "-C", path, "push", "-u", remote, branch])
189
- end
190
-
191
- def remote_exists?(path, name)
192
- out, _err, status = Open3.capture3("git", "-C", path, "remote")
193
- status.success? && out.split("\n").include?(name)
194
- end
195
-
196
- def run!(argv)
197
- _stdout, stderr_str, status = Open3.capture3(*argv)
198
- return if status.success?
199
-
200
- raise GemContribute::AdapterError, "git #{argv[1..].join(" ")} failed: #{stderr_str.strip}"
201
- end
202
- end
203
- end
204
- end