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.
100 lines
3.0 KiB
Go
100 lines
3.0 KiB
Go
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/go-git/go-git/v5"
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
func init() {
|
|
RootCmd.AddCommand(applyCommand)
|
|
}
|
|
|
|
var applyCommand = &cobra.Command{
|
|
Use: "apply <repo-url>",
|
|
Short: "Clone a dotfiles repo and link all tracked configs",
|
|
Long: `apply clones a dotfiles repository (or pulls if it already exists),
|
|
reads the dotctl config, and creates symlinks for all tracked configs.
|
|
|
|
Example:
|
|
dotctl apply https://github.com/user/dotfiles.git`,
|
|
Args: cobra.ExactArgs(1),
|
|
Run: runApplyCommand,
|
|
}
|
|
|
|
func runApplyCommand(cmd *cobra.Command, args []string) {
|
|
repoURL := args[0]
|
|
dotfilePath := viper.GetString("dotfile-path")
|
|
// strip trailing slash for consistency
|
|
dotfilePath = filepath.Clean(dotfilePath)
|
|
|
|
// Step 1: Clone or pull
|
|
stat, err := os.Stat(dotfilePath)
|
|
if os.IsNotExist(err) {
|
|
fmt.Fprintf(cmd.OutOrStdout(), "Cloning %s into %s...\n", repoURL, dotfilePath)
|
|
if !DryRun {
|
|
_, err = git.PlainClone(dotfilePath, false, &git.CloneOptions{
|
|
URL: repoURL,
|
|
Progress: cmd.OutOrStdout(),
|
|
})
|
|
if err != nil {
|
|
fmt.Fprintf(cmd.ErrOrStderr(), "Error: clone failed: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
} else if err != nil {
|
|
fmt.Fprintf(cmd.ErrOrStderr(), "Error: cannot stat %s: %v\n", dotfilePath, err)
|
|
os.Exit(1)
|
|
} else if stat.IsDir() {
|
|
// Check if it's a git repo
|
|
repo, err := git.PlainOpen(dotfilePath)
|
|
if err != nil {
|
|
fmt.Fprintf(cmd.ErrOrStderr(), "Error: %s exists but is not a git repository\n", dotfilePath)
|
|
fmt.Fprintf(cmd.ErrOrStderr(), "Remove it or use a different --dotfile-path\n")
|
|
os.Exit(1)
|
|
}
|
|
fmt.Fprintf(cmd.OutOrStdout(), "Pulling latest changes in %s...\n", dotfilePath)
|
|
if !DryRun {
|
|
w, err := repo.Worktree()
|
|
if err != nil {
|
|
fmt.Fprintf(cmd.ErrOrStderr(), "Error: cannot get worktree: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
err = w.Pull(&git.PullOptions{RemoteName: "origin"})
|
|
if err != nil && err != git.NoErrAlreadyUpToDate {
|
|
fmt.Fprintf(cmd.OutOrStdout(), "Warning: git pull failed (%v), continuing with local state\n", err)
|
|
} else if err == git.NoErrAlreadyUpToDate {
|
|
fmt.Fprintln(cmd.OutOrStdout(), "Already up to date.")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Step 2: Read config
|
|
configPath := filepath.Join(dotfilePath, "dotctl", "config.yml")
|
|
v := viper.New()
|
|
v.SetConfigFile(configPath)
|
|
if err := v.ReadInConfig(); err != nil {
|
|
fmt.Fprintf(cmd.ErrOrStderr(), "Error: cannot read config at %s: %v\n", configPath, err)
|
|
fmt.Fprintln(cmd.ErrOrStderr(), "Is this repo set up with dotctl? Run 'dotctl init' first.")
|
|
os.Exit(1)
|
|
}
|
|
links := v.GetStringMapString("links")
|
|
|
|
if len(links) == 0 {
|
|
fmt.Fprintln(cmd.OutOrStdout(), "No links configured in config.yml — nothing to link.")
|
|
return
|
|
}
|
|
|
|
// Step 3: Run idempotent link
|
|
fmt.Fprintln(cmd.OutOrStdout(), "Linking dotfiles...")
|
|
result := LinkDotfiles(cmd.OutOrStdout(), dotfilePath, links, Overwrite, NoBackup, DryRun)
|
|
|
|
// Step 4: Print summary
|
|
fmt.Fprintf(cmd.OutOrStdout(), "\nDone! %d linked, %d skipped, %d backed up\n",
|
|
result.Linked, result.Skipped, result.Backed)
|
|
}
|