feat: Complete key:value format implementation and fix tag sync
Implement complete key:value format parsing for change log entries and fix critical tag synchronization issue from server to client. Key Changes: 1. Shared Key:Value Parser (NEW: internal/engine/parser.go) - Created ParseKeyValueFormat() for both edit and sync operations - Supports flexible whitespace: 'key:value' and 'key: value' - Handles comment skipping for edit files - Consolidates parsing logic (DRY principle) 2. Database Triggers - Tags Support (internal/engine/database.go) - Added tags to track_task_create trigger - Added tags to track_task_update trigger - Tags sorted alphabetically via SQL ORDER BY - Format: 'tags: alpha,bravo,charlie' 3. Task Creation - Tag Update Fix (internal/engine/task.go) - CreateTaskWithModifier() now triggers update after adding tags - Ensures tags appear in change log (UPDATE entry) - Fixes missing tags in initial CREATE entries 4. Edit Command - Use Shared Parser (cmd/edit.go) - Replaced custom parseEditedFile() with shared ParseKeyValueFormat() - Added tag sorting in parseTags() - Removed ~30 lines, improved maintainability 5. Sync Client - Complete Implementation (internal/sync/client.go) - NEW: applyChangeDataToTask() - parses all fields from change log - NEW: Helper functions for status, priority, tag parsing - FIXED: parseChanges() - sort by timestamp+ID before grouping - Added parent/child task ordering (prevents FK violations) - Enhanced tag sync in merge loop with task reload - Specific validation error messages per field Critical Bug Fix: - When CREATE and UPDATE have same timestamp, old code kept CREATE (no tags) - New code sorts by ID as tiebreaker, ensuring UPDATE (with tags) is used - Verified: Server->client tag sync now works correctly Validation: - Description must not be empty (both edit and sync) - Recurrence validated (not negative, max 100 years) - All timestamps parsed correctly (Unix epoch) - Tags sorted alphabetically in all contexts Testing: - Fresh pull from server: ✅ All tags present - API-created tasks: ✅ Tags sync correctly - Local->server->client round-trip: ✅ No data loss - Same-second CREATE+UPDATE: ✅ Correct entry processed - Parent/child tasks: ✅ Correct ordering Files Changed: - NEW: internal/engine/parser.go (+44 lines) - Modified: internal/engine/database.go (+10 lines) - Modified: internal/engine/task.go (+8 lines) - Modified: cmd/edit.go (-25 lines net) - Modified: internal/sync/client.go (+280 lines) - Modified: srv/README.md (+1 line) Total: +318 lines added, -25 removed, net +293 lines This completes Phase 6: Full bidirectional sync with complete tag support.
This commit is contained in:
@@ -214,7 +214,12 @@ func runMigrations() error {
|
||||
CASE WHEN NEW.wait IS NOT NULL THEN 'wait: ' || NEW.wait || CHAR(10) ELSE '' END ||
|
||||
CASE WHEN NEW.until_date IS NOT NULL THEN 'until: ' || NEW.until_date || CHAR(10) ELSE '' END ||
|
||||
CASE WHEN NEW.recurrence_duration IS NOT NULL THEN 'recurrence: ' || NEW.recurrence_duration || CHAR(10) ELSE '' END ||
|
||||
CASE WHEN NEW.parent_uuid IS NOT NULL THEN 'parent_uuid: ' || NEW.parent_uuid || CHAR(10) ELSE '' END
|
||||
CASE WHEN NEW.parent_uuid IS NOT NULL THEN 'parent_uuid: ' || NEW.parent_uuid || CHAR(10) ELSE '' END ||
|
||||
(SELECT CASE WHEN COUNT(*) > 0
|
||||
THEN 'tags: ' || GROUP_CONCAT(tag, ',') || CHAR(10)
|
||||
ELSE ''
|
||||
END
|
||||
FROM (SELECT tag FROM tags WHERE task_id = NEW.id ORDER BY tag))
|
||||
);
|
||||
END;
|
||||
|
||||
@@ -248,7 +253,12 @@ func runMigrations() error {
|
||||
CASE WHEN NEW.wait IS NOT NULL THEN 'wait: ' || NEW.wait || CHAR(10) ELSE '' END ||
|
||||
CASE WHEN NEW.until_date IS NOT NULL THEN 'until: ' || NEW.until_date || CHAR(10) ELSE '' END ||
|
||||
CASE WHEN NEW.recurrence_duration IS NOT NULL THEN 'recurrence: ' || NEW.recurrence_duration || CHAR(10) ELSE '' END ||
|
||||
CASE WHEN NEW.parent_uuid IS NOT NULL THEN 'parent_uuid: ' || NEW.parent_uuid || CHAR(10) ELSE '' END
|
||||
CASE WHEN NEW.parent_uuid IS NOT NULL THEN 'parent_uuid: ' || NEW.parent_uuid || CHAR(10) ELSE '' END ||
|
||||
(SELECT CASE WHEN COUNT(*) > 0
|
||||
THEN 'tags: ' || GROUP_CONCAT(tag, ',') || CHAR(10)
|
||||
ELSE ''
|
||||
END
|
||||
FROM (SELECT tag FROM tags WHERE task_id = NEW.id ORDER BY tag))
|
||||
);
|
||||
END;
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ParseKeyValueFormat parses key:value format from text
|
||||
// Each line should be "key:value" or "key: value" (whitespace trimmed)
|
||||
// If skipComments=true, lines starting with # are ignored
|
||||
// Empty lines are always skipped
|
||||
func ParseKeyValueFormat(data string, skipComments bool) (map[string]string, error) {
|
||||
fields := make(map[string]string)
|
||||
lines := strings.Split(data, "\n")
|
||||
|
||||
for i, line := range lines {
|
||||
// Trim whitespace
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
// Skip empty lines
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip comments if requested
|
||||
if skipComments && strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Split on first ':'
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("line %d: invalid format (expected 'key:value')", i+1)
|
||||
}
|
||||
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
|
||||
fields[key] = value
|
||||
}
|
||||
|
||||
return fields, nil
|
||||
}
|
||||
@@ -173,6 +173,14 @@ func CreateTaskWithModifier(description string, mod *Modifier) (*Task, error) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Update task to trigger change log with tags
|
||||
if len(mod.AddTags) > 0 {
|
||||
task.Modified = timeNow()
|
||||
if err := task.Save(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return task, nil
|
||||
|
||||
Reference in New Issue
Block a user