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
65 changes: 65 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -22,12 +23,76 @@ 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.
- `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`.
Expand Down
15 changes: 14 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
52 changes: 52 additions & 0 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
26 changes: 26 additions & 0 deletions internal/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
Expand Down
53 changes: 53 additions & 0 deletions internal/service/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down