Add compacted_at_commit field and git commit capture during compaction

- Add compacted_at_commit field to Issue type (bd-405)
- Add database schema and migration for new field
- Create GetCurrentCommitHash() helper function
- Update ApplyCompaction to store git commit hash (bd-395)
- Update compaction calls to capture current commit
- Update tests to verify commit hash storage
- All tests passing

Amp-Thread-ID: https://ampcode.com/threads/T-5518cccb-7fc9-4dcd-ba5a-e22cd10e45d7
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Steve Yegge
2025-10-16 17:43:27 -07:00
parent b17fcdbb2a
commit 65f59e6b01
8 changed files with 81 additions and 12 deletions

View File

@@ -279,8 +279,8 @@ func (s *SQLiteStorage) CheckEligibility(ctx context.Context, issueID string, ti
}
// ApplyCompaction updates the compaction metadata for an issue after successfully compacting it.
// This sets compaction_level, compacted_at, and original_size fields.
func (s *SQLiteStorage) ApplyCompaction(ctx context.Context, issueID string, level int, originalSize int, compressedSize int) error {
// This sets compaction_level, compacted_at, compacted_at_commit, and original_size fields.
func (s *SQLiteStorage) ApplyCompaction(ctx context.Context, issueID string, level int, originalSize int, compressedSize int, commitHash string) error {
now := time.Now().UTC()
tx, err := s.db.BeginTx(ctx, nil)
@@ -289,14 +289,20 @@ func (s *SQLiteStorage) ApplyCompaction(ctx context.Context, issueID string, lev
}
defer tx.Rollback()
var commitHashPtr *string
if commitHash != "" {
commitHashPtr = &commitHash
}
_, err = tx.ExecContext(ctx, `
UPDATE issues
SET compaction_level = ?,
compacted_at = ?,
compacted_at_commit = ?,
original_size = ?,
updated_at = ?
WHERE id = ?
`, level, now, originalSize, now, issueID)
`, level, now, commitHashPtr, originalSize, now, issueID)
if err != nil {
return fmt.Errorf("failed to apply compaction metadata: %w", err)

View File

@@ -333,18 +333,19 @@ func TestApplyCompaction(t *testing.T) {
}
originalSize := len(issue.Description)
err := store.ApplyCompaction(ctx, issue.ID, 1, originalSize, 500)
err := store.ApplyCompaction(ctx, issue.ID, 1, originalSize, 500, "abc123")
if err != nil {
t.Fatalf("ApplyCompaction failed: %v", err)
}
var compactionLevel int
var compactedAt sql.NullTime
var compactedAtCommit sql.NullString
var storedSize int
err = store.db.QueryRowContext(ctx, `
SELECT COALESCE(compaction_level, 0), compacted_at, COALESCE(original_size, 0)
SELECT COALESCE(compaction_level, 0), compacted_at, compacted_at_commit, COALESCE(original_size, 0)
FROM issues WHERE id = ?
`, issue.ID).Scan(&compactionLevel, &compactedAt, &storedSize)
`, issue.ID).Scan(&compactionLevel, &compactedAt, &compactedAtCommit, &storedSize)
if err != nil {
t.Fatalf("Failed to query issue: %v", err)
}
@@ -355,6 +356,9 @@ func TestApplyCompaction(t *testing.T) {
if !compactedAt.Valid {
t.Error("Expected compacted_at to be set")
}
if !compactedAtCommit.Valid || compactedAtCommit.String != "abc123" {
t.Errorf("Expected compacted_at_commit 'abc123', got %v", compactedAtCommit)
}
if storedSize != originalSize {
t.Errorf("Expected original_size %d, got %d", originalSize, storedSize)
}

View File

@@ -20,6 +20,7 @@ CREATE TABLE IF NOT EXISTS issues (
external_ref TEXT,
compaction_level INTEGER DEFAULT 0,
compacted_at DATETIME,
compacted_at_commit TEXT,
original_size INTEGER,
CHECK ((status = 'closed') = (closed_at IS NOT NULL))
);

View File

@@ -87,6 +87,11 @@ func New(path string) (*SQLiteStorage, error) {
return nil, fmt.Errorf("failed to migrate compaction config: %w", err)
}
// Migrate existing databases to add compacted_at_commit column
if err := migrateCompactedAtCommitColumn(db); err != nil {
return nil, fmt.Errorf("failed to migrate compacted_at_commit column: %w", err)
}
return &SQLiteStorage{
db: db,
}, nil
@@ -401,6 +406,31 @@ func migrateCompactionConfig(db *sql.DB) error {
return nil
}
// migrateCompactedAtCommitColumn adds compacted_at_commit column to the issues table.
// This migration is idempotent and safe to run multiple times.
func migrateCompactedAtCommitColumn(db *sql.DB) error {
var columnExists bool
err := db.QueryRow(`
SELECT COUNT(*) > 0
FROM pragma_table_info('issues')
WHERE name = 'compacted_at_commit'
`).Scan(&columnExists)
if err != nil {
return fmt.Errorf("failed to check compacted_at_commit column: %w", err)
}
if columnExists {
return nil
}
_, err = db.Exec(`ALTER TABLE issues ADD COLUMN compacted_at_commit TEXT`)
if err != nil {
return fmt.Errorf("failed to add compacted_at_commit column: %w", err)
}
return nil
}
// getNextIDForPrefix atomically generates the next ID for a given prefix
// Uses the issue_counters table for atomic, cross-process ID generation
func (s *SQLiteStorage) getNextIDForPrefix(ctx context.Context, prefix string) (int, error) {
@@ -846,11 +876,12 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue,
var compactedAt sql.NullTime
var originalSize sql.NullInt64
var compactedAtCommit sql.NullString
err := s.db.QueryRowContext(ctx, `
SELECT id, title, description, design, acceptance_criteria, notes,
status, priority, issue_type, assignee, estimated_minutes,
created_at, updated_at, closed_at, external_ref,
compaction_level, compacted_at, original_size
compaction_level, compacted_at, compacted_at_commit, original_size
FROM issues
WHERE id = ?
`, id).Scan(
@@ -858,7 +889,7 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue,
&issue.AcceptanceCriteria, &issue.Notes, &issue.Status,
&issue.Priority, &issue.IssueType, &assignee, &estimatedMinutes,
&issue.CreatedAt, &issue.UpdatedAt, &closedAt, &externalRef,
&issue.CompactionLevel, &compactedAt, &originalSize,
&issue.CompactionLevel, &compactedAt, &compactedAtCommit, &originalSize,
)
if err == sql.ErrNoRows {
@@ -884,6 +915,9 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue,
if compactedAt.Valid {
issue.CompactedAt = &compactedAt.Time
}
if compactedAtCommit.Valid {
issue.CompactedAtCommit = &compactedAtCommit.String
}
if originalSize.Valid {
issue.OriginalSize = int(originalSize.Int64)
}