Class: GemContribute::HostAdapters::GitHubAdapter

Inherits:
GemContribute::HostAdapter show all
Defined in:
lib/gem_contribute/host_adapters/github_adapter.rb

Overview

GitHub adapter. Implements the ‘HostAdapter` interface for github.com. Public read methods work anonymously; auth-required methods raise `AuthRequired` without a cached token (ADR-0001 / ADR-0004).

‘token` is optional. When present it’s sent as ‘Authorization: Bearer …` to lift the rate limit and unlock fork / comment / etc.

Class length: this adapter wraps a growing surface area of GitHub endpoints. The 150-line metric isn’t a useful constraint here — we’d just split into arbitrary sub-modules — so it’s disabled below with this rationale. rubocop:disable Metrics/ClassLength

Defined Under Namespace

Classes: RateLimit

Constant Summary collapse

API_BASE =
"https://api.github.com"
ACCEPT =
"application/vnd.github+json"
API_VERSION =
"2022-11-28"
MAX_REDIRECTS =
3
FORK_READINESS_RETRIES =

GitHub’s POST /forks returns 202 immediately; the fork resource may 404 for a few seconds while propagation finishes. Bound the wait at 12 × 5s = 60s.

12
FORK_READINESS_INTERVAL =
5

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(cache: Cache.new, http: Net::HTTP, token: nil, sleeper: ->(s) { Kernel.sleep(s) }) ⇒ GitHubAdapter

Returns a new instance of GitHubAdapter.



37
38
39
40
41
42
43
44
45
# File 'lib/gem_contribute/host_adapters/github_adapter.rb', line 37

def initialize(cache: Cache.new, http: Net::HTTP, token: nil,
               sleeper: ->(s) { Kernel.sleep(s) })
  super()
  @cache = cache
  @http = http
  @token = token
  @sleeper = sleeper
  @rate_limit = nil
end

Instance Attribute Details

#rate_limitObject (readonly)

Returns the value of attribute rate_limit.



35
36
37
# File 'lib/gem_contribute/host_adapters/github_adapter.rb', line 35

def rate_limit
  @rate_limit
end

Instance Method Details

#clone_url(owner, repo) ⇒ Object

Pure URL templating — no auth, no network. Used by Operations to construct the ‘upstream` remote and by CLI verbs for summary output.



166
167
168
# File 'lib/gem_contribute/host_adapters/github_adapter.rb', line 166

def clone_url(owner, repo)
  "https://github.com/#{owner}/#{repo}.git"
end

#comment(project, issue:, body:) ⇒ Object

POST /repos/:owner/:repo/issues/:n/comments. Returns the created comment payload (id, body, html_url, …). Used by ‘fix` to post the “working on this” announcement.

Raises:



119
120
121
122
123
124
125
# File 'lib/gem_contribute/host_adapters/github_adapter.rb', line 119

def comment(project, issue:, body:)
  raise AuthRequired, "github.com" unless @token

  ensure_known_host!(project)
  post_json("/repos/#{project.owner}/#{project.repo}/issues/#{issue}/comments",
            { "body" => body })
end

#community_profile(project) ⇒ Object



71
72
73
74
75
76
77
78
79
# File 'lib/gem_contribute/host_adapters/github_adapter.rb', line 71

def community_profile(project)
  ensure_known_host!(project)
  cache_key = "#{project.owner}/#{project.repo}"
  cached = @cache.fetch("repos", cache_key)
  return cached if cached

  body = get_json("/repos/#{project.owner}/#{project.repo}/community/profile")
  @cache.write("repos", cache_key, body)
end

#file_contents(project, path) ⇒ Object



81
82
83
84
85
86
87
88
89
# File 'lib/gem_contribute/host_adapters/github_adapter.rb', line 81

def file_contents(project, path)
  ensure_known_host!(project)
  cache_key = "#{project.owner}/#{project.repo}:#{path}"
  cached = @cache.fetch("files", cache_key)
  return cached if cached

  body = get_json("/repos/#{project.owner}/#{project.repo}/contents/#{path}")
  @cache.write("files", cache_key, body)
end

#fork(project) ⇒ Object

Idempotent, blocking fork. If the viewer already owns a fork at the same name, returns it as ‘reused: true` without a POST. Otherwise POSTs to /repos/:owner/:repo/forks and polls until the fork is reachable. Returns a `HostAdapter::ForkResult`.

Raises:



95
96
97
98
99
100
101
102
103
104
105
# File 'lib/gem_contribute/host_adapters/github_adapter.rb', line 95

