From ac4625f077198d8c95f054ab9ae4540d1cadb637 Mon Sep 17 00:00:00 2001 From: Dylan Ravel Date: Sat, 11 Jul 2026 09:30:34 -0600 Subject: [PATCH 1/3] Make `tmpo edit` undoable --- cmd/entries/edit.go | 7 +++++++ cmd/utilities/undo.go | 7 +++++++ internal/storage/undo.go | 1 + 3 files changed, 15 insertions(+) diff --git a/cmd/entries/edit.go b/cmd/entries/edit.go index 9d79c9d..4f7c492 100644 --- a/cmd/entries/edit.go +++ b/cmd/entries/edit.go @@ -433,6 +433,13 @@ func EditCmd() *cobra.Command { os.Exit(1) } + db.SaveLastAction(storage.UndoAction{ + Type: storage.ActionEdit, + EntryID: selectedEntry.ID, + ProjectName: selectedEntry.ProjectName, + Entry: selectedEntry, + }) + fmt.Println() ui.PrintSuccess(ui.EmojiSuccess, "Entry updated successfully") ui.NewlineBelow() diff --git a/cmd/utilities/undo.go b/cmd/utilities/undo.go index 7894220..cefbc44 100644 --- a/cmd/utilities/undo.go +++ b/cmd/utilities/undo.go @@ -17,6 +17,7 @@ var actionDescriptions = map[storage.ActionType]string{ storage.ActionResume: "Resumed tracking", storage.ActionManual: "Created manual entry for", storage.ActionDelete: "Deleted entry for", + storage.ActionEdit: "Edited entry for", } func UndoCmd() *cobra.Command { @@ -104,6 +105,12 @@ func applyUndo(db *storage.Database, action *storage.UndoAction) error { } return db.RestoreDeletedEntry(action.Entry) + case storage.ActionEdit: + if action.Entry == nil { + return fmt.Errorf("no entry snapshot available to restore") + } + return db.UpdateTimeEntry(action.EntryID, action.Entry) + default: return fmt.Errorf("unknown action type: %s", action.Type) } diff --git a/internal/storage/undo.go b/internal/storage/undo.go index 8c0af86..e40296a 100644 --- a/internal/storage/undo.go +++ b/internal/storage/undo.go @@ -16,6 +16,7 @@ const ( ActionResume ActionType = "resume" ActionDelete ActionType = "delete" ActionManual ActionType = "manual" + ActionEdit ActionType = "edit" ) const lastActionKey = "last_action" From 70af1d1c79bfa365f704b654d016e6ea569f1b64 Mon Sep 17 00:00:00 2001 From: Dylan Ravel Date: Sat, 11 Jul 2026 09:30:50 -0600 Subject: [PATCH 2/3] Add test case for edit undo + update docs --- cmd/utilities/undo_test.go | 43 ++++++++++++++++++++++++++++++++++++++ docs/usage.md | 17 ++++++++------- 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/cmd/utilities/undo_test.go b/cmd/utilities/undo_test.go index 3993f4d..8ad6d4f 100644 --- a/cmd/utilities/undo_test.go +++ b/cmd/utilities/undo_test.go @@ -2,6 +2,7 @@ package utilities import ( "testing" + "time" "github.com/DylanDevelops/tmpo/internal/storage" "github.com/stretchr/testify/assert" @@ -31,6 +32,7 @@ func TestUndoActionDescription(t *testing.T) { {storage.ActionResume, "Resumed tracking"}, {storage.ActionManual, "Created manual entry for"}, {storage.ActionDelete, "Deleted entry for"}, + {storage.ActionEdit, "Edited entry for"}, } for _, tt := range tests { @@ -91,6 +93,47 @@ func TestApplyUndo_Delete_ErrorWhenNoSnapshot(t *testing.T) { assert.Contains(t, err.Error(), "no entry snapshot") } +func TestApplyUndo_Edit_ErrorWhenNoSnapshot(t *testing.T) { + db := setupUndoTestDB(t) + + action := &storage.UndoAction{Type: storage.ActionEdit, ProjectName: "proj", Entry: nil} + err := applyUndo(db, action) + assert.Error(t, err) + assert.Contains(t, err.Error(), "no entry snapshot") +} + +func TestApplyUndo_Edit_RestoresOriginalEntry(t *testing.T) { + db := setupUndoTestDB(t) + + // Create and stop an entry, then capture its pre-edit snapshot. + orig, err := db.CreateEntry("proj", "before", nil, nil) + require.NoError(t, err) + require.NoError(t, db.StopEntry(orig.ID)) + + before, err := db.GetEntry(orig.ID) + require.NoError(t, err) + + // Apply an edit that changes several fields. + edited := *before + newStart := before.StartTime.Add(-time.Hour) + newEnd := before.EndTime.Add(time.Hour) + edited.StartTime = newStart + edited.EndTime = &newEnd + edited.Description = "after" + require.NoError(t, db.UpdateTimeEntry(edited.ID, &edited)) + + // Undo should restore the original values. + action := &storage.UndoAction{Type: storage.ActionEdit, EntryID: before.ID, ProjectName: "proj", Entry: before} + require.NoError(t, applyUndo(db, action)) + + restored, err := db.GetEntry(orig.ID) + require.NoError(t, err) + assert.Equal(t, "before", restored.Description) + assert.True(t, restored.StartTime.Equal(before.StartTime), "start time should be restored") + require.NotNil(t, restored.EndTime) + assert.True(t, restored.EndTime.Equal(*before.EndTime), "end time should be restored") +} + func TestApplyUndo_UnknownType_ReturnsError(t *testing.T) { db := setupUndoTestDB(t) diff --git a/docs/usage.md b/docs/usage.md index dcd73f8..0cbe51a 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -433,14 +433,15 @@ Revert the most recent time-tracking action in case of a mistake. A single confi **Supported actions:** -| Action | What undo does | -|---------------|---------------------------------------------------| -| `tmpo start` | Deletes the entry that was just created | -| `tmpo stop` | Re-opens the stopped entry as a running timer | -| `tmpo pause` | Re-opens the paused entry as a running timer | -| `tmpo resume` | Deletes the new entry that was created to resume | -| `tmpo manual` | Deletes the manually created entry | -| `tmpo delete` | Restores the deleted entry with its original data | +| Action | What undo does | +|---------------|--------------------------------------------------------| +| `tmpo start` | Deletes the entry that was just created | +| `tmpo stop` | Re-opens the stopped entry as a running timer | +| `tmpo pause` | Re-opens the paused entry as a running timer | +| `tmpo resume` | Deletes the new entry that was created to resume | +| `tmpo manual` | Deletes the manually created entry | +| `tmpo delete` | Restores the deleted entry with its original data | +| `tmpo edit` | Restores the previous version of the edited time entry | **Examples:** From 86a90b1b456a684216f619dd459acd177dafe6f2 Mon Sep 17 00:00:00 2001 From: Dylan Ravel Date: Sat, 11 Jul 2026 09:32:28 -0600 Subject: [PATCH 3/3] Edit wording of `tmpo undo` usage action table --- docs/usage.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 0cbe51a..a850089 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -433,15 +433,15 @@ Revert the most recent time-tracking action in case of a mistake. A single confi **Supported actions:** -| Action | What undo does | -|---------------|--------------------------------------------------------| -| `tmpo start` | Deletes the entry that was just created | -| `tmpo stop` | Re-opens the stopped entry as a running timer | -| `tmpo pause` | Re-opens the paused entry as a running timer | -| `tmpo resume` | Deletes the new entry that was created to resume | -| `tmpo manual` | Deletes the manually created entry | -| `tmpo delete` | Restores the deleted entry with its original data | -| `tmpo edit` | Restores the previous version of the edited time entry | +| Action | What undo does | +|---------------|-------------------------------------------------------| +| `tmpo start` | Deletes the entry that was just created | +| `tmpo stop` | Re-opens the stopped entry as a running timer | +| `tmpo pause` | Re-opens the paused entry as a running timer | +| `tmpo resume` | Deletes the new entry that was created to resume | +| `tmpo manual` | Deletes the manually created entry | +| `tmpo delete` | Restores the deleted entry with its original data | +| `tmpo edit` | Restores the previous version of an edited time entry | **Examples:**