Module: PlanMyStuff::IssueExtractions::Links

Included in:
PlanMyStuff::Issue
Defined in:
lib/plan_my_stuff/issue_extractions/links.rb

Instance Method Summary collapse

Instance Method Details

#add_blocker!(target) ⇒ PlanMyStuff::Link

Records that target blocks self. Native GitHub action; notifications are handled by GitHub itself.

Parameters:

Returns:



157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/plan_my_stuff/issue_extractions/links.rb', line 157

def add_blocker!(target)
  link = build_link!(target, type: :blocked_by)
  validate_not_self!(link)

  target_issue = resolve_target_issue(target, type: :blocked_by)
  PlanMyStuff.client.rest(
    :post,
    dependency_path('blocked_by'),
    { issue_id: target_issue.__send__(:require_github_id!) },
  )
  invalidate_links_cache!
  link
end

#add_related!(target, user: nil, reciprocal: false) ⇒ PlanMyStuff::Link

Adds a :related link to target and, unless this call is already a reciprocal, mirrors the link back on target so the pairing is symmetric. Dedups on (type, issue_number, repo) - re-adding is a no-op.

Parameters:

  • target (PlanMyStuff::Issue, PlanMyStuff::Link, Hash)
  • user (Object, nil) (defaults to: nil)

    actor for notification events

  • reciprocal (Boolean) (defaults to: false)

    internal flag; set by the mirror call

Returns:



24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/plan_my_stuff/issue_extractions/links.rb', line 24

def add_related!(target, user: nil, reciprocal: false)
  link = build_link!(target, type: :related)
  validate_not_self!(link)

  existing = current_links
  return link if existing.include?(link)

  persist_links!(existing + [link])
  unless reciprocal
    mirror_on_target(link, user: user) { |other| other.add_related!(self, user: user, reciprocal: true) }
  end

  link
end

#add_sub_issue!(target) ⇒ PlanMyStuff::Link

Adds target as a sub-issue of self via POST /issues/{number}/sub_issues. Native GitHub action; notifications are handled by GitHub itself.

Parameters:

Returns:



88
89
90
# File 'lib/plan_my_stuff/issue_extractions/links.rb', line 88

def add_sub_issue!(target)
  mutate_sub_issue!(target, method: :post, path: sub_issues_path)
end

#blocked_byArray<PlanMyStuff::Issue>

Lazy-memoized issues that block self (i.e. self is blocked by each returned issue) via GitHub’s native issue-dependency REST API.

Returns:



139
140
141
# File 'lib/plan_my_stuff/issue_extractions/links.rb', line 139

def blocked_by
  links_cache[:blocked_by] ||= fetch_dependencies('blocked_by')
end

#blockingArray<PlanMyStuff::Issue>

Lazy-memoized issues that self blocks.

Returns:



147
148
149
# File 'lib/plan_my_stuff/issue_extractions/links.rb', line 147

def blocking
  links_cache[:blocking] ||= fetch_dependencies('blocking')
end

#duplicate_ofPlanMyStuff::Issue?

Lazy-memoized issue that self was marked as duplicate of, via GitHub’s native close-as-duplicate. Returns nil for issues that are open or closed for other reasons.

Returns:



195
196
197
198
199
# File 'lib/plan_my_stuff/issue_extractions/links.rb', line 195

def duplicate_of
  return links_cache[:duplicate_of] if links_cache.key?(:duplicate_of)

  links_cache[:duplicate_of] = fetch_duplicate_of
end

#mark_duplicate!(target, user: nil) ⇒ PlanMyStuff::Link

Closes self as a duplicate of target via GitHub’s native close-as-duplicate, carrying over viewers, assignees, and a back-pointer comment on the target.

Side effects, in order:

  1. Resolves target; raises ValidationError if missing.

  2. Raises ValidationError when self is already closed.

  3. Merges self’s visibility_allowlist onto target.

  4. Merges self’s assignees onto target.

  5. Posts a PMS comment on target with the back-pointer.

  6. Closes self with state_reason: :duplicate and duplicate_of: { owner:, repo:, number: }.

  7. Reloads self; invalidates link caches.

  8. Fires plan_my_stuff.issue.marked_duplicate.

Partial failures are not rolled back - GitHub retains whatever side effects succeeded before the failing step.

Parameters:

Returns:

Raises:



224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/plan_my_stuff/issue_extractions/links.rb', line 224