def fork(project)
  raise AuthRequired, "github.com" unless @token

  ensure_known_host!(project)
  viewer = 
  return existing_fork_result(viewer, project) if fork_exists?(viewer, project.repo)

  body = post_json("/repos/#{project.owner}/#{project.repo}/forks")
  wait_until_fork_ready(viewer, project.repo)
  new_fork_result(viewer, project, body)
end

#issue(project, number) ⇒ Hash

Returns a single issue’s full payload (uncached — submit only).

Returns:

  • (Hash)

    a single issue’s full payload (uncached — submit only).



48
49
50
51
# File 'lib/gem_contribute/host_adapters/github_adapter.rb', line 48

def issue(project, number)
  ensure_known_host!(project)
  get_json("/repos/#{project.owner}/#{project.repo}/issues/#{number}")
end

#issue_comments(project, number) ⇒ Object

GET /repos/:owner/:repo/issues/:n/comments. Returns an array of comment payloads. Uncached (callers may want fresh data, e.g. to check for an idempotency marker).

Raises:



130
131
132
133
134
135
# File 'lib/gem_contribute/host_adapters/github_adapter.rb', line 130

def issue_comments(project, number)
  raise AuthRequired, "github.com" unless @token

  ensure_known_host!(project)
  get_json("/repos/#{project.owner}/#{project.repo}/issues/#{number}/comments")
end

#issues(project, labels: nil) ⇒ Array<Hash>

Returns open issues filtered to the given labels (if any).

Returns:

  • (Array<Hash>)

    open issues filtered to the given labels (if any)



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/gem_contribute/host_adapters/github_adapter.rb', line 54

def issues(project, labels: nil)
  ensure_known_host!(project)

  cache_key = issue_cache_key(project, labels)
  cached = @cache.fetch("issues", cache_key)
  return cached if cached

  params = { state: "open", per_page: 50 }
  params[:labels] = Array(labels).join(",") if labels && !Array(labels).empty?
  body = get_json("/repos/#{project.owner}/#{project.repo}/issues", params)

  # GitHub's /issues endpoint mixes pull requests in. PRs have a
  # `pull_request` key; filter those out so callers see issues only.
  only_issues = body.reject { |i| i.key?("pull_request") }
  @cache.write("issues", cache_key, only_issues)
end

#pull_request_url(upstream, head_owner:, head_branch:, title:, body:) ⇒ Object

Builds GitHub’s pre-filled compare URL. The browser-based PR flow (ADR-0011) means the user reviews the title/body before submitting, so this method just templates — it doesn’t post.



155
156
157
158
159
160
161
162
# File 'lib/gem_contribute/host_adapters/github_adapter.rb', line 155

def pull_request_url(upstream, head_owner:, head_branch:, title:, body:)
  ensure_known_host!(upstream)
  same_repo = head_owner == upstream.owner
  head = same_repo ? head_branch : "#{head_owner}:#{head_branch}"
  params = { "expand" => "1", "title" => title, "body" => body }
  "https://github.com/#{upstream.owner}/#{upstream.repo}/compare/#{head}?" \
    "#{URI.encode_www_form(params)}"
end

#repo_url(owner, repo) ⇒ Object



170
171
172
# File 'lib/gem_contribute/host_adapters/github_adapter.rb', line 170

def repo_url(owner, repo)
  "https://github.com/#{owner}/#{repo}"
end

#search_issues(query) ⇒ Object

GET /search/issues. Wraps GitHub’s issue search; works without auth (subject to the 60/hr anonymous rate limit). Returns an array of issue payloads (the search response’s ‘items` key). Cached under the `issues` namespace using the query as the key. Used to find issues already claimed via the gem-contribute marker.



142
143
144
145
146
147
148
149
150
# File 'lib/gem_contribute/host_adapters/github_adapter.rb', line 142

def search_issues(query)
  cache_key = "search:#{query}"
  cached = @cache.fetch("issues", cache_key)
  return cached if cached

  raw = get_json("/search/issues", q: query)
  items = raw.fetch("items", [])
  @cache.write("issues", cache_key, items)
end

#viewer_loginObject

GET /user. Used by ‘auth status` and internally by `fork`. Returns the authenticated user’s login string (e.g. “cdhagmann”).

Raises:



109
110
111
112
113
114
# File 'lib/gem_contribute/host_adapters/github_adapter.rb', line 109

def 
  raise AuthRequired, "github.com" unless @token

  body = get_json("/user")
  body.fetch("login")
end