Class: Aidp::Watch::RepositoryClient

Inherits:
Object
  • Object
show all
Defined in:
lib/aidp/watch/repository_client.rb

Overview

Lightweight adapter around GitHub for watch mode. Prefers the GitHub CLI (works for private repositories) and falls back to public REST endpoints when the CLI is unavailable.

Defined Under Namespace

Classes: BinaryChecker

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(owner:, repo:, gh_available: nil, binary_checker: BinaryChecker.new) ⇒ RepositoryClient

Returns a new instance of RepositoryClient.



42
43
44
45
46
47
# File 'lib/aidp/watch/repository_client.rb', line 42

def initialize(owner:, repo:, gh_available: nil, binary_checker: BinaryChecker.new)
  @owner = owner
  @repo = repo
  @binary_checker = binary_checker
  @gh_available = gh_available.nil? ? @binary_checker.gh_cli_available? : gh_available
end

Instance Attribute Details

#owner ⇒ Object (readonly)

Returns the value of attribute owner.



24
25
26
# File 'lib/aidp/watch/repository_client.rb', line 24

def owner
  @owner
end

#repo ⇒ Object (readonly)

Returns the value of attribute repo.



24
25
26
# File 'lib/aidp/watch/repository_client.rb', line 24

def repo
  @repo
end

Class Method Details

.parse_issues_url(issues_url) ⇒ Object

gh commands are executed in argument form: the executable is passed as a literal leading argument and every remaining token is a separate argument, so shell metacharacters in library-provided values (owner/repo, issue numbers, labels, comment bodies) are treated as literal data rather than interpreted by a shell.



31
32
33
34
35
36
37
38
39
40
# File 'lib/aidp/watch/repository_client.rb', line 31

def self.parse_issues_url(issues_url)
  case issues_url
  when %r{\Ahttps://github\.com/([^/]+)/([^/]+)(?:/issues)?/?\z}
    [::Regexp.last_match(1), ::Regexp.last_match(2)]
  when %r{\A([^/]+)/([^/]+)\z}
    [::Regexp.last_match(1), ::Regexp.last_match(2)]
  else
    raise ArgumentError, "Unsupported issues URL: #{issues_url}"
  end
end

Instance Method Details

#add_labels(number, *labels) ⇒ Object



91
92
93
# File 'lib/aidp/watch/repository_client.rb', line 91

def add_labels(number, *labels)
  gh_available? ? add_labels_via_gh(number, labels.flatten) : add_labels_via_api(number, labels.flatten)
end

#close_issue(number) ⇒ Object



331
332
333
334
# File 'lib/aidp/watch/repository_client.rb', line 331

def close_issue(number)
  raise "GitHub CLI not available - cannot close issue" unless gh_available?
  close_issue_via_gh(number)
end

#consolidate_category_comment(number, category_header, content, append: false) ⇒ Object

Create or update a categorized comment (e.g., under a header) on an issue. If a comment with the category header exists, either append to it or replace it while archiving the previous content inline.



185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# File 'lib/aidp/watch/repository_client.rb', line 185

def consolidate_category_comment(number, category_header, content, append: false)
  Aidp.log_debug(
    "repository_client",
    "consolidate_category_comment_started",
    number: number,
    category_header: category_header,
    append: append,
    content_length: content.length,
    content_preview: content[0, 100]
  )

  existing_comment = find_comment(number, category_header)

  if existing_comment
    body = if append
      Aidp.log_debug(
        "repository_client",
        "updating_category_comment_appending",
        comment_id: existing_comment[:id],
        existing_body_length: existing_comment[:body].length,
        existing_body_preview: existing_comment[:body][0, 100],
        appending_content_length: content.length,
        appending_content_preview: content[0, 100]
      )
      "#{existing_comment[:body]}\n\n#{content}"
    else
      Aidp.log_debug(
        "repository_client",
        "updating_category_comment_replacing",
        comment_id: existing_comment[:id],
        existing_body_length: existing_comment[:body].length,
        existing_body_preview: existing_comment[:body][0, 100],
        replacement_content_length: content.length,
        replacement_content_preview: content[0, 100]
      )

      archived_prefix = "<!-- ARCHIVED_PLAN_START "
      archived_suffix = " ARCHIVED_PLAN_END -->"
      archived_content = "#{archived_prefix}#{Time.now.utc.iso8601}#{archived_suffix}\n\n#{existing_comment[:body].gsub(
        /^(#{Regexp.escape(category_header)}|#{Regexp.escape(archived_prefix)}.*?#{Regexp.escape(archived_suffix)})/m, ""
      )}\n\n"

      "#{category_header}\n\n#{archived_content}#{content}"
    end

    update_comment(existing_comment[:id], body)

    Aidp.log_debug(
      "repository_client",
      "existing_category_comment_updated",
      comment_id: existing_comment[:id],
      updated_body_length: body.length,
      updated_body_preview: body[0, 100],
      update_method: append ? "append" : "replace"
    )

    {
      id: existing_comment[:id],
      body: body
    }
  else
    body = "#{category_header}\n\n#{content}"

    post_comment(number, body)

    Aidp.log_debug(
      "repository_client",
      "new_category_comment_created",
      issue_number: number,
      body_length: body.length,
      body_preview: body[0, 100],
      category_header: category_header
    )

    {
      id: 999,
      body: body
    }
  end
