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  }