From ce9aa24569464205be135ce54f376e674790b69a Mon Sep 17 00:00:00 2001 From: Grant Ammons Date: Sun, 16 Aug 2020 06:29:24 -0400 Subject: [PATCH] Update the sync command (#211) * Update the sync command Previously, setting up a list to sync with ultralist.io was fairly convoluted. Bot the `init` and `sync` commands were dependent on a list's sync state, and the behavior of these commands would change based upon that. **Previous flow:** * `ultralist init` - Create a new list and optionally, sync it with Ultralist.io. * `ultralist sync` - Depending if a list is synced or not, it does multiple things: * If not synced, sets up the local list to sync to ultralist.io. * If is already synced, pull remote changes to local, and push any local changes to remote. This PR simplifies the commands by adding a couple of flags to the `sync` command. The result is that each command has one job to do, instead of many dependent on state. It's simpler to understand as well. **With this PR:** * `ultralist init` - makes this command do just one thing - initialize a list. It does not handle syncing a list as well. * `ultralist sync --setup` - sets up a local list to sync with ultralist.io, or pulls a list from ultralist.io and replaces what's local. * `ultralist sync --unsync` - stops a local list from syncing with ultralist.io. * `ultralist sync` - just handles the actual list syncing between local and ultralist.io. * allow a list to be synced before a .todos.json file exists also, refactor file_store so it is less rigid. * Clean up FileStore more, update tests Co-authored-by: Grant Ammons --- cmd/sync.go | 39 ++++++-- ultralist/app.go | 168 +++++++++++++++++++++-------------- ultralist/event_logger.go | 11 +++ ultralist/file_store.go | 48 +++++----- ultralist/file_store_test.go | 7 +- ultralist/memory_store.go | 4 + ultralist/store.go | 1 + ultralist/todo_test.go | 4 +- 8 files changed, 176 insertions(+), 106 deletions(-) diff --git a/cmd/sync.go b/cmd/sync.go index 97cc3b26..85d83d06 100644 --- a/cmd/sync.go +++ b/cmd/sync.go @@ -6,16 +6,30 @@ import ( ) var ( - syncCmdDesc = "Sync a list with ultralist.io" - syncCmdExample = "ultralist sync" + syncCmdDesc = "Sync a list with ultralist.io" + syncCmdExample = `ultralist sync +ultralist sync --setup +ultralist sync --unsync +ultralist sync --quiet +` + syncCmdQuiet bool - syncCmdLongDesc = ` + setupCmd bool + unsyncCmd bool + syncCmdLongDesc = `Sync a list with Ultralist Pro. + +ultralist sync + The Ultralist CLI stores tasks locally. When running sync, it will push + up any local changes to ultralist.io, as well as pulling down any remote changes from ultralist.io. -The sync command has a few uses: -* If you are logged into ultralist.io (via "ultralist auth"), you can sync an existing list using "ultralist sync". -* For a synced list, you can run "ultralist sync" explicitly to receive any changes that may have occurred elsewhere. +ultralist sync --setup + Set up the local list to sync with ultralist.io. Or, pull a list from Ultralist.io to local. -Local changes to a synced list automatically get pushed to ultralist.io. +ultralist sync --unsync + Stop syncing a local list with ultralist.io. + +ultralist sync --quiet + Perform a sync without showing output to the screen. See https://ultralist.io/docs/cli/pro_integration for more info. ` @@ -27,11 +41,22 @@ var syncCmd = &cobra.Command{ Long: syncCmdLongDesc, Short: syncCmdDesc, Run: func(cmd *cobra.Command, args []string) { + if setupCmd { + ultralist.NewApp().SetupSync() + return + } + if unsyncCmd { + ultralist.NewApp().Unsync() + return + } + ultralist.NewApp().Sync(syncCmdQuiet) }, } func init() { syncCmd.Flags().BoolVarP(&syncCmdQuiet, "quiet", "q", false, "Run without output") + syncCmd.Flags().BoolVarP(&setupCmd, "setup", "", false, "Set up a list to sync with ultralist.io, or pull a remote list to local") + syncCmd.Flags().BoolVarP(&unsyncCmd, "unsync", "", false, "Stop syncing a list with ultralist.io") rootCmd.AddCommand(syncCmd) } diff --git a/ultralist/app.go b/ultralist/app.go index 98a41082..9fda206f 100644 --- a/ultralist/app.go +++ b/ultralist/app.go @@ -55,63 +55,6 @@ func NewAppWithPrintOptions(unicodeSupport bool, colorSupport bool) *App { func (a *App) InitializeRepo() { a.TodoStore.Initialize() fmt.Println("Repo initialized.") - - backend := NewBackend() - eventLogger := &EventLogger{Store: a.TodoStore, CurrentTodoList: a.TodoList} - eventLogger.LoadSyncedLists() - - if !backend.CredsFileExists() { - return - } - - prompt := promptui.Prompt{ - Label: "Do you wish to sync this list with ultralist.io", - IsConfirm: true, - } - - result, _ := prompt.Run() - if result != "y" { - return - } - - if !backend.CanConnect() { - fmt.Println("I cannot connect to ultralist.io right now.") - return - } - - // fetch lists from ultralist.io, or allow user to create a new list - // use the "select_add" example in promptui as a way to do this - type Response struct { - Todolists []TodoList `json:"todolists"` - } - - var response *Response - - resp := backend.PerformRequest("GET", "/api/v1/todo_lists", []byte{}) - json.Unmarshal(resp, &response) - - var todolistNames []string - for _, todolist := range response.Todolists { - todolistNames = append(todolistNames, todolist.Name) - } - - prompt2 := promptui.SelectWithAdd{ - Label: "You can sync with an existing list on ultralist, or create a new list.", - Items: todolistNames, - AddLabel: "New list...", - } - - idx, name, _ := prompt2.Run() - if idx == -1 { - eventLogger.CurrentSyncedList.Name = name - } else { - eventLogger.CurrentSyncedList.Name = response.Todolists[idx].Name - eventLogger.CurrentSyncedList.UUID = response.Todolists[idx].UUID - a.TodoList = &response.Todolists[idx] - a.save() - } - - eventLogger.WriteSyncedLists() } // AddTodo is adding a new todo. @@ -360,19 +303,16 @@ func (a *App) GarbageCollect() { // Sync will sync the todolist with ultralist.io. func (a *App) Sync(quiet bool) { - a.Load() - - if a.EventLogger.CurrentSyncedList.Name == "" { - prompt := promptui.Prompt{ - Label: "Give this list a name", - } + backend := NewBackend() + if !backend.CredsFileExists() { + fmt.Println("You're not authenticated with ultralist.io yet. Please run `ultralist auth` first.") + return + } - result, err := prompt.Run() - if err != nil { - fmt.Println("A name is required to sync a list.") - return - } - a.EventLogger.CurrentSyncedList.Name = result + a.Load() + if !a.TodoList.IsSynced { + fmt.Println("This list isn't currently syncing with ultralist.io. Please run `ultralist sync --setup` to set up syncing.") + return } var synchronizer *Synchronizer @@ -389,6 +329,96 @@ func (a *App) Sync(quiet bool) { } } +// SetupSync sets up a todolist to sync with ultralist.io. +func (a *App) SetupSync() { + backend := NewBackend() + if !backend.CredsFileExists() { + fmt.Println("You're not authenticated with ultralist.io yet. Please run `ultralist auth` first.") + return + } + + if a.TodoStore.LocalTodosFileExists() { + a.Load() + + if a.TodoList.IsSynced { + fmt.Println("This list is already sycned with ultralist.io. Use the --unsync flag to stop syncing this list.") + return + } + + prompt := promptui.Select{ + Label: "You have a todos list in this directory. What would you like to do?", + Items: []string{"Sync my list to ultralist.io", "Pull a list from ultralist.io, replacing the list that's here"}, + } + _, result, err := prompt.Run() + if err != nil { + return + } + if strings.HasPrefix(result, "Sync my list") { + prompt := promptui.Prompt{ + Label: "Give this list a name", + } + + result, err := prompt.Run() + if err != nil { + fmt.Println("A name is required to sync a list.") + return + } + a.EventLogger.CurrentSyncedList.Name = result + a.TodoList.IsSynced = true + a.EventLogger.WriteSyncedLists() + a.Sync(false) + return + } + } + // pull a list from ultralist.io + type Response struct { + Todolists []TodoList `json:"todolists"` + } + + var response *Response + + resp := backend.PerformRequest("GET", "/api/v1/todo_lists", []byte{}) + json.Unmarshal(resp, &response) + + var todolistNames []string + for _, todolist := range response.Todolists { + todolistNames = append(todolistNames, todolist.Name) + } + prompt2 := promptui.Select{ + Label: "Choose a list to import", + Items: todolistNames, + } + + idx, _, _ := prompt2.Run() + // a.EventLogger.CurrentSyncedList.Name = response.Todolists[idx].Name + // a.EventLogger.CurrentSyncedList.UUID = response.Todolists[idx].UUID + a.TodoList = &response.Todolists[idx] + a.TodoStore.Save(a.TodoList.Data) + + a.EventLogger = NewEventLogger(a.TodoList, a.TodoStore) + a.EventLogger.WriteSyncedLists() +} + +// Unsync stops a list from syncing with Ultralist.io. +func (a *App) Unsync() { + backend := NewBackend() + if !backend.CredsFileExists() { + fmt.Println("You're not authenticated with ultralist.io yet. Please run `ultralist auth` first.") + return + } + + a.Load() + + if !a.TodoList.IsSynced { + fmt.Println("This list isn't currently syncing with ultralist.io.") + return + } + + a.EventLogger.DeleteCurrentSyncedList() + a.EventLogger.WriteSyncedLists() + fmt.Println("This list will no longer sync with ultralist.io. To set up syncing again, run `ultralist sync --setup`.") +} + // CheckAuth is checking the authentication against ultralist.io. func (a *App) CheckAuth() { synchronizer := NewSynchronizer() diff --git a/ultralist/event_logger.go b/ultralist/event_logger.go index efc424d1..cdd97a2d 100644 --- a/ultralist/event_logger.go +++ b/ultralist/event_logger.go @@ -145,6 +145,17 @@ func (e *EventLogger) initializeSyncedList() { e.CurrentSyncedList = list } +// DeleteCurrentSyncedList - delete a synced list from the synced_lists.json file +func (e *EventLogger) DeleteCurrentSyncedList() { + var syncedListsWithoutDeleted []*SyncedList + for _, list := range e.SyncedLists { + if list.UUID != e.CurrentSyncedList.UUID { + syncedListsWithoutDeleted = append(syncedListsWithoutDeleted, list) + } + } + e.SyncedLists = syncedListsWithoutDeleted +} + // WriteSyncedLists is writing a synced list. func (e *EventLogger) WriteSyncedLists() { data, _ := json.Marshal(e.SyncedLists) diff --git a/ultralist/file_store.go b/ultralist/file_store.go index 4fe3dfc2..905ddb31 100644 --- a/ultralist/file_store.go +++ b/ultralist/file_store.go @@ -5,43 +5,48 @@ import ( "fmt" "io/ioutil" "os" + "path/filepath" ) +// TodosJSONFile is the filename to store todos in +const TodosJSONFile = ".todos.json" + // FileStore is the main struct of this file. type FileStore struct { - FileLocation string - Loaded bool + Loaded bool } // NewFileStore is creating a new file store. func NewFileStore() *FileStore { - return &FileStore{FileLocation: "", Loaded: false} + return &FileStore{Loaded: false} } // Initialize is initializing a new .todos.json file. func (f *FileStore) Initialize() { - if f.FileLocation == "" { - f.FileLocation = ".todos.json" - } - - _, err := ioutil.ReadFile(f.FileLocation) - if err == nil { + if f.LocalTodosFileExists() { fmt.Println("It looks like a .todos.json file already exists! Doing nothing.") os.Exit(0) } - if err := ioutil.WriteFile(f.FileLocation, []byte("[]"), 0644); err != nil { + if err := ioutil.WriteFile(TodosJSONFile, []byte("[]"), 0644); err != nil { fmt.Println("Error writing json file", err) os.Exit(1) } } -// Load is loading a .todos.json file. -func (f *FileStore) Load() ([]*Todo, error) { - if f.FileLocation == "" { - f.FileLocation = f.GetLocation() +// Returns if a local .todos.json file exists in the current dir. +func (f *FileStore) LocalTodosFileExists() bool { + dir, _ := os.Getwd() + localrepo := filepath.Join(dir, TodosJSONFile) + _, err := os.Stat(localrepo) + if err != nil { + return false } + return true +} - data, err := ioutil.ReadFile(f.FileLocation) +// Load is loading a .todos.json file, either from cwd, or the home directory +func (f *FileStore) Load() ([]*Todo, error) { + data, err := ioutil.ReadFile(f.GetLocation()) if err != nil { fmt.Println("No todo file found!") fmt.Println("Initialize a new todo repo by running 'ultralist init'") @@ -71,20 +76,15 @@ func (f *FileStore) Save(todos []*Todo) { } data, _ := json.Marshal(todos) - if err := ioutil.WriteFile(f.FileLocation, []byte(data), 0644); err != nil { + if err := ioutil.WriteFile(TodosJSONFile, []byte(data), 0644); err != nil { fmt.Println("Error writing json file", err) } } // GetLocation is returning the location of the .todos.json file. func (f *FileStore) GetLocation() string { - dir, _ := os.Getwd() - localrepo := fmt.Sprintf("%s/.todos.json", dir) - _, ferr := os.Stat(localrepo) - if ferr == nil { - return localrepo + if f.LocalTodosFileExists() { + return TodosJSONFile } - - home := UserHomeDir() - return fmt.Sprintf("%s/.todos.json", home) + return fmt.Sprintf("%s/%s", UserHomeDir(), TodosJSONFile) } diff --git a/ultralist/file_store_test.go b/ultralist/file_store_test.go index d9a9c593..1e5c2a9d 100644 --- a/ultralist/file_store_test.go +++ b/ultralist/file_store_test.go @@ -9,13 +9,12 @@ import ( func TestFileStore(t *testing.T) { assert := assert.New(t) list := SetUpTestMemoryTodoList() - testFilename := "TestFileStore_todos.json" - store := &FileStore{FileLocation: testFilename} - defer testFileCleanUp(testFilename) + store := &FileStore{} + defer testFileCleanUp() list.FindByID(2).Subject = "this is an non-fixture subject" store.Save(list.Todos()) - store1 := &FileStore{FileLocation: testFilename} + store1 := &FileStore{} todos, _ := store1.Load() assert.Equal(todos[1].Subject, "this is the first subject", "") diff --git a/ultralist/memory_store.go b/ultralist/memory_store.go index 91f17528..c728b01c 100644 --- a/ultralist/memory_store.go +++ b/ultralist/memory_store.go @@ -18,6 +18,10 @@ func (m *MemoryStore) Load() ([]*Todo, error) { return m.Todos, nil } +func (m *MemoryStore) LocalTodosFileExists() bool { + return false +} + // Save is saving todos to the memory store. func (m *MemoryStore) Save(todos []*Todo) { m.Todos = todos diff --git a/ultralist/store.go b/ultralist/store.go index caf6a9f7..e042e22d 100644 --- a/ultralist/store.go +++ b/ultralist/store.go @@ -3,6 +3,7 @@ package ultralist // Store is the interface for ultralist todos. type Store interface { GetLocation() string + LocalTodosFileExists() bool Initialize() Load() ([]*Todo, error) Save(todos []*Todo) diff --git a/ultralist/todo_test.go b/ultralist/todo_test.go index e2283d15..e3b82012 100644 --- a/ultralist/todo_test.go +++ b/ultralist/todo_test.go @@ -50,8 +50,8 @@ func SetUpTestMemoryTodoList() *TodoList { return list } -func testFileCleanUp(filename string) { - var err = os.Remove(filename) +func testFileCleanUp() { + var err = os.Remove(TodosJSONFile) if err != nil { panic(err) }