gitlab.com/pidrakin/dotfiles-cli@v1.7.5/cmd/link/link.go (about) 1 package link 2 3 import ( 4 "fmt" 5 "os" 6 "path" 7 "path/filepath" 8 "strings" 9 "time" 10 11 "github.com/fatih/color" 12 "github.com/mitchellh/go-homedir" 13 log "github.com/sirupsen/logrus" 14 "gitlab.com/pidrakin/dotfiles-cli/cmd/common" 15 "gitlab.com/pidrakin/dotfiles-cli/common/logging" 16 "gitlab.com/pidrakin/dotfiles-cli/config" 17 "gitlab.com/pidrakin/dotfiles-cli/text" 18 "gitlab.com/pidrakin/go/interactive" 19 "gitlab.com/pidrakin/go/slices" 20 sysfs2 "gitlab.com/pidrakin/go/sysfs" 21 ) 22 23 type stats struct { 24 added int 25 deleted int 26 ignored int 27 modified int 28 backedUp int 29 createdDirs int 30 hooksSucceeded int 31 hooksFailed int 32 } 33 34 type Linker struct { 35 diffs []common.Diff 36 configManager *config.Manager 37 stateConfig *config.StateConfig 38 collection string 39 interactive bool 40 backup bool 41 force bool 42 runHooks *bool 43 stats *stats 44 } 45 46 func Run(interactive bool, backup bool, force bool, runHooks *bool, collection string) error { 47 l := Linker{ 48 interactive: interactive, 49 backup: backup, 50 force: force, 51 collection: collection, 52 runHooks: runHooks, 53 } 54 return l.Link() 55 } 56 57 func (linker *Linker) Link() (err error) { 58 if err = linker.setup(); err != nil { 59 return 60 } 61 if err = linker.handleHookDiffs(hook.PRE); err != nil { 62 return 63 } 64 if err = linker.handleOldLinks(); err != nil { 65 return 66 } 67 if err = linker.handleDiffs(); err != nil { 68 return 69 } 70 if err = linker.handleHookDiffs(hook.POST); err != nil { 71 return 72 } 73 if err = linker.stateConfig.Save(); err != nil { 74 return 75 } 76 77 if err = logging.Successfully("linked"); err != nil { 78 return 79 } 80 if err = linker.outputStats(); err != nil { 81 return 82 } 83 84 return 85 } 86 87 func (linker *Linker) setup() (err error) { 88 if err = linker.interactiveMethodPrompt(); err != nil { 89 return 90 } 91 92 differ := common.Differ{} 93 linker.diffs, err = differ.Diff(linker.collection) 94 if err != nil { 95 return 96 } 97 98 linker.configManager = differ.ConfigManager 99 linker.stateConfig = differ.ConfigManager.StateConfig 100 101 if linker.runHooks == nil { 102 runHooks := linker.stateConfig.Linked == nil 103 linker.runHooks = &runHooks 104 } 105 106 linker.stateConfig.Linked = config.NewLinked() 107 108 linker.stats = &stats{} 109 110 return 111 } 112 113 func (linker *Linker) interactiveMethodPrompt() error { 114 115 prompter, err := interactive.NewPrompter(interactive.SetInteractive(linker.interactive)) 116 if err != nil { 117 return err 118 } 119 120 if !prompter.Interactive() && linker.backup && linker.force { 121 prompter.SetInteractive(true) 122 } 123 124 methodOptions := []string{ 125 fmt.Sprintf("skip %s", "skip target if exists"), 126 fmt.Sprintf("backup %s", "backup target if exists"), 127 fmt.Sprintf("force %s", "overwrite target even if exists"), 128 } 129 130 i, _, err := prompter.SingleChoice( 131 methodOptions, 132 "How do you want to handle already existing target files?", 133 ) 134 135 if err != nil || i == -1 { 136 return nil 137 } 138 linker.backup = methodOptions[i] == "backup" 139 linker.force = methodOptions[i] == "force" 140 141 return nil 142 } 143 144 func (linker *Linker) runHook(basepath string, hooktype HookType, collection string) error { 145 found, succeeded, err := runHook(basepath, hooktype, collection) 146 if found { 147 if succeeded { 148 linker.stats.hooksSucceeded += 1 149 } else { 150 linker.stats.hooksFailed += 1 151 } 152 } 153 return err 154 } 155 156 func (linker *Linker) handleHookDiffs(hookType HookType) (err error) { 157 if !*linker.runHooks { 158 return 159 } 160 161 if _, err = fmt.Fprintf(os.Stdout, "running %s hooks...\n", hookType); err != nil { 162 return 163 } 164 165 start := time.Now() 166 167 basepath := strings.TrimSuffix(linker.diffs[0].Root, "/"+linker.diffs[0].Collection) 168 169 if len(linker.diffs) > 0 { 170 if err = linker.runHook(basepath, hookType, ""); err != nil { 171 return 172 } 173 if linker.stateConfig.Collection != "" { 174 if err = linker.runHook(basepath, hookType, linker.stateConfig.Collection); err != nil { 175 return 176 } 177 } 178 } 179 180 for _, diff2 := range linker.diffs { 181 if err = linker.handleHookDiff(hookType, basepath, diff2); err != nil { 182 return 183 } 184 } 185 186 elapsed := time.Since(start) 187 elapsedString := color.New(color.FgGreen, color.Bold).Sprintf("%d", int(elapsed.Seconds())) 188 if _, err = fmt.Fprintf(os.Stdout, "hooks took %s seconds\n\n", elapsedString); err != nil { 189 return 190 } 191 192 return 193 } 194 195 func (linker *Linker) handleHookDiff(hookType HookType, basepath string, diff common.Diff) (err error) { 196 if diff.Collection != "" { 197 if err = linker.runHook(basepath, hookType, diff.Collection); err != nil { 198 return 199 } 200 } 201 return 202 } 203 204 func (linker *Linker) handleOldLinks() (err error) { 205 var linkList []string 206 homeDir, err := homedir.Dir() 207 if err != nil { 208 return 209 } 210 211 previousStateConfig := linker.configManager.PreviousStateConfig 212 if previousStateConfig != nil && previousStateConfig.Linked != nil { 213 if previousStateConfig.Linked.IsPackages() { 214 for _, list := range previousStateConfig.Linked.Packages { 215 for _, file := range list { 216 linkPath := filepath.Join(homeDir, file) 217 if _, err := os.Stat(linkPath); !os.IsNotExist(err) { 218 linkList = append(linkList, file) 219 } 220 } 221 } 222 } else { 223 for _, file := range previousStateConfig.Linked.Files { 224 linkPath := filepath.Join(homeDir, file) 225 if _, err := os.Stat(linkPath); !os.IsNotExist(err) { 226 linkList = append(linkList, file) 227 } 228 } 229 } 230 return linker.deleteStaleLinks(homeDir, linkList) 231 } 232 return nil 233 } 234 235 func (linker *Linker) handleDiffs() (err error) { 236 for _, diff2 := range linker.diffs { 237 if err = linker.handleDiff(diff2); err != nil { 238 return 239 } 240 } 241 return 242 } 243 244 func (linker *Linker) handleDiff(diff common.Diff) (err error) { 245 homeDir, err := homedir.Dir() 246 if err != nil { 247 return 248 } 249 250 linker.stats.ignored += len(diff.ToIgnore) 251 252 if err = linker.deleteStaleLinks(homeDir, diff.ToDelete); err != nil { 253 return 254 } 255 256 filesToAdd := append(diff.ToAdd, diff.ToModify...) 257 if err = linker.link(diff.Root, homeDir, filesToAdd); err != nil { 258 return 259 } 260 pkg, ioErr := filepath.Rel(linker.stateConfig.GetContextPath(), diff.Root) 261 if ioErr != nil { 262 return ioErr 263 } 264 if pkg == "." { 265 pkg = "" 266 } 267 paths := append(diff.ToIgnore, filesToAdd...) 268 if len(paths) > 0 { 269 if err = linker.stateConfig.Linked.Set(paths, pkg); err != nil { 270 return 271 } 272 } 273 274 return 275 } 276 277 func (linker *Linker) deleteStaleLinks(linkRoot string, linkList []string) error { 278 for _, linkPath := range linkList { 279 if err := sysfs2.DeleteSymlink(linkRoot, linkPath); err != nil { 280 return err 281 } 282 linker.stats.deleted += len(linkList) 283 } 284 return nil 285 } 286 287 func (linker *Linker) link(root string, linkRoot string, filesFound []string) (err error) { 288 date := time.Now().Format(time.RFC3339) 289 backupDir := path.Join(linkRoot, fmt.Sprintf("dotfiles-backup-%s", date)) 290 291 for _, rel := range filesFound { 292 source := filepath.Join(root, rel) 293 target := filepath.Join(linkRoot, rel) 294 295 if sysfs2.FileExists(target) { 296 linkTarget, _ := sysfs2.ReadLink(target) 297 if linkTarget == source { 298 return nil 299 } else if linker.backup { 300 log.WithField("backupDir", backupDir).Debug(text.EnsureFileExists) 301 if !sysfs2.DryRun { 302 if err = os.MkdirAll(backupDir, os.FileMode(0755)); err != nil { 303 return err 304 } 305 if err = os.Rename(target, path.Join(backupDir, rel)); err != nil { 306 return err 307 } 308 } 309 log.WithField("file", target).Debug(text.MoveToBackup) 310 linker.stats.backedUp += 1 311 } else if linker.force { 312 if err = sysfs2.DeleteSymlink(linkRoot, rel); err != nil { 313 return err 314 } 315 } 316 linker.stats.modified += 1 317 } else { 318 dir := filepath.Dir(target) 319 if !sysfs2.DirExists(dir) { 320 log.WithField("dir", dir).Debug(text.EnsureFileExists) 321 if !sysfs2.DryRun { 322 if err = os.MkdirAll(dir, os.FileMode(0755)); err != nil { 323 return err 324 } 325 } 326 linker.stats.createdDirs += 1 327 } 328 } 329 if err = sysfs2.Symlink(source, target, nil); err != nil { 330 return 331 } 332 linker.stats.added += 1 333 } 334 return nil 335 } 336 337 func (linker *Linker) outputStats() error { 338 if linker.stateConfig.Collection != "" { 339 if _, err := fmt.Fprintf(os.Stdout, "\ncollection [%s]\n", linker.stateConfig.Collection); err != nil { 340 return err 341 } 342 } 343 344 sums := []int{ 345 linker.stats.added, 346 linker.stats.deleted, 347 linker.stats.ignored, 348 linker.stats.modified, 349 linker.stats.backedUp, 350 linker.stats.createdDirs, 351 linker.stats.hooksSucceeded, 352 linker.stats.hooksFailed, 353 } 354 355 if slices.Sum(sums) > 0 { 356 if _, err := fmt.Fprintf(os.Stdout, "\n"); err != nil { 357 return err 358 } 359 } 360 361 if err := outputStat(sums[0], "added file links", color.FgGreen, color.Bold); err != nil { 362 return err 363 } 364 if err := outputStat(sums[1], "deleted file link", color.FgRed, color.Bold); err != nil { 365 return err 366 } 367 if err := outputStat(sums[2], "ignored file links", color.FgYellow, color.Bold); err != nil { 368 return err 369 } 370 if err := outputStat(sums[3], "modified file links", color.FgBlue, color.Bold); err != nil { 371 return err 372 } 373 if err := outputStat(sums[4], "backed upped file links", color.FgWhite, color.Bold); err != nil { 374 return err 375 } 376 if err := outputStat(sums[5], "dirs created", color.FgGreen, color.Bold); err != nil { 377 return err 378 } 379 if err := outputStat(sums[6], "hooks succeeded", color.FgGreen, color.Bold); err != nil { 380 return err 381 } 382 if err := outputStat(sums[7], "hooks failed", color.FgRed, color.Bold); err != nil { 383 return err 384 } 385 386 return nil 387 } 388 389 func outputStat(stat int, mod string, attrs ...color.Attribute) error { 390 if stat == 0 { 391 return nil 392 } 393 colored := color.New(attrs...).Sprintf("%d", stat) 394 _, err := fmt.Fprintf(os.Stdout, "%s %s\n", colored, mod) 395 return err 396 }