rescue => e
  Aidp.log_error(
    "repository_client",
    "consolidate_category_comment_failed",
    error: e.message,
    error_class: e.class.name,
    number: number,
    category_header: category_header,
    content_length: content.length,
    content_preview: content[0, 100],
    backtrace: e.backtrace&.first(5)
  )

  raise RuntimeError, "GitHub error", e.backtrace
end

#create_issue(title:, body:, labels: [], assignees: []) ⇒ Object



326
327
328
329
# File 'lib/aidp/watch/repository_client.rb', line 326

def create_issue(title:, body:, labels: [], assignees: [])
  raise "GitHub CLI not available - cannot create issue" unless gh_available?
  create_issue_via_gh(title: title, body: body, labels: labels, assignees: assignees)
end

#create_project(title:, repository_id: nil) ⇒ Object



311
312
313
314
# File 'lib/aidp/watch/repository_client.rb', line 311

def create_project(title:, repository_id: nil)
  raise "GitHub CLI not available - Projects API requires gh CLI" unless gh_available?
  create_project_via_gh(title: title, repository_id: repository_id)
end

#create_project_field(project_id, name, field_type, options: nil) ⇒ Object



306
307
308
309
# File 'lib/aidp/watch/repository_client.rb', line 306

def create_project_field(project_id, name, field_type, options: nil)
  raise "GitHub CLI not available - Projects API requires gh CLI" unless gh_available?
  create_project_field_via_gh(project_id, name, field_type, options: options)
end

#create_pull_request(title:, body:, head:, base:, issue_number:, draft: false, assignee: nil) ⇒ Object



84
85
86
87
88
89
# File 'lib/aidp/watch/repository_client.rb', line 84

def create_pull_request(title:, body:, head:, base:, issue_number:, draft: false, assignee: nil)
  raise("GitHub CLI not available - cannot create PR") unless gh_available?

  create_pull_request_via_gh(title: title, body: body, head: head, base: base,
    issue_number: issue_number, draft: draft, assignee: assignee)
end

#fetch_ci_status(number) ⇒ Object



122
123
124
# File 'lib/aidp/watch/repository_client.rb', line 122

def fetch_ci_status(number)
  gh_available? ? fetch_ci_status_via_gh(number) : fetch_ci_status_via_api(number)
end

#fetch_comment_reactions(comment_id) ⇒ Object

Fetch reactions on a specific comment Returns array of reactions with user and content (emoji type)



178
179
180
# File 'lib/aidp/watch/repository_client.rb', line 178

def fetch_comment_reactions(comment_id)
  gh_available? ? fetch_comment_reactions_via_gh(comment_id) : fetch_comment_reactions_via_api(comment_id)
end

#fetch_issue(number) ⇒ Object



68
69
70
# File 'lib/aidp/watch/repository_client.rb', line 68

def fetch_issue(number)
  gh_available? ? fetch_issue_via_gh(number) : fetch_issue_via_api(number)
end

#fetch_pr_comments(number) ⇒ Object



147
148
149
# File 'lib/aidp/watch/repository_client.rb', line 147

def fetch_pr_comments(number)
  gh_available? ? fetch_pr_comments_via_gh(number) : fetch_pr_comments_via_api(number)
end

#fetch_project(project_id) ⇒ Object

GitHub Projects V2 operations



