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.



37
38
39
40
41
42
# File 'lib/aidp/watch/repository_client.rb', line 37

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

#ownerObject (readonly)

Returns the value of attribute owner.



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

def owner
  @owner
end

#repoObject (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



26
27
28
29
30
31
32
33
34
35
# File 'lib/aidp/watch/repository_client.rb', line 26

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



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

def add_labels(number, *labels)
  gh_available? ? add_labels_via_gh(number, labels.flatten) : add_labels_via_api(number, labels.flatten)
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.



180
181
182
183
184
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
# File 'lib/aidp/watch/repository_client.rb', line 180

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



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

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_field(project_id, name, field_type, options: nil) ⇒ Object



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

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



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

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



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

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)



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

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



63
64
65
# File 'lib/aidp/watch/repository_client.rb', line 63

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

#fetch_pr_comments(number) ⇒ Object



142
143
144
# File 'lib/aidp/watch/repository_client.rb', line 142

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



276
277
278
279
# File 'lib/aidp/watch/repository_client.rb', line 276

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



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

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



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

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



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

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



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

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

#find_comment(number, header_text) ⇒ Object



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

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

#full_repoObject



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

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

#gh_available?Boolean

Returns:

  • (Boolean)


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

def gh_available?
  @gh_available
end


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

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



52
53
54
55
56
57
58
59
60
61
# File 'lib/aidp/watch/repository_client.rb', line 52

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



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

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



131
132
133
134
135
136
137
138
139
140
# File 'lib/aidp/watch/repository_client.rb', line 131

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



149
150
151
152
# File 'lib/aidp/watch/repository_client.rb', line 149

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



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

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



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

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



167
168
169
# File 'lib/aidp/watch/repository_client.rb', line 167

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

#post_comment(number, body) ⇒ Object



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

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



121
122
123
124
125
126
127
128
129
# File 'lib/aidp/watch/repository_client.rb', line 121

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



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

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



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

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

#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



158
159
160
161
162
# File 'lib/aidp/watch/repository_client.rb', line 158

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



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

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

#update_project_item_field(item_id, field_id, value) ⇒ Object



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

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