Class: Hiiro::Git::Pr
- Inherits:
-
Object
- Object
- Hiiro::Git::Pr
- Defined in:
- lib/hiiro/git/pr.rb
Constant Summary collapse
- PINNED_FILE =
Hiiro::Config.path('pinned_prs.yml')
- FAILED_CONCLUSIONS =
%w[FAILURE ERROR TIMED_OUT STALE STARTUP_FAILURE ACTION_REQUIRED].freeze
- STATE_FILTER_KEYS =
Filter dimensions. Flags within each group OR together; groups AND together. e.g. -o -g → (active?) AND (green?), -o -r -g → (active?) AND (red? OR green?)
%i[active merged drafts conflicts].freeze
- CHECK_FILTER_KEYS =
%i[red green pending].freeze
Instance Attribute Summary collapse
-
#assigned ⇒ Object
Returns the value of attribute assigned.
-
#authored ⇒ Object
Returns the value of attribute authored.
-
#base_branch ⇒ Object
Returns the value of attribute base_branch.
-
#check_runs ⇒ Object
Returns the value of attribute check_runs.
-
#checks ⇒ Object
Returns the value of attribute checks.
-
#depends_on ⇒ Object
Returns the value of attribute depends_on.
-
#head_branch ⇒ Object
Returns the value of attribute head_branch.
-
#is_draft ⇒ Object
Returns the value of attribute is_draft.
-
#last_checked ⇒ Object
Returns the value of attribute last_checked.
-
#mergeable ⇒ Object
Returns the value of attribute mergeable.
-
#number ⇒ Object
Returns the value of attribute number.
-
#pinned_at ⇒ Object
Returns the value of attribute pinned_at.
-
#repo ⇒ Object
Returns the value of attribute repo.
-
#review_decision ⇒ Object
Returns the value of attribute review_decision.
-
#reviews ⇒ Object
Returns the value of attribute reviews.
-
#slot ⇒ Object
Returns the value of attribute slot.
-
#state ⇒ Object
Returns the value of attribute state.
-
#tags ⇒ Object
Returns the value of attribute tags.
-
#task ⇒ Object
Returns the value of attribute task.
-
#title ⇒ Object
Returns the value of attribute title.
-
#tmux_session ⇒ Object
Returns the value of attribute tmux_session.
-
#updated_at ⇒ Object
Returns the value of attribute updated_at.
-
#url ⇒ Object
Returns the value of attribute url.
-
#worktree ⇒ Object
Returns the value of attribute worktree.
Class Method Summary collapse
- .create(title:, body: nil, base: nil, draft: false) ⇒ Object
- .current ⇒ Object
-
.from_gh_json(data) ⇒ Object
Build a Pr from GitHub API JSON (gh pr view –json or GraphQL).
- .from_link(link) ⇒ Object
- .from_number(number) ⇒ Object
-
.from_pinned_hash(hash) ⇒ Object
Build a Pr from a stored YAML hash.
- .is_link?(link) ⇒ Boolean
- .list(state: 'open', limit: 30) ⇒ Object
-
.pinned_prs ⇒ Object
Load all pinned PRs from YAML, returning an array of Pr instances.
- .repo_from_url(url) ⇒ Object
-
.summarize_checks(rollup, truncated: false) ⇒ Object
Summarizes raw statusCheckRollup contexts into { total, success, pending, failed, frozen }.
-
.summarize_reviews(reviews) ⇒ Object
Summarizes raw review nodes into { approved, changes_requested, commented, reviewers }.
Instance Method Summary collapse
-
#active? ⇒ Boolean
Aliases matching filter option names.
- #checkout ⇒ Object
- #close ⇒ Object
- #closed? ⇒ Boolean
- #conflicting? ⇒ Boolean
- #conflicts? ⇒ Boolean
- #draft? ⇒ Boolean
- #drafts? ⇒ Boolean
- #green? ⇒ Boolean
-
#initialize(number:, title: nil, state: nil, url: nil, head_branch: nil, base_branch: nil, repo: nil, slot: nil, is_draft: nil, mergeable: nil, review_decision: nil, checks: nil, check_runs: nil, reviews: nil, last_checked: nil, pinned_at: nil, updated_at: nil, task: nil, worktree: nil, tmux_session: nil, tags: nil, assigned: nil, authored: nil, depends_on: nil) ⇒ Pr
constructor
A new instance of Pr.
-
#matches_filters?(opts, forced: []) ⇒ Boolean
Returns true if this PR satisfies the filter options set in opts.
- #merge(method: nil, delete_branch: true) ⇒ Object
- #merged? ⇒ Boolean
- #open? ⇒ Boolean
- #pending? ⇒ Boolean
-
#red? ⇒ Boolean
Check-status predicates.
- #reopen ⇒ Object
- #to_h ⇒ Object
-
#to_pinned_h ⇒ Object
Serialize back to string-keyed hash for YAML storage.
- #to_s ⇒ Object
- #view ⇒ Object
Constructor Details
#initialize(number:, title: nil, state: nil, url: nil, head_branch: nil, base_branch: nil, repo: nil, slot: nil, is_draft: nil, mergeable: nil, review_decision: nil, checks: nil, check_runs: nil, reviews: nil, last_checked: nil, pinned_at: nil, updated_at: nil, task: nil, worktree: nil, tmux_session: nil, tags: nil, assigned: nil, authored: nil, depends_on: nil) ⇒ Pr
Returns a new instance of Pr.
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 |
# File 'lib/hiiro/git/pr.rb', line 192 def initialize(number:, title: nil, state: nil, url: nil, head_branch: nil, base_branch: nil, repo: nil, slot: nil, is_draft: nil, mergeable: nil, review_decision: nil, checks: nil, check_runs: nil, reviews: nil, last_checked: nil, pinned_at: nil, updated_at: nil, task: nil, worktree: nil, tmux_session: nil, tags: nil, assigned: nil, authored: nil, depends_on: nil) @number = number @title = title @state = state @url = url @head_branch = head_branch @base_branch = base_branch @repo = repo @slot = slot @is_draft = is_draft @mergeable = mergeable @review_decision = review_decision @checks = checks @check_runs = check_runs @reviews = reviews @last_checked = last_checked @pinned_at = pinned_at @updated_at = updated_at @task = task @worktree = worktree @tmux_session = tmux_session @tags = @assigned = assigned @authored = @depends_on = depends_on ? Array(depends_on).map(&:to_i) : nil end |
Instance Attribute Details
#assigned ⇒ Object
Returns the value of attribute assigned.
9 10 11 |
# File 'lib/hiiro/git/pr.rb', line 9 def assigned @assigned end |
#authored ⇒ Object
Returns the value of attribute authored.
9 10 11 |
# File 'lib/hiiro/git/pr.rb', line 9 def @authored end |
#base_branch ⇒ Object
Returns the value of attribute base_branch.
9 10 11 |
# File 'lib/hiiro/git/pr.rb', line 9 def base_branch @base_branch end |
#check_runs ⇒ Object
Returns the value of attribute check_runs.
9 10 11 |
# File 'lib/hiiro/git/pr.rb', line 9 def check_runs @check_runs end |
#checks ⇒ Object
Returns the value of attribute checks.
9 10 11 |
# File 'lib/hiiro/git/pr.rb', line 9 def checks @checks end |
#depends_on ⇒ Object
Returns the value of attribute depends_on.
9 10 11 |
# File 'lib/hiiro/git/pr.rb', line 9 def depends_on @depends_on end |
#head_branch ⇒ Object
Returns the value of attribute head_branch.
9 10 11 |
# File 'lib/hiiro/git/pr.rb', line 9 def head_branch @head_branch end |
#is_draft ⇒ Object
Returns the value of attribute is_draft.
9 10 11 |
# File 'lib/hiiro/git/pr.rb', line 9 def is_draft @is_draft end |
#last_checked ⇒ Object
Returns the value of attribute last_checked.
9 10 11 |
# File 'lib/hiiro/git/pr.rb', line 9 def last_checked @last_checked end |
#mergeable ⇒ Object
Returns the value of attribute mergeable.
9 10 11 |
# File 'lib/hiiro/git/pr.rb', line 9 def mergeable @mergeable end |
#number ⇒ Object
Returns the value of attribute number.
9 10 11 |
# File 'lib/hiiro/git/pr.rb', line 9 def number @number end |
#pinned_at ⇒ Object
Returns the value of attribute pinned_at.
9 10 11 |
# File 'lib/hiiro/git/pr.rb', line 9 def pinned_at @pinned_at end |
#repo ⇒ Object
Returns the value of attribute repo.
9 10 11 |
# File 'lib/hiiro/git/pr.rb', line 9 def repo @repo end |
#review_decision ⇒ Object
Returns the value of attribute review_decision.
9 10 11 |
# File 'lib/hiiro/git/pr.rb', line 9 def review_decision @review_decision end |
#reviews ⇒ Object
Returns the value of attribute reviews.
9 10 11 |
# File 'lib/hiiro/git/pr.rb', line 9 def reviews @reviews end |
#slot ⇒ Object
Returns the value of attribute slot.
9 10 11 |
# File 'lib/hiiro/git/pr.rb', line 9 def slot @slot end |
#state ⇒ Object
Returns the value of attribute state.
9 10 11 |
# File 'lib/hiiro/git/pr.rb', line 9 def state @state end |
#tags ⇒ Object
Returns the value of attribute tags.
9 10 11 |
# File 'lib/hiiro/git/pr.rb', line 9 def @tags end |
#task ⇒ Object
Returns the value of attribute task.
9 10 11 |
# File 'lib/hiiro/git/pr.rb', line 9 def task @task end |
#title ⇒ Object
Returns the value of attribute title.
9 10 11 |
# File 'lib/hiiro/git/pr.rb', line 9 def title @title end |
#tmux_session ⇒ Object
Returns the value of attribute tmux_session.
9 10 11 |
# File 'lib/hiiro/git/pr.rb', line 9 def tmux_session @tmux_session end |
#updated_at ⇒ Object
Returns the value of attribute updated_at.
9 10 11 |
# File 'lib/hiiro/git/pr.rb', line 9 def updated_at @updated_at end |
#url ⇒ Object
Returns the value of attribute url.
9 10 11 |
# File 'lib/hiiro/git/pr.rb', line 9 def url @url end |
#worktree ⇒ Object
Returns the value of attribute worktree.
9 10 11 |
# File 'lib/hiiro/git/pr.rb', line 9 def worktree @worktree end |
Class Method Details
.create(title:, body: nil, base: nil, draft: false) ⇒ Object
125 126 127 128 129 130 131 |
# File 'lib/hiiro/git/pr.rb', line 125 def self.create(title:, body: nil, base: nil, draft: false) args = ['gh', 'pr', 'create', '--title', title] args += ['--body', body] if body args += ['--base', base] if base args << '--draft' if draft system(*args) end |
.current ⇒ Object
182 183 184 185 186 187 188 189 190 |
# File 'lib/hiiro/git/pr.rb', line 182 def self.current output = `gh pr view --json number,title,state,url,headRefName,baseRefName 2>/dev/null` return nil if output.empty? require 'json' from_gh_json(JSON.parse(output)) rescue nil end |
.from_gh_json(data) ⇒ Object
Build a Pr from GitHub API JSON (gh pr view –json or GraphQL). Stores both the raw check run nodes (check_runs) and the summary (checks).
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/hiiro/git/pr.rb', line 93 def self.from_gh_json(data) rollup = data['statusCheckRollup'] rollup = rollup.is_a?(Array) && rollup.any? ? rollup : nil reviews = data['reviews'].is_a?(Array) ? data['reviews'] : data.dig('reviews', 'nodes') new( number: data['number'], title: data['title'], state: data['state'], url: data['url'], head_branch: data['headRefName'], base_branch: data['baseRefName'], repo: data['repo'] || repo_from_url(data['url']), is_draft: data['isDraft'], mergeable: data['mergeable'], review_decision: data['reviewDecision'], checks: rollup ? summarize_checks(rollup) : nil, check_runs: rollup, reviews: reviews ? summarize_reviews(reviews) : nil, ) end |
.from_link(link) ⇒ Object
30 31 32 33 34 35 36 37 38 39 40 |
# File 'lib/hiiro/git/pr.rb', line 30 def self.from_link(link) return nil unless is_link?(link) number = link[/pull\/(\d+)/].sub(/\D*/, '') owner, name, _ = link.sub(/.*github.com./, '').split(?/, 3) new( number: number, url: link, repo: [owner, name].join(?/), ) end |
.from_number(number) ⇒ Object
42 43 44 45 46 47 |
# File 'lib/hiiro/git/pr.rb', line 42 def self.from_number(number) number = number.to_s.strip[/^\d+$/] return if number&.length == 0 new(number: number) end |
.from_pinned_hash(hash) ⇒ Object
Build a Pr from a stored YAML hash. Handles both camelCase keys (legacy) and snake_case keys. Falls back to computing checks from raw statusCheckRollup if the summarized checks hash is missing (pre-refresh data).
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/hiiro/git/pr.rb', line 57 def self.from_pinned_hash(hash) raw_rollup = hash['statusCheckRollup'] raw_rollup = nil unless raw_rollup.is_a?(Array) && raw_rollup.any? stored_checks = hash['checks'] checks = stored_checks || (raw_rollup ? summarize_checks(raw_rollup) : nil) new( number: hash['number'], title: hash['title'], state: hash['state'], url: hash['url'], head_branch: hash['headRefName'] || hash['head_branch'], base_branch: hash['baseRefName'] || hash['base_branch'], repo: hash['repo'], slot: hash['slot'], is_draft: hash.key?('is_draft') ? hash['is_draft'] : hash['isDraft'], mergeable: hash['mergeable'], review_decision: hash['review_decision'] || hash['reviewDecision'], checks: checks, check_runs: raw_rollup, reviews: hash['reviews'], last_checked: hash['last_checked'], pinned_at: hash['pinned_at'], updated_at: hash['updated_at'], task: hash['task'], worktree: hash['worktree'], tmux_session: hash['tmux_session'], tags: hash['tags'], assigned: hash['assigned'], authored: hash['authored'], depends_on: hash['depends_on'], ) end |
.is_link?(link) ⇒ Boolean
23 24 25 26 27 28 |
# File 'lib/hiiro/git/pr.rb', line 23 def self.is_link?(link) temp_link = link.to_s return false unless temp_link.match?('github.com') && temp_link.match?(/pull\/[0-9]+/) true end |
.list(state: 'open', limit: 30) ⇒ Object
115 116 117 118 119 120 121 122 123 |
# File 'lib/hiiro/git/pr.rb', line 115 def self.list(state: 'open', limit: 30) output = `gh pr list --state #{state} --limit #{limit} --json number,title,state,url,headRefName,baseRefName 2>/dev/null` return [] if output.empty? require 'json' JSON.parse(output).map { |data| from_gh_json(data) } rescue [] end |
.pinned_prs ⇒ Object
Load all pinned PRs from YAML, returning an array of Pr instances.
15 16 17 18 19 20 21 |
# File 'lib/hiiro/git/pr.rb', line 15 def self.pinned_prs return [] unless File.exist?(PINNED_FILE) prs = YAML.load_file(PINNED_FILE) || [] prs.map { |h| from_pinned_hash(h) } rescue [] end |
.repo_from_url(url) ⇒ Object
49 50 51 52 |
# File 'lib/hiiro/git/pr.rb', line 49 def self.repo_from_url(url) return nil unless url url.match(%r{github\.com/([^/]+/[^/]+)/pull/})&.[](1) end |
.summarize_checks(rollup, truncated: false) ⇒ Object
Summarizes raw statusCheckRollup contexts into { total, success, pending, failed, frozen }. frozen = number of failed contexts that are specifically the ISC code freeze check. truncated: true is added when pagination couldn’t retrieve all checks.
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 |
# File 'lib/hiiro/git/pr.rb', line 136 def self.summarize_checks(rollup, truncated: false) return nil unless rollup contexts = rollup.is_a?(Array) ? rollup : [] return nil if contexts.empty? total = contexts.length success = contexts.count { |c| c['conclusion'] == 'SUCCESS' || c['state'] == 'SUCCESS' } pending = contexts.count do |c| %w[QUEUED IN_PROGRESS PENDING REQUESTED WAITING].include?(c['status']) || c['state'] == 'PENDING' end failed = contexts.count do |c| FAILED_CONCLUSIONS.include?(c['conclusion']) || %w[FAILURE ERROR].include?(c['state']) end frozen = contexts.count do |c| c['context'] == 'ISC code freeze' && (FAILED_CONCLUSIONS.include?(c['conclusion']) || %w[FAILURE ERROR].include?(c['state'])) end result = { 'total' => total, 'success' => success, 'pending' => pending, 'failed' => failed, 'frozen' => frozen } result['truncated'] = true if truncated result end |
.summarize_reviews(reviews) ⇒ Object
Summarizes raw review nodes into { approved, changes_requested, commented, reviewers }.
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 |
# File 'lib/hiiro/git/pr.rb', line 162 def self.summarize_reviews(reviews) return nil unless reviews.is_a?(Array) && !reviews.empty? = {} reviews.each do |review| = review.dig('author', 'login') next unless state = review['state'] next unless %w[APPROVED CHANGES_REQUESTED COMMENTED].include?(state) [] = state end approved = .values.count { |s| s == 'APPROVED' } changes_requested = .values.count { |s| s == 'CHANGES_REQUESTED' } commented = .values.count { |s| s == 'COMMENTED' } { 'approved' => approved, 'changes_requested' => changes_requested, 'commented' => commented, 'reviewers' => } end |
Instance Method Details
#active? ⇒ Boolean
Aliases matching filter option names
236 |
# File 'lib/hiiro/git/pr.rb', line 236 def active? = !merged? && !closed? |
#checkout ⇒ Object
258 |
# File 'lib/hiiro/git/pr.rb', line 258 def checkout = system('gh', 'pr', 'checkout', number.to_s) |
#close ⇒ Object
267 |
# File 'lib/hiiro/git/pr.rb', line 267 def close = system('gh', 'pr', 'close', number.to_s) |
#closed? ⇒ Boolean
225 |
# File 'lib/hiiro/git/pr.rb', line 225 def closed? = state&.upcase == 'CLOSED' |
#conflicting? ⇒ Boolean
228 |
# File 'lib/hiiro/git/pr.rb', line 228 def conflicting? = mergeable == 'CONFLICTING' |
#conflicts? ⇒ Boolean
238 |
# File 'lib/hiiro/git/pr.rb', line 238 def conflicts? = conflicting? |
#draft? ⇒ Boolean
227 |
# File 'lib/hiiro/git/pr.rb', line 227 def draft? = is_draft == true |
#drafts? ⇒ Boolean
237 |
# File 'lib/hiiro/git/pr.rb', line 237 def drafts? = draft? |
#green? ⇒ Boolean
232 |
# File 'lib/hiiro/git/pr.rb', line 232 def green? = (c = checks) && c['failed'].to_i == 0 && c['pending'].to_i == 0 && c['success'].to_i > 0 |
#matches_filters?(opts, forced: []) ⇒ Boolean
Returns true if this PR satisfies the filter options set in opts. forced: injects additional filter keys as if the user had set them.
247 248 249 250 251 252 253 254 255 |
# File 'lib/hiiro/git/pr.rb', line 247 def matches_filters?(opts, forced: []) state_active = STATE_FILTER_KEYS.select { |k| forced.include?(k) || (opts.respond_to?(k) && opts.send(k)) } check_active = CHECK_FILTER_KEYS.select { |k| forced.include?(k) || (opts.respond_to?(k) && opts.send(k)) } state_match = state_active.empty? || state_active.any? { |k| send(:"#{k}?") } check_match = check_active.empty? || check_active.any? { |k| send(:"#{k}?") } state_match && check_match end |
#merge(method: nil, delete_branch: true) ⇒ Object
260 261 262 263 264 265 |
# File 'lib/hiiro/git/pr.rb', line 260 def merge(method: nil, delete_branch: true) args = ['gh', 'pr', 'merge', number.to_s] args << "--#{method}" if method args << '--delete-branch' if delete_branch system(*args) end |
#merged? ⇒ Boolean
226 |
# File 'lib/hiiro/git/pr.rb', line 226 def merged? = state&.upcase == 'MERGED' |
#open? ⇒ Boolean
224 |
# File 'lib/hiiro/git/pr.rb', line 224 def open? = state&.upcase == 'OPEN' |
#pending? ⇒ Boolean
233 |
# File 'lib/hiiro/git/pr.rb', line 233 def pending? = (c = checks) && c['pending'].to_i > 0 && c['failed'].to_i == 0 |
#red? ⇒ Boolean
Check-status predicates
231 |
# File 'lib/hiiro/git/pr.rb', line 231 def red? = (c = checks) && c['failed'].to_i > 0 |
#reopen ⇒ Object
268 |
# File 'lib/hiiro/git/pr.rb', line 268 def reopen = system('gh', 'pr', 'reopen', number.to_s) |
#to_h ⇒ Object
301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 |
# File 'lib/hiiro/git/pr.rb', line 301 def to_h { number: number, title: title, state: state, url: url, head_branch: head_branch, base_branch: base_branch, repo: repo, slot: slot, is_draft: is_draft, mergeable: mergeable, review_decision: review_decision, checks: checks, reviews: reviews, last_checked: last_checked, pinned_at: pinned_at, updated_at: updated_at, task: task, worktree: worktree, tmux_session: tmux_session, tags: , assigned: assigned, authored: , }.compact end |
#to_pinned_h ⇒ Object
Serialize back to string-keyed hash for YAML storage.
271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 |
# File 'lib/hiiro/git/pr.rb', line 271 def to_pinned_h { 'number' => number, 'title' => title, 'state' => state, 'url' => url, 'headRefName' => head_branch, 'repo' => repo, 'slot' => slot, 'is_draft' => is_draft, 'mergeable' => mergeable, 'review_decision' => review_decision, 'checks' => checks, 'statusCheckRollup' => check_runs, 'reviews' => reviews, 'last_checked' => last_checked, 'pinned_at' => pinned_at, 'updated_at' => updated_at, 'task' => task, 'worktree' => worktree, 'tmux_session' => tmux_session, 'tags' => (Array().empty? ? nil : ), 'assigned' => assigned, 'authored' => , 'depends_on' => (Array(depends_on).empty? ? nil : depends_on), }.compact end |
#to_s ⇒ Object
299 |
# File 'lib/hiiro/git/pr.rb', line 299 def to_s = "##{number}: #{title}" |
#view ⇒ Object
257 |
# File 'lib/hiiro/git/pr.rb', line 257 def view = system('gh', 'pr', 'view', number.to_s) |