Skip to content

Commit 3cf3e6c

Browse files
committed
Improvement to Issue Sync
1 parent 00bf065 commit 3cf3e6c

5 files changed

Lines changed: 216 additions & 95 deletions

File tree

internal/repository/item_repository.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"database/sql"
55
"encoding/json"
66
"fmt"
7+
"strings"
78
"time"
89

910
"windshift/internal/database"
@@ -336,6 +337,46 @@ func (r *ItemRepository) Update(tx database.Tx, item *models.Item) error {
336337
return nil
337338
}
338339

340+
// allowedItemColumns is the whitelist of columns that UpdateFields may touch.
341+
var allowedItemColumns = map[string]bool{
342+
"title": true, "description": true, "status_id": true, "priority_id": true,
343+
"due_date": true, "start_date": true, "end_date": true,
344+
"milestone_id": true, "iteration_id": true, "project_id": true, "inherit_project": true,
345+
"assignee_id": true, "creator_id": true, "custom_field_values": true,
346+
"parent_id": true, "related_work_item_id": true, "item_type_id": true,
347+
"frac_index": true, "is_task": true, "time_project_id": true,
348+
}
349+
350+
// UpdateFields updates only the specified columns of an item.
351+
// Keys must be valid item column names; unknown keys return an error.
352+
func (r *ItemRepository) UpdateFields(tx database.Tx, itemID int, fields map[string]interface{}) error {
353+
if len(fields) == 0 {
354+
return nil
355+
}
356+
357+
setClauses := make([]string, 0, len(fields)+1)
358+
args := make([]interface{}, 0, len(fields)+2)
359+
360+
for col, val := range fields {
361+
if !allowedItemColumns[col] {
362+
return fmt.Errorf("unknown item column: %s", col)
363+
}
364+
setClauses = append(setClauses, col+" = ?")
365+
args = append(args, val)
366+
}
367+
368+
setClauses = append(setClauses, "updated_at = ?")
369+
args = append(args, time.Now())
370+
args = append(args, itemID)
371+
372+
query := "UPDATE items SET " + strings.Join(setClauses, ", ") + " WHERE id = ?"
373+
_, err := tx.Exec(query, args...)
374+
if err != nil {
375+
return fmt.Errorf("failed to update item fields: %w", err)
376+
}
377+
return nil
378+
}
379+
339380
// Delete removes an item by ID
340381
func (r *ItemRepository) Delete(tx database.Tx, id int) error {
341382
_, err := tx.Exec("DELETE FROM items WHERE id = ?", id)

internal/scm/github.go

Lines changed: 48 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"encoding/pem"
99
"fmt"
1010
"io"
11+
"log/slog"
1112
"net/http"
1213
"net/url"
1314
"strconv"
@@ -1047,46 +1048,57 @@ func (g *GitHubProvider) ListIssueComments(ctx context.Context, owner, repo stri
10471048
return nil, err
10481049
}
10491050

1050-
reqURL := fmt.Sprintf("%s/repos/%s/%s/issues/%d/comments?per_page=100", g.baseURL, owner, repo, number)
1051+
var allComments []IssueComment
1052+
page := 1
1053+
for {
1054+
reqURL := fmt.Sprintf("%s/repos/%s/%s/issues/%d/comments?per_page=100&page=%d", g.baseURL, owner, repo, number, page)
10511055

1052-
req, err := http.NewRequestWithContext(ctx, "GET", reqURL, http.NoBody)
1053-
if err != nil {
1054-
return nil, err
1055-
}
1056-
g.setAuthHeader(req)
1056+
req, err := http.NewRequestWithContext(ctx, "GET", reqURL, http.NoBody)
1057+
if err != nil {
1058+
return nil, err
1059+
}
1060+
g.setAuthHeader(req)
10571061

1058-
resp, err := g.httpClient.Do(req)
1059-
if err != nil {
1060-
return nil, err
1061-
}
1062-
defer func() { _ = resp.Body.Close() }()
1062+
resp, err := g.httpClient.Do(req)
1063+
if err != nil {
1064+
return nil, err
1065+
}
10631066

1064-
if resp.StatusCode != http.StatusOK {
1065-
return nil, g.handleErrorResponse(resp)
1066-
}
1067+
if resp.StatusCode != http.StatusOK {
1068+
err := g.handleErrorResponse(resp)
1069+
_ = resp.Body.Close()
1070+
return nil, err
1071+
}
10671072

1068-
var ghComments []struct {
1069-
ID int64 `json:"id"`
1070-
Body string `json:"body"`
1071-
User githubUser `json:"user"`
1072-
CreatedAt time.Time `json:"created_at"`
1073-
UpdatedAt time.Time `json:"updated_at"`
1074-
}
1075-
if err := json.NewDecoder(resp.Body).Decode(&ghComments); err != nil {
1076-
return nil, err
1077-
}
1073+
var ghComments []struct {
1074+
ID int64 `json:"id"`
1075+
Body string `json:"body"`
1076+
User githubUser `json:"user"`
1077+
CreatedAt time.Time `json:"created_at"`
1078+
UpdatedAt time.Time `json:"updated_at"`
1079+
}
1080+
if err := json.NewDecoder(resp.Body).Decode(&ghComments); err != nil {
1081+
_ = resp.Body.Close()
1082+
return nil, err
1083+
}
1084+
_ = resp.Body.Close()
1085+
1086+
for _, c := range ghComments {
1087+
allComments = append(allComments, IssueComment{
1088+
ID: c.ID,
1089+
Body: c.Body,
1090+
User: c.User.toUser(),
1091+
CreatedAt: c.CreatedAt,
1092+
UpdatedAt: c.UpdatedAt,
1093+
})
1094+
}
10781095

1079-
comments := make([]IssueComment, len(ghComments))
1080-
for i, c := range ghComments {
1081-
comments[i] = IssueComment{
1082-
ID: c.ID,
1083-
Body: c.Body,
1084-
User: c.User.toUser(),
1085-
CreatedAt: c.CreatedAt,
1086-
UpdatedAt: c.UpdatedAt,
1096+
if len(ghComments) < 100 {
1097+
break
10871098
}
1099+
page++
10881100
}
1089-
return comments, nil
1101+
return allComments, nil
10901102
}
10911103

10921104
// UpdateIssueComment updates an existing comment on an issue
@@ -1328,7 +1340,9 @@ func (g *GitHubProvider) handleErrorResponse(resp *http.Response) error {
13281340
case http.StatusUnauthorized:
13291341
return ErrInvalidCredentials
13301342
case http.StatusForbidden:
1331-
if strings.Contains(bodyStr, "rate limit") {
1343+
if strings.Contains(bodyStr, "rate limit") || resp.Header.Get("X-RateLimit-Remaining") == "0" {
1344+
resetAt := resp.Header.Get("X-RateLimit-Reset")
1345+
slog.Warn("GitHub rate limit hit", "reset_at", resetAt)
13321346
return ErrRateLimited
13331347
}
13341348
if bodyStr != "" {

0 commit comments

Comments
 (0)