From e9cf788dbec332f7164331b6e9bfca1915fe3eae Mon Sep 17 00:00:00 2001 From: okaryo Date: Sat, 18 Apr 2026 23:24:42 +0900 Subject: [PATCH 1/2] feat: add `amend` command to replace the most recent log entry --- README.md | 2 ++ cmd/root.go | 15 ++++++++- cmd/root_test.go | 52 +++++++++++++++++++++++++++++++ internal/service/service.go | 26 ++++++++++++++++ internal/service/service_test.go | 53 ++++++++++++++++++++++++++++++++ 5 files changed, 147 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 84f0524..6872829 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ go install github.com/okaryo/dlog@latest ```bash dlog "task progress update" +dlog amend "corrected task progress update" dlog dlog log dlog log --date 2026-04-12 @@ -22,6 +23,7 @@ dlog md --date 2026-04-12 ## Behavior - `dlog "text"` appends a log entry for today with the current local timestamp. +- `dlog amend "text"` replaces today's most recent log entry while keeping its original timestamp. - `dlog` and `dlog log` show today's logs in reverse chronological order. - `dlog log --date YYYY-MM-DD` shows logs for the specified date. - `dlog md` prints today's logs in Markdown order from oldest to newest. diff --git a/cmd/root.go b/cmd/root.go index aa4970f..0738a09 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -39,7 +39,7 @@ func NewRootCmd(svc *service.Service, out io.Writer, errOut io.Writer) *cobra.Co cmd.SetOut(out) cmd.SetErr(errOut) - cmd.AddCommand(newLogCmd(svc, out), newMarkdownCmd(svc, out)) + cmd.AddCommand(newLogCmd(svc, out), newMarkdownCmd(svc, out), newAmendCmd(svc)) return cmd } @@ -117,3 +117,16 @@ func newMarkdownCmd(svc *service.Service, out io.Writer) *cobra.Command { return cmd } + +func newAmendCmd(svc *service.Service) *cobra.Command { + cmd := &cobra.Command{ + Use: "amend [text]", + Short: "Replace today's most recent log entry", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return svc.AmendTodayLog(args[0]) + }, + } + + return cmd +} diff --git a/cmd/root_test.go b/cmd/root_test.go index fa14d3e..c7179bf 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -189,6 +189,58 @@ func TestRootCommandRejectsEmptyText(t *testing.T) { } } +func TestAmendSubcommandReplacesMostRecentLog(t *testing.T) { + current := time.Date(2026, 4, 12, 10, 3, 0, 0, time.FixedZone("JST", 9*60*60)) + root, _, _, store := newTestRootCmd(t, func() time.Time { return current }) + + root.SetArgs([]string{"first task"}) + if err := root.Execute(); err != nil { + t.Fatalf("seed first log: %v", err) + } + + current = time.Date(2026, 4, 12, 11, 10, 0, 0, time.FixedZone("JST", 9*60*60)) + root.SetArgs([]string{"second task"}) + if err := root.Execute(); err != nil { + t.Fatalf("seed second log: %v", err) + } + + root.SetArgs([]string{"amend", "corrected second task"}) + if err := root.Execute(); err != nil { + t.Fatalf("execute amend command: %v", err) + } + + data, err := os.ReadFile(filepath.Join(store.BaseDir(), "2026-04-12.json")) + if err != nil { + t.Fatalf("read saved file: %v", err) + } + + content := string(data) + if !strings.Contains(content, `"text": "first task"`) { + t.Fatalf("saved file missing first text: %s", content) + } + if !strings.Contains(content, `"text": "corrected second task"`) { + t.Fatalf("saved file missing amended text: %s", content) + } + if strings.Contains(content, `"text": "second task"`) { + t.Fatalf("saved file still has old text: %s", content) + } +} + +func TestAmendSubcommandFailsWithoutExistingLogs(t *testing.T) { + now := time.Date(2026, 4, 12, 10, 3, 21, 0, time.FixedZone("JST", 9*60*60)) + root, _, _, _ := newTestRootCmd(t, func() time.Time { return now }) + + root.SetArgs([]string{"amend", "corrected task"}) + err := root.Execute() + if err == nil { + t.Fatalf("expected error") + } + + if !strings.Contains(err.Error(), "no logs to amend for today") { + t.Fatalf("unexpected error: %v", err) + } +} + func newTestRootCmd(t *testing.T, now func() time.Time) (*cobra.Command, *bytes.Buffer, *bytes.Buffer, *storage.Store) { t.Helper() diff --git a/internal/service/service.go b/internal/service/service.go index 73a1f81..c5b7282 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -55,6 +55,32 @@ func (s *Service) AddTodayLog(text string) error { return s.store.SaveDay(dayLog) } +func (s *Service) AmendTodayLog(text string) error { + text = strings.TrimSpace(text) + if text == "" { + return fmt.Errorf("log text cannot be empty") + } + + now := s.now() + dayLog, err := s.store.LoadDay(now) + if err != nil { + return err + } + + expectedDate := now.Format(dayLayout) + if dayLog.Date != expectedDate { + return fmt.Errorf("day log date mismatch: expected %s, got %s", expectedDate, dayLog.Date) + } + + if len(dayLog.Logs) == 0 { + return fmt.Errorf("no logs to amend for today") + } + + dayLog.Logs[len(dayLog.Logs)-1].Text = text + + return s.store.SaveDay(dayLog) +} + func (s *Service) GetTodayLog() (*model.DayLog, error) { return s.getLogByTime(s.now()) } diff --git a/internal/service/service_test.go b/internal/service/service_test.go index f90acd7..6e7d5a3 100644 --- a/internal/service/service_test.go +++ b/internal/service/service_test.go @@ -66,6 +66,59 @@ func TestAddTodayLogFailsOnCorruptJSONWithoutChangingFile(t *testing.T) { } } +func TestAmendTodayLogReplacesMostRecentEntryText(t *testing.T) { + now := time.Date(2026, 4, 12, 10, 3, 21, 0, time.FixedZone("JST", 9*60*60)) + store := storage.NewStore(t.TempDir()) + svc := NewWithNow(store, func() time.Time { return now }) + + if err := svc.AddTodayLog("first entry"); err != nil { + t.Fatalf("add first log: %v", err) + } + + now = time.Date(2026, 4, 12, 11, 10, 0, 0, time.FixedZone("JST", 9*60*60)) + if err := svc.AddTodayLog("second entry"); err != nil { + t.Fatalf("add second log: %v", err) + } + + if err := svc.AmendTodayLog(" corrected second entry "); err != nil { + t.Fatalf("amend log: %v", err) + } + + data, err := os.ReadFile(filepath.Join(store.BaseDir(), "2026-04-12.json")) + if err != nil { + t.Fatalf("read day log file: %v", err) + } + + content := string(data) + if !strings.Contains(content, `"text": "first entry"`) { + t.Fatalf("first log text changed unexpectedly: %s", content) + } + if !strings.Contains(content, `"timestamp": "2026-04-12T11:10:00+09:00"`) { + t.Fatalf("most recent timestamp changed unexpectedly: %s", content) + } + if !strings.Contains(content, `"text": "corrected second entry"`) { + t.Fatalf("most recent text was not amended: %s", content) + } + if strings.Contains(content, `"text": "second entry"`) { + t.Fatalf("old most recent text still present: %s", content) + } +} + +func TestAmendTodayLogFailsWhenNoLogsExist(t *testing.T) { + now := time.Date(2026, 4, 12, 10, 3, 21, 0, time.FixedZone("JST", 9*60*60)) + store := storage.NewStore(t.TempDir()) + svc := NewWithNow(store, func() time.Time { return now }) + + err := svc.AmendTodayLog("corrected") + if err == nil { + t.Fatalf("expected error") + } + + if !strings.Contains(err.Error(), "no logs to amend for today") { + t.Fatalf("unexpected error: %v", err) + } +} + func TestGetLogByDateReturnsSpecifiedDayLog(t *testing.T) { now := time.Date(2026, 4, 11, 18, 45, 0, 0, time.FixedZone("JST", 9*60*60)) store := storage.NewStore(t.TempDir()) From 76f667821599475b77565cc1cd0d037006ede91b Mon Sep 17 00:00:00 2001 From: okaryo Date: Sat, 18 Apr 2026 23:25:46 +0900 Subject: [PATCH 2/2] docs: update README with usage examples for dlog commands --- README.md | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/README.md b/README.md index 6872829..9e12f57 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,69 @@ dlog md --date 2026-04-12 - `dlog md --date YYYY-MM-DD` prints logs for the specified date in Markdown order. - Displayed times use the timezone recorded in each log entry, not the viewer's current local timezone. +## Examples + +Record logs for today: + +```bash +$ dlog "task progress update" +$ dlog "api design" +$ dlog "bug fix" +``` + +Amend the most recent log entry: + +```bash +$ dlog amend "bug fix (retry with test case)" +``` + +Show today's logs in reverse chronological order with `dlog` or `dlog log`: + +```bash +$ dlog +2026-04-12 + +11:10 bug fix (retry with test case) +10:25 api design +10:03 task progress update +``` + +```bash +$ dlog log +2026-04-12 + +11:10 bug fix (retry with test case) +10:25 api design +10:03 task progress update +``` + +Show a specific date: + +```bash +$ dlog log --date 2026-04-11 +2026-04-11 + +18:45 previous day task +``` + +Output logs as Markdown in chronological order: + +```bash +$ dlog md +# 2026-04-12 +- 10:03 task progress update +- 10:25 api design +- 11:10 bug fix (retry with test case) +``` + +Output a specific date as Markdown: + +```bash +$ dlog md --date 2026-04-11 +# 2026-04-11 +- 18:45 previous day task +``` + ## Storage - Logs are stored under `~/.dlog`.