def mark_duplicate!(target, user: nil)
  raise(PlanMyStuff::ValidationError, 'Cannot mark a closed issue as duplicate') if state == 'closed'

  target_issue = resolve_duplicate_target!(target)
  merge_visibility_allowlist_onto!(target_issue)
  merge_assignees_onto!(target_issue)
  post_duplicate_back_pointer!(target_issue, user: user)
  close_as_duplicate!(target_issue)

  reload
  invalidate_links_cache!
  PlanMyStuff::Notifications.instrument('issue.marked_duplicate', self, target: target_issue, user: user)

  build_link!(target_issue, type: :duplicate_of)
end

#parentPlanMyStuff::Issue?

Lazy-memoized parent issue via GitHub’s native sub-issues API. GitHub enforces at most one parent per issue.

Returns:



67
68
69
70
71
# File 'lib/plan_my_stuff/issue_extractions/links.rb', line 67

def parent
  return links_cache[:parent] if links_cache.key?(:parent)

  links_cache[:parent] = fetch_parent
end

Lazy-memoized array of Issue objects for :related links. Silently drops targets that 404 so a dangling pointer doesn’t break the rest of the list.

Returns:



11
12
13
# File 'lib/plan_my_stuff/issue_extractions/links.rb', line 11

def related
  links_cache[:related] ||= fetch_related
end

#remove_blocker!(target) ⇒ PlanMyStuff::Link

Removes the record that target blocks self.

Parameters:

Returns:



177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/plan_my_stuff/issue_extractions/links.rb', line 177

def remove_blocker!(target)
  link = build_link!(target, type: :blocked_by)
  validate_not_self!(link)

  target_issue = resolve_target_issue(target, type: :blocked_by)
  PlanMyStuff.client.rest(
    :delete,
    "#{dependency_path('blocked_by')}/#{target_issue.__send__(:require_github_id!)}",
  )
  invalidate_links_cache!
  link
end

#remove_parent!PlanMyStuff::Link?

Detaches self from its current parent, if any. Returns the Link that was removed, or nil when there was no parent.

Returns:



124
125
126
127
128
129
130
131
132
# File 'lib/plan_my_stuff/issue_extractions/links.rb', line 124

def remove_parent!
  current = parent
  return if current.nil?

  current.remove_sub_issue!(self)
  invalidate_links_cache!

  build_link!(current, type: :parent)
end

#remove_related!(target, user: nil, reciprocal: false) ⇒ PlanMyStuff::Link

Removes a :related link to target and, unless this call is already a reciprocal, mirrors the removal on target. No-op when the link isn’t present locally.

Parameters:

Returns:



48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/plan_my_stuff/issue_extractions/links.rb', line 48

def remove_related!(target, user: nil, reciprocal: false)
  link = build_link!(target, type: :related)
  validate_not_self!(link)

  existing = current_links
  return link if existing.exclude?(link)

  persist_links!(existing.reject { |l| l == link })
  unless reciprocal
    mirror_on_target(link, user: user) { |other| other.remove_related!(self, user: user, reciprocal: true) }
  end

  link
end

#remove_sub_issue!(target) ⇒ PlanMyStuff::Link

Removes target as a sub-issue of self via DELETE /issues/{number}/sub_issue (singular).

Parameters:

Returns:



98
99
100
# File 'lib/plan_my_stuff/issue_extractions/links.rb', line 98

def remove_sub_issue!(target)
  mutate_sub_issue!(target, method: :delete, path: remove_sub_issue_path)
end

#set_parent!(target) ⇒ PlanMyStuff::Link

Makes target the parent of self. If self already has a parent, it is detached first. Returns a Link describing the new :parent relationship.

Parameters:

Returns:



109
110
111
112
113
114
115
116
117
# File 'lib/plan_my_stuff/issue_extractions/links.rb', line 109

def set_parent!(target)
  parent.presence&.remove_sub_issue!(self)

  target_issue = resolve_target_issue(target, type: :parent)
  target_issue.add_sub_issue!(self)
  invalidate_links_cache!

  build_link!(target_issue, type: :parent)
end

#sub_ticketsArray<PlanMyStuff::Issue>

Lazy-memoized sub-issues via GitHub’s native sub-issues API.

Returns:



77
78
79
# File 'lib/plan_my_stuff/issue_extractions/links.rb', line 77

def sub_tickets
  links_cache[:sub_tickets] ||= fetch_sub_tickets
end