281
282
283
284
# File 'lib/aidp/watch/repository_client.rb', line 281

def fetch_project(project_id)
  raise "GitHub CLI not available - Projects API requires gh CLI" unless gh_available?
  fetch_project_via_gh(project_id)
end

#fetch_project_fields(project_id) ⇒ Object



301
302
303
304
# File 'lib/aidp/watch/repository_client.rb', line 301

def fetch_project_fields(project_id)
  raise "GitHub CLI not available - Projects API requires gh CLI" unless gh_available?
  fetch_project_fields_via_gh(project_id)
end

#fetch_pull_request(number) ⇒ Object

PR-specific operations



110
111
112
# File 'lib/aidp/watch/repository_client.rb', line 110

def fetch_pull_request(number)
  gh_available? ? fetch_pull_request_via_gh(number) : fetch_pull_request_via_api(number)
end

#fetch_pull_request_diff(number) ⇒ Object



114
115
116
# File 'lib/aidp/watch/repository_client.rb', line 114

def fetch_pull_request_diff(number)
  gh_available? ? fetch_pull_request_diff_via_gh(number) : fetch_pull_request_diff_via_api(number)
end

#fetch_pull_request_files(number) ⇒ Object



118
119
120
# File 'lib/aidp/watch/repository_client.rb', line 118

def fetch_pull_request_files(number)
  gh_available? ? fetch_pull_request_files_via_gh(number) : fetch_pull_request_files_via_api(number)
end

#find_active_project ⇒ Object



316
317
318
319
# File 'lib/aidp/watch/repository_client.rb', line 316

def find_active_project
  raise "GitHub CLI not available - Projects API requires gh CLI" unless gh_available?
  find_active_project_via_gh
end

#find_comment(number, header_text) ⇒ Object



76
77
78
# File 'lib/aidp/watch/repository_client.rb', line 76

def find_comment(number, header_text)
  gh_available? ? find_comment_via_gh(number, header_text) : find_comment_via_api(number, header_text)
end

#full_repo ⇒ Object



53
54
55
# File 'lib/aidp/watch/repository_client.rb', line 53

def full_repo
  "#{owner}/#{repo}"
end

#gh_available? ⇒ Boolean

Returns:

  • (Boolean)


49
50
51
# File 'lib/aidp/watch/repository_client.rb', line 49

def gh_available?
  @gh_available
end


291
292
293
294
# File 'lib/aidp/watch/repository_client.rb', line 291

def link_issue_to_project(project_id, issue_number)
  raise "GitHub CLI not available - Projects API requires gh CLI" unless gh_available?
  link_issue_to_project_via_gh(project_id, issue_number)
end

#list_issues(labels: [], state: "open") ⇒ Object



57
58
59
60
61
62
63
64
65
66
# File 'lib/aidp/watch/repository_client.rb', line 57

def list_issues(labels: [], state: "open")
  if gh_available?
    list_issues_via_gh(labels: labels,
      state: state)
  else
    list_issues_via_api(
      labels: labels, state: state
    )
  end
end

#list_project_items(project_id) ⇒ Object



286
287
288
289
# File 'lib/aidp/watch/repository_client.rb', line 286

def list_project_items(project_id)
  raise "GitHub CLI not available - Projects API requires gh CLI" unless gh_available?
  list_project_items_via_gh(project_id)
end

#list_pull_requests(labels: [], state: "open") ⇒ Object



136
137
138
139
140
141
142
143
144
145
# File 'lib/aidp/watch/repository_client.rb', line 136

def list_pull_requests(labels: [], state: "open")
  if gh_available?
    list_pull_requests_via_gh(labels: labels,
      state: state)
  else
    list_pull_requests_via_api(
      labels: labels, state: state
    )
  end
end

#mark_pr_ready_for_review(number) ⇒ Boolean

Convert a draft PR to ready for review

Parameters:

  • number (Integer) —

    PR number

Returns:

  • (Boolean) —

    True if successful



154
155
156
157
# File 'lib/aidp/watch/repository_client.rb', line 154

def mark_pr_ready_for_review(number)
  raise("GitHub CLI not available - cannot mark PR ready") unless gh_available?
  mark_pr_ready_for_review_via_gh(number)
end

#merge_pull_request(number, merge_method: "squash") ⇒ Object



