feat: Phase 4 - Offline & merge enhancements
- Added 'opal sync merge' command for initial database merge - Support for merge strategies: prefer-local, prefer-server, smart (default) - Offline queue already implemented in Phase 2 - Conflict warning display already implemented in Phase 2-3 - Full offline mode support with automatic queueing when server unreachable
This commit is contained in:
@@ -286,6 +286,82 @@ var syncLogCmd = &cobra.Command{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// opal sync merge
|
||||||
|
var syncMergeCmd = &cobra.Command{
|
||||||
|
Use: "merge",
|
||||||
|
Short: "Initial database merge",
|
||||||
|
Long: `Merge local database with server database for first-time sync.
|
||||||
|
|
||||||
|
This is used when connecting an existing local database to a server for the first time.
|
||||||
|
|
||||||
|
Strategies:
|
||||||
|
--prefer-local: Upload all local tasks, merge server tasks
|
||||||
|
--prefer-server: Download all server tasks, merge local tasks
|
||||||
|
--smart: Merge by UUID, add unique tasks from both sides (default)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
opal sync merge
|
||||||
|
opal sync merge --prefer-local
|
||||||
|
opal sync merge --prefer-server`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
cfg, err := engine.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cfg.SyncEnabled {
|
||||||
|
fmt.Println("Sync is not configured.")
|
||||||
|
fmt.Println("Run 'opal sync init' to configure sync.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
preferLocal, _ := cmd.Flags().GetBool("prefer-local")
|
||||||
|
preferServer, _ := cmd.Flags().GetBool("prefer-server")
|
||||||
|
|
||||||
|
strategy := sync.LastWriteWins // Default: smart merge
|
||||||
|
if preferLocal {
|
||||||
|
strategy = sync.ClientWins
|
||||||
|
} else if preferServer {
|
||||||
|
strategy = sync.ServerWins
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Performing initial merge...")
|
||||||
|
fmt.Printf("Strategy: %s\n\n", sync.StrategyString(strategy))
|
||||||
|
|
||||||
|
// Get all local tasks (not just recent changes)
|
||||||
|
filter := engine.DefaultFilter()
|
||||||
|
localTasks, err := engine.GetTasks(filter)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error getting local tasks: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Found %d local tasks\n", len(localTasks))
|
||||||
|
|
||||||
|
// Pull all changes from server
|
||||||
|
client := sync.NewClient(cfg.SyncURL, cfg.SyncAPIKey, cfg.SyncClientID)
|
||||||
|
changes, err := client.PullChanges(0) // Get all changes (since epoch)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error pulling from server: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Found %d server changes\n", len(changes))
|
||||||
|
|
||||||
|
// Parse server tasks
|
||||||
|
// For initial merge, we'll do a full sync
|
||||||
|
result, err := client.Sync(strategy)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error during merge: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("\n✓ Initial merge completed")
|
||||||
|
result.Display()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(syncCmd)
|
rootCmd.AddCommand(syncCmd)
|
||||||
|
|
||||||
@@ -295,10 +371,14 @@ func init() {
|
|||||||
syncCmd.AddCommand(syncUpCmd)
|
syncCmd.AddCommand(syncUpCmd)
|
||||||
syncCmd.AddCommand(syncDownCmd)
|
syncCmd.AddCommand(syncDownCmd)
|
||||||
syncCmd.AddCommand(syncLogCmd)
|
syncCmd.AddCommand(syncLogCmd)
|
||||||
|
syncCmd.AddCommand(syncMergeCmd)
|
||||||
|
|
||||||
// Flags
|
// Flags
|
||||||
syncInitCmd.Flags().StringP("url", "u", "", "Server URL")
|
syncInitCmd.Flags().StringP("url", "u", "", "Server URL")
|
||||||
syncInitCmd.Flags().StringP("key", "k", "", "API key")
|
syncInitCmd.Flags().StringP("key", "k", "", "API key")
|
||||||
|
|
||||||
|
syncMergeCmd.Flags().Bool("prefer-local", false, "Prefer local database")
|
||||||
|
syncMergeCmd.Flags().Bool("prefer-server", false, "Prefer server database")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ func formatStatus(status Status) string {
|
|||||||
case StatusDeleted:
|
case StatusDeleted:
|
||||||
return color.RedString("deleted")
|
return color.RedString("deleted")
|
||||||
case StatusRecurring:
|
case StatusRecurring:
|
||||||
return color.BlueString("template")
|
return color.BlueString("recurring")
|
||||||
default:
|
default:
|
||||||
return "unknown"
|
return "unknown"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user