Assign readable ticket keys from project code and sequence.
Replace UUID-looking sync labels in TUI and web with keys like TAS-1, while keeping the remote UUID for task-master matching. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+100
-14
@@ -24,9 +24,11 @@ func scanTicket(scanner interface{ Scan(dest ...any) error }) (ticket.Ticket, er
|
||||
var t ticket.Ticket
|
||||
var created, updated, synced string
|
||||
var status string
|
||||
var number sql.NullInt64
|
||||
err := scanner.Scan(
|
||||
&t.ID, &t.Title, &t.Description, &status, &t.ProjectID,
|
||||
&created, &updated, &t.Source, &t.ExternalID, &t.ExternalURL, &synced,
|
||||
&number, &t.Key,
|
||||
)
|
||||
if err != nil {
|
||||
return ticket.Ticket{}, err
|
||||
@@ -35,10 +37,13 @@ func scanTicket(scanner interface{ Scan(dest ...any) error }) (ticket.Ticket, er
|
||||
t.CreatedAt = parseTime(created)
|
||||
t.UpdatedAt = parseTime(updated)
|
||||
t.SyncedAt = parseTime(synced)
|
||||
if number.Valid {
|
||||
t.Number = int(number.Int64)
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
const ticketCols = `id, title, description, status, project_id, created_at, updated_at, source, external_id, external_url, synced_at`
|
||||
const ticketCols = `id, title, description, status, project_id, created_at, updated_at, source, external_id, external_url, synced_at, number, key`
|
||||
|
||||
// List returns tickets for a project.
|
||||
func (s *TicketStore) List(projectID string) ([]ticket.Ticket, error) {
|
||||
@@ -74,7 +79,7 @@ func (s *TicketStore) Get(id string) (ticket.Ticket, error) {
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// Create inserts a ticket.
|
||||
// Create inserts a ticket and assigns the next project key atomically.
|
||||
func (s *TicketStore) Create(t ticket.Ticket) (ticket.Ticket, error) {
|
||||
if t.ProjectID == "" {
|
||||
return ticket.Ticket{}, fmt.Errorf("projectId is required")
|
||||
@@ -95,24 +100,63 @@ func (s *TicketStore) Create(t ticket.Ticket) (ticket.Ticket, error) {
|
||||
if t.UpdatedAt.IsZero() {
|
||||
t.UpdatedAt = now
|
||||
}
|
||||
_, err := s.db.SQL.Exec(`
|
||||
INSERT INTO tickets(`+ticketCols+`) VALUES(?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
|
||||
tx, err := s.db.SQL.Begin()
|
||||
if err != nil {
|
||||
return ticket.Ticket{}, err
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
if t.Number == 0 || t.Key == "" {
|
||||
res, err := tx.Exec(`UPDATE projects SET ticket_seq = ticket_seq + 1 WHERE id = ?`, t.ProjectID)
|
||||
if err != nil {
|
||||
return ticket.Ticket{}, fmt.Errorf("create ticket: bump seq: %w", err)
|
||||
}
|
||||
n, _ := res.RowsAffected()
|
||||
if n == 0 {
|
||||
return ticket.Ticket{}, fmt.Errorf("create ticket: project %s not found", t.ProjectID)
|
||||
}
|
||||
var code string
|
||||
var seq int
|
||||
if err := tx.QueryRow(`SELECT code, ticket_seq FROM projects WHERE id = ?`, t.ProjectID).Scan(&code, &seq); err != nil {
|
||||
return ticket.Ticket{}, fmt.Errorf("create ticket: read seq: %w", err)
|
||||
}
|
||||
if code == "" {
|
||||
return ticket.Ticket{}, fmt.Errorf("create ticket: project %s has no code", t.ProjectID)
|
||||
}
|
||||
t.Number = seq
|
||||
t.Key = fmt.Sprintf("%s-%d", code, seq)
|
||||
}
|
||||
|
||||
var number any
|
||||
if t.Number > 0 {
|
||||
number = t.Number
|
||||
}
|
||||
_, err = tx.Exec(`
|
||||
INSERT INTO tickets(`+ticketCols+`) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
t.ID, t.Title, t.Description, string(t.Status), t.ProjectID,
|
||||
formatTime(t.CreatedAt), formatTime(t.UpdatedAt), t.Source, t.ExternalID, t.ExternalURL, formatTime(t.SyncedAt),
|
||||
number, t.Key,
|
||||
)
|
||||
if err != nil {
|
||||
return ticket.Ticket{}, fmt.Errorf("create ticket: %w", err)
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return ticket.Ticket{}, err
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// Update replaces a ticket and bumps UpdatedAt.
|
||||
// Update replaces a ticket and bumps UpdatedAt. Number and Key are preserved from DB.
|
||||
func (s *TicketStore) Update(t ticket.Ticket) error {
|
||||
if t.ID == "" || t.ProjectID == "" {
|
||||
return fmt.Errorf("id and projectId are required")
|
||||
}
|
||||
var created string
|
||||
err := s.db.SQL.QueryRow(`SELECT created_at FROM tickets WHERE id = ? AND project_id = ?`, t.ID, t.ProjectID).Scan(&created)
|
||||
var number sql.NullInt64
|
||||
var key string
|
||||
err := s.db.SQL.QueryRow(`SELECT created_at, number, key FROM tickets WHERE id = ? AND project_id = ?`, t.ID, t.ProjectID).
|
||||
Scan(&created, &number, &key)
|
||||
if err == sql.ErrNoRows {
|
||||
return fmt.Errorf("ticket %s: not found", t.ID)
|
||||
}
|
||||
@@ -121,12 +165,16 @@ func (s *TicketStore) Update(t ticket.Ticket) error {
|
||||
}
|
||||
t.CreatedAt = parseTime(created)
|
||||
t.UpdatedAt = time.Now().UTC()
|
||||
if number.Valid {
|
||||
t.Number = int(number.Int64)
|
||||
}
|
||||
t.Key = key
|
||||
res, err := s.db.SQL.Exec(`
|
||||
UPDATE tickets SET title=?, description=?, status=?, project_id=?, created_at=?, updated_at=?,
|
||||
source=?, external_id=?, external_url=?, synced_at=?
|
||||
source=?, external_id=?, external_url=?, synced_at=?, number=?, key=?
|
||||
WHERE id=?`,
|
||||
t.Title, t.Description, string(t.Status), t.ProjectID, formatTime(t.CreatedAt), formatTime(t.UpdatedAt),
|
||||
t.Source, t.ExternalID, t.ExternalURL, formatTime(t.SyncedAt), t.ID,
|
||||
t.Source, t.ExternalID, t.ExternalURL, formatTime(t.SyncedAt), nullInt(t.Number), t.Key, t.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -138,6 +186,13 @@ func (s *TicketStore) Update(t ticket.Ticket) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func nullInt(n int) any {
|
||||
if n == 0 {
|
||||
return nil
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// MarkSynced sets SyncedAt = UpdatedAt without bumping UpdatedAt.
|
||||
func (s *TicketStore) MarkSynced(id string) error {
|
||||
if id == "" {
|
||||
@@ -154,7 +209,7 @@ func (s *TicketStore) MarkSynced(id string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Move relocates a ticket to another project.
|
||||
// Move relocates a ticket to another project and assigns a new key there.
|
||||
func (s *TicketStore) Move(id, toProjectID string) (ticket.Ticket, error) {
|
||||
if id == "" {
|
||||
return ticket.Ticket{}, fmt.Errorf("ticket id is required")
|
||||
@@ -162,17 +217,48 @@ func (s *TicketStore) Move(id, toProjectID string) (ticket.Ticket, error) {
|
||||
if toProjectID == "" {
|
||||
return ticket.Ticket{}, fmt.Errorf("projectId is required")
|
||||
}
|
||||
t, err := s.Get(id)
|
||||
|
||||
tx, err := s.db.SQL.Begin()
|
||||
if err != nil {
|
||||
return ticket.Ticket{}, err
|
||||
}
|
||||
t.ProjectID = toProjectID
|
||||
t.UpdatedAt = time.Now().UTC()
|
||||
_, err = s.db.SQL.Exec(`UPDATE tickets SET project_id=?, updated_at=? WHERE id=?`,
|
||||
toProjectID, formatTime(t.UpdatedAt), id)
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
row := tx.QueryRow(`SELECT `+ticketCols+` FROM tickets WHERE id = ?`, id)
|
||||
t, err := scanTicket(row)
|
||||
if err == sql.ErrNoRows {
|
||||
return ticket.Ticket{}, fmt.Errorf("ticket %s: not found", id)
|
||||
}
|
||||
if err != nil {
|
||||
return ticket.Ticket{}, err
|
||||
}
|
||||
|
||||
res, err := tx.Exec(`UPDATE projects SET ticket_seq = ticket_seq + 1 WHERE id = ?`, toProjectID)
|
||||
if err != nil {
|
||||
return ticket.Ticket{}, err
|
||||
}
|
||||
n, _ := res.RowsAffected()
|
||||
if n == 0 {
|
||||
return ticket.Ticket{}, fmt.Errorf("project %s: not found", toProjectID)
|
||||
}
|
||||
var code string
|
||||
var seq int
|
||||
if err := tx.QueryRow(`SELECT code, ticket_seq FROM projects WHERE id = ?`, toProjectID).Scan(&code, &seq); err != nil {
|
||||
return ticket.Ticket{}, err
|
||||
}
|
||||
t.ProjectID = toProjectID
|
||||
t.Number = seq
|
||||
t.Key = fmt.Sprintf("%s-%d", code, seq)
|
||||
t.UpdatedAt = time.Now().UTC()
|
||||
|
||||
_, err = tx.Exec(`UPDATE tickets SET project_id=?, updated_at=?, number=?, key=? WHERE id=?`,
|
||||
toProjectID, formatTime(t.UpdatedAt), t.Number, t.Key, id)
|
||||
if err != nil {
|
||||
return ticket.Ticket{}, err
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return ticket.Ticket{}, err
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user