fix(import): preserve comment created_at timestamps during import (#735)
Comment timestamps were being overwritten with CURRENT_TIMESTAMP during import, causing infinite sync loops between hosts as each import would update timestamps. Added ImportIssueComment() method that accepts and preserves the original timestamp from JSONL, and updated importComments() to use it. Closes #735
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/steveyegge/beads/internal/config"
|
||||
"github.com/steveyegge/beads/internal/linear"
|
||||
@@ -892,7 +893,10 @@ func importComments(ctx context.Context, sqliteStore *sqlite.SQLiteStorage, issu
|
||||
for _, comment := range issue.Comments {
|
||||
key := fmt.Sprintf("%s:%s", comment.Author, strings.TrimSpace(comment.Text))
|
||||
if !existingComments[key] {
|
||||
if _, err := sqliteStore.AddIssueComment(ctx, issue.ID, comment.Author, comment.Text); err != nil {
|
||||
// Use ImportIssueComment to preserve original timestamp (GH#735)
|
||||
// Format timestamp as RFC3339 for SQLite compatibility
|
||||
createdAt := comment.CreatedAt.UTC().Format(time.RFC3339)
|
||||
if _, err := sqliteStore.ImportIssueComment(ctx, issue.ID, comment.Author, comment.Text, createdAt); err != nil {
|
||||
if opts.Strict {
|
||||
return fmt.Errorf("error adding comment to %s: %w", issue.ID, err)
|
||||
}
|
||||
|
||||
@@ -52,6 +52,54 @@ func (s *SQLiteStorage) AddIssueComment(ctx context.Context, issueID, author, te
|
||||
return comment, nil
|
||||
}
|
||||
|
||||
// ImportIssueComment adds a comment during import, preserving the original timestamp.
|
||||
// Unlike AddIssueComment which uses CURRENT_TIMESTAMP, this method uses the provided
|
||||
// createdAt time from the JSONL file. This prevents timestamp drift during sync cycles.
|
||||
// GH#735: Comment created_at timestamps were being overwritten with current time during import.
|
||||
func (s *SQLiteStorage) ImportIssueComment(ctx context.Context, issueID, author, text string, createdAt string) (*types.Comment, error) {
|
||||
// Verify issue exists
|
||||
var exists bool
|
||||
err := s.db.QueryRowContext(ctx, `SELECT EXISTS(SELECT 1 FROM issues WHERE id = ?)`, issueID).Scan(&exists)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check issue existence: %w", err)
|
||||
}
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("issue %s not found", issueID)
|
||||
}
|
||||
|
||||
// Insert comment with provided timestamp
|
||||
result, err := s.db.ExecContext(ctx, `
|
||||
INSERT INTO comments (issue_id, author, text, created_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`, issueID, author, text, createdAt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to insert comment: %w", err)
|
||||
}
|
||||
|
||||
// Get the inserted comment ID
|
||||
commentID, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get comment ID: %w", err)
|
||||
}
|
||||
|
||||
// Fetch the complete comment
|
||||
comment := &types.Comment{}
|
||||
err = s.db.QueryRowContext(ctx, `
|
||||
SELECT id, issue_id, author, text, created_at
|
||||
FROM comments WHERE id = ?
|
||||
`, commentID).Scan(&comment.ID, &comment.IssueID, &comment.Author, &comment.Text, &comment.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch comment: %w", err)
|
||||
}
|
||||
|
||||
// Mark issue as dirty for JSONL export
|
||||
if err := s.MarkIssueDirty(ctx, issueID); err != nil {
|
||||
return nil, fmt.Errorf("failed to mark issue dirty: %w", err)
|
||||
}
|
||||
|
||||
return comment, nil
|
||||
}
|
||||
|
||||
// GetIssueComments retrieves all comments for an issue
|
||||
func (s *SQLiteStorage) GetIssueComments(ctx context.Context, issueID string) ([]*types.Comment, error) {
|
||||
rows, err := s.db.QueryContext(ctx, `
|
||||
|
||||
Reference in New Issue
Block a user