Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 3 additions & 8 deletions cmd/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func runAdd(cmd *cobra.Command, args []string) error {
}
}

id, err := s.NextID()
id, err := store.NewID()
if err != nil {
return err
}
Expand All @@ -72,12 +72,7 @@ func runAdd(cmd *cobra.Command, args []string) error {
CreatedAt: time.Now().UTC(),
}

tickets, err := s.ReadTickets()
if err != nil {
return err
}
tickets = append(tickets, ticket)
if err := s.WriteTickets(tickets); err != nil {
if err := s.WriteTicket(ticket); err != nil {
return err
}

Expand All @@ -101,7 +96,7 @@ func runAdd(cmd *cobra.Command, args []string) error {
}
}

fmt.Printf("#%d %s\n", id, title)
fmt.Printf("#%s %s\n", id, title)
return nil
}

Expand Down
21 changes: 5 additions & 16 deletions cmd/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package cmd
import (
"fmt"
"os"
"strconv"

"github.com/frankcruz/tasklin/internal/store"
"github.com/spf13/cobra"
Expand All @@ -17,10 +16,7 @@ var deleteCmd = &cobra.Command{
}

func runDelete(cmd *cobra.Command, args []string) error {
ticketID, err := strconv.Atoi(args[0])
if err != nil {
return fmt.Errorf("invalid ticket id: %s", args[0])
}
ticketID := args[0]

cwd, err := os.Getwd()
if err != nil {
Expand All @@ -44,26 +40,19 @@ func runDelete(cmd *cobra.Command, args []string) error {
}
}
if idx == -1 {
return fmt.Errorf("ticket %d not found", ticketID)
}

deleted, err := s.ReadDeleted()
if err != nil {
return err
return fmt.Errorf("ticket %s not found", ticketID)
}

title := tickets[idx].Title
deleted = append(deleted, tickets[idx])
tickets = append(tickets[:idx], tickets[idx+1:]...)

if err := s.WriteTickets(tickets); err != nil {
if err := s.WriteDeletedTicket(tickets[idx]); err != nil {
return err
}
if err := s.WriteDeleted(deleted); err != nil {
if err := s.DeleteTicketFile(ticketID); err != nil {
return err
}

fmt.Printf("#%d %s deleted\n", ticketID, title)
fmt.Printf("#%s %s deleted\n", ticketID, title)
return nil
}

Expand Down
17 changes: 6 additions & 11 deletions cmd/move.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package cmd
import (
"fmt"
"os"
"strconv"
"strings"
"time"

Expand All @@ -20,10 +19,7 @@ var moveCmd = &cobra.Command{
}

func runMove(cmd *cobra.Command, args []string) error {
ticketID, err := strconv.Atoi(args[0])
if err != nil {
return fmt.Errorf("invalid ticket id: %s", args[0])
}
ticketID := args[0]
targetStatus := args[1]

cwd, err := os.Getwd()
Expand Down Expand Up @@ -68,17 +64,16 @@ func runMove(cmd *cobra.Command, args []string) error {
tr := model.Transition{From: t.Status, To: resolved, At: time.Now().UTC()}
tickets[i].Status = resolved
tickets[i].Transitions = append(tickets[i].Transitions, tr)
if err := s.WriteTicket(tickets[i]); err != nil {
return err
}
break
}
if !found {
return fmt.Errorf("ticket %d not found", ticketID)
}

if err := s.WriteTickets(tickets); err != nil {
return err
return fmt.Errorf("ticket %s not found", ticketID)
}

fmt.Printf("#%d → %s\n", ticketID, resolved)
fmt.Printf("#%s → %s\n", ticketID, resolved)
return nil
}

Expand Down
11 changes: 3 additions & 8 deletions cmd/show.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package cmd
import (
"fmt"
"os"
"strconv"
"strings"

"github.com/charmbracelet/lipgloss"
Expand Down Expand Up @@ -53,10 +52,7 @@ func statusColor(name string) lipgloss.Color {
}

func runShow(cmd *cobra.Command, args []string) error {
ticketID, err := strconv.Atoi(args[0])
if err != nil {
return fmt.Errorf("invalid ticket id: %s", args[0])
}
ticketID := args[0]

cwd, err := os.Getwd()
if err != nil {
Expand Down Expand Up @@ -85,11 +81,10 @@ func runShow(cmd *cobra.Command, args []string) error {
}
}
if idx == -1 {
return fmt.Errorf("ticket %d not found", ticketID)
return fmt.Errorf("ticket %s not found", ticketID)
}
t := tickets[idx]

// Resolve status color from config.
dotColor := lipgloss.Color("252")
for _, st := range cfg.Statuses {
if st.Name == t.Status {
Expand All @@ -102,7 +97,7 @@ func runShow(cmd *cobra.Command, args []string) error {

fmt.Println()
fmt.Printf(" %s %s\n",
showIDStyle.Render(fmt.Sprintf("#%d", t.ID)),
showIDStyle.Render(fmt.Sprintf("#%s", t.ID)),
showTitleStyle.Render(t.Title),
)
fmt.Println(" " + showSepStyle.Render(sep))
Expand Down
14 changes: 5 additions & 9 deletions cmd/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package cmd
import (
"fmt"
"os"
"strconv"
"strings"

"github.com/frankcruz/tasklin/internal/store"
Expand All @@ -22,10 +21,7 @@ var updateCmd = &cobra.Command{
}

func runUpdate(cmd *cobra.Command, args []string) error {
ticketID, err := strconv.Atoi(args[0])
if err != nil {
return fmt.Errorf("invalid ticket id: %s", args[0])
}
ticketID := args[0]

if !cmd.Flags().Changed("title") && !cmd.Flags().Changed("add-label") && !cmd.Flags().Changed("remove-label") {
return fmt.Errorf("nothing to update: specify --title, --add-label, or --remove-label")
Expand Down Expand Up @@ -53,7 +49,7 @@ func runUpdate(cmd *cobra.Command, args []string) error {
}
}
if idx == -1 {
return fmt.Errorf("ticket %d not found", ticketID)
return fmt.Errorf("ticket %s not found", ticketID)
}

t := &tickets[idx]
Expand Down Expand Up @@ -109,11 +105,11 @@ func runUpdate(cmd *cobra.Command, args []string) error {
}

if len(changes) == 0 {
fmt.Printf("#%d no changes\n", ticketID)
fmt.Printf("#%s no changes\n", ticketID)
return nil
}

if err := s.WriteTickets(tickets); err != nil {
if err := s.WriteTicket(*t); err != nil {
return err
}

Expand All @@ -137,7 +133,7 @@ func runUpdate(cmd *cobra.Command, args []string) error {
}
}

fmt.Printf("#%d %s\n", t.ID, t.Title)
fmt.Printf("#%s %s\n", t.ID, t.Title)
for _, c := range changes {
fmt.Println(c)
}
Expand Down
13 changes: 10 additions & 3 deletions internal/store/migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"fmt"
"os"
"path/filepath"
"strconv"
"time"

"github.com/frankcruz/tasklin/internal/model"
Expand Down Expand Up @@ -49,8 +48,12 @@ func (s *Store) migrateTickets() (bool, error) {
return false, err
}
for _, lt := range ltf.Tickets {
id, err := NewID()
if err != nil {
return false, fmt.Errorf("migrate tickets: generate id: %w", err)
}
t := model.Ticket{
ID: strconv.Itoa(lt.ID),
ID: id,
Title: lt.Title,
Status: lt.Status,
Labels: lt.Labels,
Expand Down Expand Up @@ -78,8 +81,12 @@ func (s *Store) migrateDeleted() (bool, error) {
return false, err
}
for _, lt := range ltf.Tickets {
id, err := NewID()
if err != nil {
return false, fmt.Errorf("migrate deleted: generate id: %w", err)
}
t := model.Ticket{
ID: strconv.Itoa(lt.ID),
ID: id,
Title: lt.Title,
Status: lt.Status,
Labels: lt.Labels,
Expand Down
100 changes: 91 additions & 9 deletions internal/store/store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,20 +149,102 @@ func TestReadTickets_EmptyDir(t *testing.T) {

func TestNewID(t *testing.T) {
hexRe := regexp.MustCompile(`^[0-9a-f]{8}$`)
id, err := store.NewID()
seen := make(map[string]bool, 200)
for i := 0; i < 200; i++ {
id, err := store.NewID()
if err != nil {
t.Fatalf("NewID call %d: %v", i, err)
}
if !hexRe.MatchString(id) {
t.Errorf("NewID returned %q, want 8 lowercase hex chars", id)
}
if seen[id] {
t.Errorf("NewID produced duplicate id %q after %d calls", id, i)
}
seen[id] = true
}
}

func TestWriteReadDeletedTicket(t *testing.T) {
s := newTempStore(t)
if err := s.Init(model.DefaultConfig()); err != nil {
t.Fatal(err)
}
tk := model.Ticket{ID: "dead0001", Title: "archived task", Status: "Done", CreatedAt: time.Now().UTC()}
if err := s.WriteDeletedTicket(tk); err != nil {
t.Fatalf("WriteDeletedTicket: %v", err)
}
got, err := s.ReadDeleted()
if err != nil {
t.Fatalf("ReadDeleted: %v", err)
}
if len(got) != 1 {
t.Fatalf("expected 1 deleted ticket, got %d", len(got))
}
if got[0].ID != tk.ID {
t.Errorf("ID mismatch: want %q, got %q", tk.ID, got[0].ID)
}
if got[0].Title != tk.Title {
t.Errorf("Title mismatch: want %q, got %q", tk.Title, got[0].Title)
}
}

func TestMigrateIfNeeded_Tickets(t *testing.T) {
dir := t.TempDir()
s := store.New(dir)
if err := s.Init(model.DefaultConfig()); err != nil {
t.Fatal(err)
}

// Write a legacy tickets.yaml with integer-ID tickets.
legacyYAML := `tickets:
- id: 1
title: First
status: To Do
created_at: 2024-01-01T00:00:00Z
- id: 2
title: Second
status: In Progress
created_at: 2024-01-02T00:00:00Z
`
if err := os.WriteFile(filepath.Join(s.TodoPath(), "tickets.yaml"), []byte(legacyYAML), 0644); err != nil {
t.Fatal(err)
}

migrated, err := s.MigrateIfNeeded()
if err != nil {
t.Fatalf("NewID: %v", err)
t.Fatalf("MigrateIfNeeded: %v", err)
}
if !hexRe.MatchString(id) {
t.Errorf("NewID returned %q, want 8 lowercase hex chars", id)
if !migrated {
t.Error("expected migration to have occurred")
}
// Two calls should produce different IDs.
id2, err := store.NewID()

tickets, err := s.ReadTickets()
if err != nil {
t.Fatalf("NewID (second call): %v", err)
t.Fatalf("ReadTickets after migration: %v", err)
}
if len(tickets) != 2 {
t.Fatalf("expected 2 tickets after migration, got %d", len(tickets))
}

hexRe := regexp.MustCompile(`^[0-9a-f]{8}$`)
titles := map[string]bool{}
for _, tk := range tickets {
if !hexRe.MatchString(tk.ID) {
t.Errorf("migrated ticket has non-hex ID %q; git hooks require 8-char hex IDs", tk.ID)
}
titles[tk.Title] = true
}
if !titles["First"] || !titles["Second"] {
t.Error("migrated tickets are missing expected titles")
}

// Legacy file must have been renamed to .bak.
if _, err := os.Stat(filepath.Join(s.TodoPath(), "tickets.yaml")); !os.IsNotExist(err) {
t.Error("expected tickets.yaml to be removed after migration")
}
if id == id2 {
t.Error("two NewID calls returned the same value")
if _, err := os.Stat(filepath.Join(s.TodoPath(), "tickets.yaml.bak")); err != nil {
t.Error("expected tickets.yaml.bak to exist after migration")
}
}

Expand Down
Loading
Loading