mirror of https://github.com/Marcusk19/dotctl
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
156 lines
4.4 KiB
Go
156 lines
4.4 KiB
Go
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
// LinkResult holds counts from a link operation
|
|
type LinkResult struct {
|
|
Linked int
|
|
Skipped int
|
|
Backed int
|
|
}
|
|
|
|
func init() {
|
|
RootCmd.AddCommand(linkCommand)
|
|
}
|
|
|
|
var linkCommand = &cobra.Command{
|
|
Use: "link",
|
|
Run: runLinkCommand,
|
|
Short: "generate symlinks according to config",
|
|
Long: "runs through all configs in the dotctl config file and links them to configured symlinks",
|
|
}
|
|
|
|
func runLinkCommand(cmd *cobra.Command, args []string) {
|
|
fmt.Fprintln(cmd.OutOrStdout(), "Symlinking dotfiles...")
|
|
dotfileRoot := viper.GetString("dotfile-path")
|
|
links := viper.GetStringMapString("links")
|
|
|
|
result := LinkDotfiles(cmd.OutOrStdout(), dotfileRoot, links, Overwrite, NoBackup, DryRun)
|
|
|
|
fmt.Fprintf(cmd.OutOrStdout(), "\nSummary: %d linked, %d skipped, %d backed up\n",
|
|
result.Linked, result.Skipped, result.Backed)
|
|
}
|
|
|
|
// LinkDotfiles performs idempotent symlinking of dotfiles.
|
|
// dotfileRoot: path to the dotfiles repo (e.g. ~/dotfiles)
|
|
// links: map of config name -> target path (from config.yml)
|
|
// overwrite: if true, replaces existing wrong symlinks
|
|
// noBackup: if true, skips conflicts instead of backing up real files
|
|
// dryRun: if true, prints actions without making filesystem changes
|
|
func LinkDotfiles(out io.Writer, dotfileRoot string, links map[string]string, overwrite, noBackup, dryRun bool) LinkResult {
|
|
result := LinkResult{}
|
|
|
|
for configName, targetPath := range links {
|
|
if configName == ".git" || configName == "dotctl" {
|
|
continue
|
|
}
|
|
|
|
dotPath := filepath.Join(dotfileRoot, configName)
|
|
|
|
if targetPath == "" {
|
|
fmt.Fprintf(out, " Warning: no target path configured for %s, skipping\n", configName)
|
|
result.Skipped++
|
|
continue
|
|
}
|
|
|
|
// Ensure parent directory exists
|
|
parentDir := filepath.Dir(targetPath)
|
|
if !dryRun {
|
|
if err := os.MkdirAll(parentDir, 0755); err != nil {
|
|
log.Printf("Warning: could not create parent dir %s: %v\n", parentDir, err)
|
|
}
|
|
}
|
|
|
|
// Check what's at the target path using Lstat (doesn't follow symlinks)
|
|
lstat, err := os.Lstat(targetPath)
|
|
|
|
if os.IsNotExist(err) {
|
|
// Target missing - create symlink
|
|
if dryRun {
|
|
fmt.Fprintf(out, " %s → %s [would link]\n", targetPath, dotPath)
|
|
} else {
|
|
if err := os.Symlink(dotPath, targetPath); err != nil {
|
|
log.Printf("Error: cannot symlink %s → %s: %v\n", targetPath, dotPath, err)
|
|
result.Skipped++
|
|
continue
|
|
}
|
|
fmt.Fprintf(out, " %s → %s [linked]\n", targetPath, dotPath)
|
|
}
|
|
result.Linked++
|
|
continue
|
|
}
|
|
|
|
if err != nil {
|
|
log.Printf("Warning: cannot stat %s: %v\n", targetPath, err)
|
|
result.Skipped++
|
|
continue
|
|
}
|
|
|
|
// Check if it's a symlink
|
|
if lstat.Mode()&os.ModeSymlink != 0 {
|
|
existing, err := os.Readlink(targetPath)
|
|
if err == nil && existing == dotPath {
|
|
// Already pointing to the right place
|
|
fmt.Fprintf(out, " %s → %s [already linked]\n", targetPath, dotPath)
|
|
result.Skipped++
|
|
continue
|
|
}
|
|
// Symlink to wrong place
|
|
if overwrite {
|
|
if dryRun {
|
|
fmt.Fprintf(out, " %s [would remove wrong symlink and relink → %s]\n", targetPath, dotPath)
|
|
} else {
|
|
os.Remove(targetPath)
|
|
if err := os.Symlink(dotPath, targetPath); err != nil {
|
|
log.Printf("Error: cannot relink %s: %v\n", targetPath, err)
|
|
result.Skipped++
|
|
continue
|
|
}
|
|
fmt.Fprintf(out, " %s → %s [relinked]\n", targetPath, dotPath)
|
|
}
|
|
result.Linked++
|
|
} else {
|
|
fmt.Fprintf(out, " %s [symlink to wrong path, skipping (use --overwrite to force)]\n", targetPath)
|
|
result.Skipped++
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Real file or directory exists at target
|
|
if noBackup {
|
|
fmt.Fprintf(out, " %s [real file exists, skipping (use --overwrite or remove --no-backup)]\n", targetPath)
|
|
result.Skipped++
|
|
continue
|
|
}
|
|
|
|
backupPath := targetPath + ".dotctl.bak"
|
|
if dryRun {
|
|
fmt.Fprintf(out, " %s [would back up to %s and link → %s]\n", targetPath, backupPath, dotPath)
|
|
} else {
|
|
if err := os.Rename(targetPath, backupPath); err != nil {
|
|
log.Printf("Error: cannot back up %s: %v\n", targetPath, err)
|
|
result.Skipped++
|
|
continue
|
|
}
|
|
if err := os.Symlink(dotPath, targetPath); err != nil {
|
|
log.Printf("Error: cannot symlink after backup %s: %v\n", targetPath, err)
|
|
result.Skipped++
|
|
continue
|
|
}
|
|
fmt.Fprintf(out, " %s → %s [backed up to .dotctl.bak, linked]\n", targetPath, dotPath)
|
|
}
|
|
result.Backed++
|
|
}
|
|
|
|
return result
|
|
}
|