340
341
342
343
# File 'lib/aidp/watch/repository_client.rb', line 340

def merge_pull_request(number, merge_method: "squash")
  raise "GitHub CLI not available - cannot merge PR" unless gh_available?
  merge_pull_request_via_gh(number, merge_method: merge_method)
end

#most_recent_label_actor(number) ⇒ Object



105
106
107
# File 'lib/aidp/watch/repository_client.rb', line 105

def most_recent_label_actor(number)
  gh_available? ? most_recent_label_actor_via_gh(number) : nil
end

#most_recent_pr_label_actor(number) ⇒ String?

Get the actor who most recently added a label to a PR

Parameters:

  • number (Integer) —

    PR number

Returns:

  • (String, nil) —

    GitHub username or nil



172
173
174
# File 'lib/aidp/watch/repository_client.rb', line 172

def most_recent_pr_label_actor(number)
  gh_available? ? most_recent_pr_label_actor_via_gh(number) : nil
end

#post_comment(number, body) ⇒ Object



72
73
74
# File 'lib/aidp/watch/repository_client.rb', line 72

def post_comment(number, body)
  gh_available? ? post_comment_via_gh(number, body) : post_comment_via_api(number, body)
end

#post_review_comment(number, body, commit_id: nil, path: nil, line: nil) ⇒ Object



126
127
128
129
130
131
132
133
134
# File 'lib/aidp/watch/repository_client.rb', line 126

def post_review_comment(number, body, commit_id: nil, path: nil, line: nil)
  if gh_available?
    post_review_comment_via_gh(number, body, commit_id: commit_id, path: path,
      line: line)
  else
    post_review_comment_via_api(number,
      body, commit_id: commit_id, path: path, line: line)
  end
end

#remove_labels(number, *labels) ⇒ Object



95
96
97
# File 'lib/aidp/watch/repository_client.rb', line 95

def remove_labels(number, *labels)
  gh_available? ? remove_labels_via_gh(number, labels.flatten) : remove_labels_via_api(number, labels.flatten)
end

#replace_labels(number, old_labels:, new_labels:) ⇒ Object



99
100
101
102
103
# File 'lib/aidp/watch/repository_client.rb', line 99

def replace_labels(number, old_labels:, new_labels:)
  # Remove old labels and add new ones atomically where possible
  remove_labels(number, *old_labels) unless old_labels.empty?
  add_labels(number, *new_labels) unless new_labels.empty?
end

#repository_node_data ⇒ Object



321
322
323
324
# File 'lib/aidp/watch/repository_client.rb', line 321

def repository_node_data
  raise "GitHub CLI not available - Projects API requires gh CLI" unless gh_available?
  repository_node_data_via_gh
end

#request_reviewers(number, reviewers:) ⇒ Boolean

Request reviewers for a PR

Parameters:

  • number (Integer) —

    PR number

  • reviewers (Array<String>) —

    GitHub usernames to request as reviewers

Returns:

  • (Boolean) —

    True if successful



163
164
165
166
167
# File 'lib/aidp/watch/repository_client.rb', line 163

def request_reviewers(number, reviewers:)
  return true if reviewers.nil? || reviewers.empty?
  raise("GitHub CLI not available - cannot request reviewers") unless gh_available?
  request_reviewers_via_gh(number, reviewers: reviewers)
end

#update_comment(comment_id, body) ⇒ Object



80
81
82
# File 'lib/aidp/watch/repository_client.rb', line 80

def update_comment(comment_id, body)
  gh_available? ? update_comment_via_gh(comment_id, body) : update_comment_via_api(comment_id, body)
end

#update_issue(number, title:, body:, labels:, assignees:) ⇒ Object



336
337
338
# File 'lib/aidp/watch/repository_client.rb', line 336

def update_issue(number, title:, body:, labels:, assignees:)
  gh_available? ? update_issue_via_gh(number, title: title, body: body, labels: labels, assignees: assignees) : update_issue_via_api(number, title: title, body: body, labels: labels, assignees: assignees)
end

#update_project_item_field(item_id, field_id, value) ⇒ Object



296
297
298
299
# File 'lib/aidp/watch/repository_client.rb', line 296

def update_project_item_field(item_id, field_id, value)
  raise "GitHub CLI not available - Projects API requires gh CLI" unless gh_available?
  update_project_item_field_via_gh(item_id, field_id, value)
end