github.com/knoebber/dotfile@v1.0.6/local/local.go (about) 1 // Package local tracks files by writing to JSON files in the dotfile directory. 2 // 3 // For every new file that is tracked a new .json file is created. 4 // For each commit on a tracked file, a new file is created with the same name as the hash. 5 // 6 // Example: ~/.emacs.d/init.el is added with alias "emacs". 7 // Supposing Storage.dir is ~/.config/dotfile, then the following files are created: 8 // 9 // ~/.config/dotfile/emacs.json 10 // ~/.config/dotfile/emacs/8f94c7720a648af9cf9dab33e7f297d28b8bf7cd 11 // 12 // The emacs.json file would look something like this: 13 // 14 // { 15 // "path": "~/.emacs.d/init.el", 16 // "revision": "8f94c7720a648af9cf9dab33e7f297d28b8bf7cd" 17 // "commits": [{ 18 // "hash": "8f94c7720a648af9cf9dab33e7f297d28b8bf7cd", 19 // "timestamp": 1558896290, 20 // "message": "Initial commit" 21 // }] 22 // } 23 package local 24 25 import ( 26 "fmt" 27 "os" 28 "path/filepath" 29 "strings" 30 31 "github.com/knoebber/dotfile/dotfile" 32 "github.com/pkg/errors" 33 ) 34 35 const jsonIndent = " " 36 37 // Creates a path that is reusable between machines. 38 // Returns an error when path does not exist. 39 func convertPath(path string) (string, error) { 40 var err error 41 42 home, err := os.UserHomeDir() 43 if err != nil { 44 return "", err 45 } 46 47 if !exists(path) { 48 return "", fmt.Errorf("%q not found", path) 49 } 50 51 // the full path. 52 if !filepath.IsAbs(path) { 53 path, err = filepath.Abs(path) 54 if err != nil { 55 return "", err 56 } 57 } 58 59 // If the path is not in $HOME then use as is. 60 if !strings.Contains(path, home) { 61 return path, nil 62 } 63 64 return strings.Replace(path, home, "~", 1), nil 65 } 66 67 // Returns whether the file or directory exists. 68 func exists(path string) bool { 69 _, err := os.Stat(path) 70 if os.IsNotExist(err) { 71 return false 72 } 73 74 return true 75 } 76 77 // Creates a directory if it does not exist. 78 func createDir(dir string) error { 79 if exists(dir) { 80 return nil 81 } 82 83 return os.Mkdir(dir, 0755) 84 } 85 86 func writeCommit(contents []byte, storageDir string, alias, hash string) error { 87 // The directory for the files commits. 88 commitDir := filepath.Join(storageDir, alias) 89 90 // Example: ~/.local/share/dotfile/bash_profile 91 if err := createDir(commitDir); err != nil { 92 return errors.Wrap(err, "creating directory for revision") 93 } 94 95 // Example: ~/.local/share/dotfile/bash_profile/8f94c7720a648af9cf9dab33e7f297d28b8bf7cd 96 commitPath := filepath.Join(commitDir, hash) 97 98 if err := os.WriteFile(commitPath, contents, 0644); err != nil { 99 return errors.Wrap(err, "writing revision") 100 } 101 102 return nil 103 } 104 105 // DefaultStorageDir returns the default location for storing dotfile information. 106 // Creates the location when it does not exist. 107 func DefaultStorageDir() (storageDir string, err error) { 108 home, err := os.UserHomeDir() 109 if err != nil { 110 return "", err 111 } 112 113 localSharePath := filepath.Join(home, ".local/share/") 114 if exists(localSharePath) { 115 // Priority one : ~/.local/share/dotfile 116 storageDir = filepath.Join(localSharePath, "dotfile/") 117 } else { 118 // Priority two: ~/.dotfile/ 119 storageDir = filepath.Join(home, ".dotfile/") 120 } 121 122 if err = createDir(storageDir); err != nil { 123 return 124 } 125 126 return 127 } 128 129 // InitializeFile sets up a new file to be tracked. 130 // When alias is empty its generated from path. 131 // Returns a Storage that is loaded with the new file. 132 func InitializeFile(storageDir, path, alias string) (*Storage, error) { 133 var err error 134 135 alias, err = dotfile.Alias(alias, path) 136 if err != nil { 137 return nil, err 138 } 139 140 s := &Storage{Dir: storageDir, Alias: alias} 141 if s.hasSavedData() { 142 return nil, fmt.Errorf("%q is already tracked", alias) 143 } 144 145 s.FileData = new(dotfile.TrackingData) 146 s.FileData.Path, err = convertPath(path) 147 if err != nil { 148 return nil, err 149 } 150 151 if err := dotfile.Init(s, s.FileData.Path, s.Alias); err != nil { 152 return nil, err 153 } 154 155 return s, nil 156 } 157 158 func listAliases(storageDir string) ([]string, error) { 159 files, err := filepath.Glob(filepath.Join(storageDir, "*.json")) 160 if err != nil { 161 return nil, err 162 } 163 164 aliases := make([]string, len(files)) 165 for i, filename := range files { 166 aliases[i] = strings.TrimSuffix(filepath.Base(filename), ".json") 167 } 168 169 return aliases, nil 170 } 171 172 // ListAliases returns a funtion that lists all aliases in the storage directory. 173 func ListAliases(storageDir string) func() []string { 174 return func() []string { 175 aliases, err := listAliases(storageDir) 176 if err != nil { 177 return nil 178 } 179 180 return aliases 181 } 182 } 183 184 // List returns a slice of aliases for all locally tracked files. 185 // When the file has uncommitted changes an asterisks is added to the end. 186 func List(storageDir string, path bool) ([]string, error) { 187 aliases, err := listAliases(storageDir) 188 if err != nil { 189 return nil, err 190 } 191 192 result := make([]string, len(aliases)) 193 194 s := &Storage{Dir: storageDir} 195 s.FileData = new(dotfile.TrackingData) 196 197 for i, alias := range aliases { 198 s.Alias = alias 199 200 if err := s.SetTrackingData(); err != nil { 201 return nil, err 202 } 203 204 fullPath, err := s.Path() 205 if err != nil { 206 return nil, err 207 } 208 209 if !exists(fullPath) { 210 alias += " - removed" 211 } else { 212 clean, err := dotfile.IsClean(s, s.FileData.Revision) 213 if err != nil { 214 return nil, err 215 } 216 217 if !clean { 218 alias += "*" 219 } 220 } 221 222 result[i] = alias 223 if path { 224 result[i] += " " + s.FileData.Path 225 } 226 } 227 228 return result, nil 229 } 230 231 func createDirectories(path string) error { 232 if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { 233 return errors.Wrapf(err, "creating %q", filepath.Dir(path)) 234 } 235 236 return nil 237 }