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