Skip to content

Commit

Permalink
Update the sync command (#211)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
gammons and Grant Ammons authored Aug 16, 2020
1 parent e817184 commit ce9aa24
Show file tree
Hide file tree
Showing 8 changed files with 176 additions and 106 deletions.
39 changes: 32 additions & 7 deletions cmd/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
`
Expand All @@ -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)
}
168 changes: 99 additions & 69 deletions ultralist/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand Down
11 changes: 11 additions & 0 deletions ultralist/event_logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
48 changes: 24 additions & 24 deletions ultralist/file_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -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'")
Expand Down Expand Up @@ -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)
}
7 changes: 3 additions & 4 deletions ultralist/file_store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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", "")
Expand Down
4 changes: 4 additions & 0 deletions ultralist/memory_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions ultralist/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading

0 comments on commit ce9aa24

Please sign in to comment.