tenseleyflow/shithub / da1eb54

Browse files

issues: add Edit orchestrator for title/body updates with markdown re-render

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
da1eb54527deca6c4599abf069031ef9a46e8de0
Parents
d346354
Tree
24af05c

1 changed file

StatusFile+-
M internal/issues/issues.go 67 0
internal/issues/issues.gomodified
@@ -254,6 +254,73 @@ func AddComment(ctx context.Context, deps Deps, p CommentCreateParams) (issuesdb
254254
 	return c, nil
255255
 }
256256
 
257
+// EditParams describes a title/body update applied to an existing
258
+// issue. Both fields are optional pointers: nil means "leave as-is".
259
+type EditParams struct {
260
+	IssueID int64
261
+	Title   *string
262
+	Body    *string
263
+}
264
+
265
+// Edit updates an issue's title and/or body, re-rendering the cached
266
+// markdown body and re-indexing cross-references. Pure plumbing: the
267
+// caller is responsible for the policy check before invoking.
268
+func Edit(ctx context.Context, deps Deps, p EditParams) (issuesdb.Issue, error) {
269
+	q := issuesdb.New()
270
+	tx, err := deps.Pool.Begin(ctx)
271
+	if err != nil {
272
+		return issuesdb.Issue{}, fmt.Errorf("begin: %w", err)
273
+	}
274
+	committed := false
275
+	defer func() {
276
+		if !committed {
277
+			_ = tx.Rollback(ctx)
278
+		}
279
+	}()
280
+	cur, err := q.GetIssueByID(ctx, tx, p.IssueID)
281
+	if err != nil {
282
+		return issuesdb.Issue{}, err
283
+	}
284
+	title := cur.Title
285
+	if p.Title != nil {
286
+		title = strings.TrimSpace(*p.Title)
287
+		if title == "" {
288
+			return issuesdb.Issue{}, ErrEmptyTitle
289
+		}
290
+		if len(title) > 256 {
291
+			return issuesdb.Issue{}, ErrTitleTooLong
292
+		}
293
+	}
294
+	body := cur.Body
295
+	if p.Body != nil {
296
+		body = *p.Body
297
+		if len(body) > 65535 {
298
+			return issuesdb.Issue{}, ErrBodyTooLong
299
+		}
300
+	}
301
+	html, _ := renderBody(ctx, deps, body)
302
+	if err := q.UpdateIssueTitleBody(ctx, tx, issuesdb.UpdateIssueTitleBodyParams{
303
+		ID: p.IssueID, Title: title, Body: body,
304
+		BodyHtmlCached: pgtype.Text{String: html, Valid: html != ""},
305
+	}); err != nil {
306
+		return issuesdb.Issue{}, fmt.Errorf("update: %w", err)
307
+	}
308
+	// Re-index cross-references when the body changed; new refs land,
309
+	// stale ones are no longer indexed (existing rows are left alone,
310
+	// matching how GitHub leaves prior references in place).
311
+	if p.Body != nil {
312
+		fresh, _ := q.GetIssueByID(ctx, tx, p.IssueID)
313
+		if err := insertReferencesFromBody(ctx, tx, deps, fresh, body, "issue_body", fresh.ID); err != nil {
314
+			return issuesdb.Issue{}, fmt.Errorf("refs: %w", err)
315
+		}
316
+	}
317
+	if err := tx.Commit(ctx); err != nil {
318
+		return issuesdb.Issue{}, fmt.Errorf("commit: %w", err)
319
+	}
320
+	committed = true
321
+	return q.GetIssueByID(ctx, deps.Pool, p.IssueID)
322
+}
323
+
257324
 // SetState closes or reopens an issue and emits an `closed` /
258325
 // `reopened` event.
259326
 func SetState(ctx context.Context, deps Deps, actorUserID, issueID int64, newState, reason string) error {