Compare commits

..

No commits in common. 'main' and 'v0.1.0' have entirely different histories.
main ... v0.1.0

@ -1,5 +0,0 @@
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.2
hooks:
- id: gitleaks

@ -1,11 +1,11 @@
unit-test: clean:
TESTING=true go test -v ./test
rm -rf test/dotctl_test 2> /dev/null rm -rf test/dotctl_test 2> /dev/null
rm -rf tmp 2> /dev/null
install: sandbox:
go build mkdir -p ./tmp/ 2> /dev/null
cp dotctl /usr/local/bin cp -r ~/.config/ ./tmp/config 2> /dev/null
pre-commit-hooks: unit-test:
pre-commit autoupdate TESTING=true go test -v ./test
pre-commit install rm -rf test/dotctl_test 2> /dev/null

@ -1,25 +1,13 @@
# Dotctl # Dotctl
dotfile management A cli tool to manage your dotfiles
## About ## About
Dotctl is a tool to help you easily manage your dotfiles and sync them across separate machines using Dotctl is a tool to help you easily manage your dotfiles and sync them across separate machines using
git. It creates a `dotfiles` subdirectory in the user's `$HOME` and provides simple commands to add git. It aims to abstract away the manual effort of symlinking your dotfiles to config directories and
and symlink config files/directories to the central `dotfiles` directory. updating them with git.
## Installation ## Installation
- TBD
### Build From Source
_Prerequisites_
- [go](https://go.dev/doc/install)
clone the repo and run script to build binary and copy it to your path
```sh
git clone https://github.com/Marcusk19/dotctl.git
cd dotctl
make install
```
## Usage ## Usage
@ -30,19 +18,20 @@ dotctl init
dotctl add ~/.config/nvim dotctl add ~/.config/nvim
# create symlinks # create symlinks
dotctl link dotctl link
# sync changes
dotctl sync -r <your-git-repo>
``` ```
### Syncing to git
_Warning: using the sync command can have some unexpected behavior, currently the recommendation
is to manually track the dotfiles with git_
dotctl comes with a `sync` command that performs the following operations for the dotfiles directory: ## Development
It's preferable to create a temporary directory and copy your system's config
directory over to avoid making undesirable changes to your system.
A couple of useful makefile scripts exist to set up and tear down this.
It will create a testing directory in `./tmp/config` and copy your system configs
over.
1. pulls changes from configured upstream git repo ```bash
2. commits and pushes any changes detected in the dotfile repo make sandbox # creates the directory and copies over from ~/.config
make clean # removes directory
```
set the upstream repo using the `-r` flag or manually edit the config at `$HOME/dotfiles/dotctl/config.yaml`
example usage:
```
dotctl sync -r https://github.com/example/dotfiles.git
```

@ -3,122 +3,72 @@ package cmd
import ( import (
"fmt" "fmt"
"log" "log"
"os"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/Marcusk19/dotctl/tools" "github.com/Marcusk19/dotctl/tools"
"github.com/manifoldco/promptui" "github.com/manifoldco/promptui"
"github.com/spf13/afero"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
func init() { func init() {
addCommand.Flags().BoolVar(&absolutePath, "absolute", false, "absolute path of config") RootCmd.AddCommand(addCommand)
RootCmd.AddCommand(addCommand)
} }
var addCommand = &cobra.Command{ var addCommand = &cobra.Command {
Use: "add", Use: "add",
Short: "Adds config to be tracked by dotctl", Short: "Adds config to be tracked by dotctl",
Long: "will copy files passed as argument to the dotfiles directory and symlink them", // TODO add more description Long: "TODO: add longer description", // TODO add more description
Run: runAddCommand, Run: runAddCommand,
} }
var absolutePath bool
func runAddCommand(cmd *cobra.Command, args []string) { func runAddCommand(cmd *cobra.Command, args []string) {
fs := FileSystem fs := FileSystem
testing := viper.GetBool("testing") if len(args) <= 0 {
fmt.Println("ERROR: requires at least one argument")
if len(args) <= 0 { return
fmt.Println("ERROR: requires config path") }
return
} configSrc := args[0]
dirs := strings.Split(configSrc, "/")
configSrc := args[0] name := dirs[len(dirs) - 1] // take the last section of the path, this should be the name
if !absolutePath { links := viper.GetStringMap("links")
cwd, _ := os.Getwd() links[name] = configSrc
configSrc = cwd + "/" + configSrc viper.Set("links", links)
} err := viper.WriteConfig()
if err != nil {
dirs := strings.Split(configSrc, "/") fmt.Printf("Problem updating dotctl config %s", err)
name := dirs[len(dirs)-1] // take the last section of the path, this should be the name }
if name[0] == '.' {
name = name[1:] dotfilePath := viper.Get("dotfile-path").(string)
}
dotfileDest := filepath.Join(dotfilePath, name)
links := viper.GetStringMap("links")
links[name] = configSrc if DryRun {
viper.Set("links", links) fmt.Printf("Will copy %s -> %s \n", configSrc, dotfileDest)
return
dotfilePath := viper.Get("dotfile-path").(string) }
dotfileDest := filepath.Join(dotfilePath, name) _, err = fs.Stat(dotfileDest)
if err == nil {
if DryRun { fmt.Printf("Looks like %s exists in current dotfile directory\n", dotfileDest)
fmt.Printf("Will copy %s -> %s \n", configSrc, dotfileDest) fmt.Println("Do you want to overwrite it?")
return confirm := promptui.Prompt{
} Label: "overwrite config",
IsConfirm: true,
_, err := fs.Stat(dotfileDest) }
if err == nil { overwrite, _ := confirm.Run()
fmt.Printf("Looks like %s exists in current dotfile directory\n", dotfileDest) if strings.ToUpper(overwrite) != "Y" {
fmt.Printf("Do you want to overwrite it with what is in %s?\n", configSrc) return
confirm := promptui.Prompt{ }
Label: "overwrite config", }
IsConfirm: true,
} err = tools.CopyDir(fs, configSrc, dotfileDest)
overwrite, _ := confirm.Run() if err != nil {
if strings.ToUpper(overwrite) == "Y" { log.Fatal(err)
addConfigToDir(fs, configSrc, dotfileDest) }
} fmt.Printf("Copied %s -> %s\n", configSrc, dotfileDest)
} else {
addConfigToDir(fs, configSrc, dotfileDest)
}
if !DryRun {
// symlink the copied dotfile destination back to the config src
fs.RemoveAll(configSrc)
linkPaths(dotfileDest, configSrc)
} else {
fmt.Println("Files were not symlinked")
}
if !testing {
// write to the config to persist changes
err := viper.WriteConfig()
if err != nil {
fmt.Printf("Problem updating dotctl config %s", err)
}
}
}
func addConfigToDir(fs afero.Fs, configSrc, dotfileDest string) {
configFile, err := fs.Open(configSrc)
if err != nil {
log.Fatal(err)
}
defer configFile.Close()
fileInfo, err := configFile.Stat()
if err != nil {
log.Fatal(err)
}
if fileInfo.IsDir() {
err = tools.CopyDir(fs, configSrc, dotfileDest)
} else {
err = tools.CopyFile(fs, configSrc, dotfileDest)
}
if err != nil {
log.Fatal(err)
}
fmt.Printf("Copied %s -> %s\n", configSrc, dotfileDest)
} }

@ -14,75 +14,75 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
) )
func init() { func init() {
RootCmd.AddCommand(initCommand) RootCmd.AddCommand(initCommand)
} }
func copyExistingConfigs(programs []string, fs afero.Fs) { func copyExistingConfigs(programs []string, fs afero.Fs) {
// takes list of programs and backs up configs for them // takes list of programs and backs up configs for them
destRoot := DotfilePath destRoot := DotfilePath
configRoot := ConfigPath configRoot := ConfigPath
for _, program := range programs { for _, program := range(programs) {
// TODO: do something here // TODO: do something here
err := tools.CopyDir(fs, filepath.Join(configRoot, program), filepath.Join(destRoot, program)) err := tools.CopyDir(fs, filepath.Join(configRoot, program), filepath.Join(destRoot, program))
if err != nil { if err != nil {
log.Fatalf("Problem copying %s", err.Error()) log.Fatalf("Problem copying %s", err.Error())
} }
} }
} }
func createDotfileStructure(programs []string, fs afero.Fs) { func createDotfileStructure(programs []string, fs afero.Fs) {
// takes list of programs and creates dotfiles for them // takes list of programs and creates dotfiles for them
dotfileRoot := DotfilePath dotfileRoot := DotfilePath
fmt.Printf("creating dotfile directory structure at %s\n", dotfileRoot) fmt.Printf("creating dotfile directory structure at %s\n", dotfileRoot)
for _, program := range programs { for _, program := range(programs) {
if err := fs.MkdirAll(path.Join(dotfileRoot, program), os.ModePerm); err != nil { if err := fs.MkdirAll(path.Join(dotfileRoot, program), os.ModePerm); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
} }
var initCommand = &cobra.Command{ var initCommand = &cobra.Command {
Use: "init", Use: "init",
Short: "Copy configs to dotfile directory", Short: "Copy configs to dotfile directory",
Long: "Searches existing config directory for configs and then copies them to dotfile directory", Long: "Searches existing config directory for configs and then copies them to dotfile directory",
Run: runInitCommand, Run: runInitCommand,
} }
func runInitCommand(cmd *cobra.Command, args []string) { func runInitCommand(cmd *cobra.Command, args []string) {
fs := FileSystem fs := FileSystem
// if user has passed a dotfile path flag need to add it to // if user has passed a dotfile path flag need to add it to
// viper's search path for a config file // viper's search path for a config file
testing := viper.GetBool("testing") viper.AddConfigPath(filepath.Join(DotfilePath, "dotctl"))
viper.AddConfigPath(filepath.Join(DotfilePath, "dotctl"))
if(viper.Get("testing") == true && fs.Name() != "MemMapFS") {
if viper.Get("testing") == true && fs.Name() != "MemMapFS" { log.Fatalf("wrong filesystem, got %s", fs.Name())
log.Fatalf("wrong filesystem, got %s", fs.Name()) }
}
err := fs.MkdirAll(path.Join(DotfilePath, "dotctl"), 0755)
err := fs.MkdirAll(path.Join(DotfilePath, "dotctl"), 0755) if err != nil {
if err != nil { log.Fatalf("Unable to create dotfile structure: %s", error.Error(err))
log.Fatalf("Unable to create dotfile structure: %s", error.Error(err)) }
}
_, err = fs.Create(path.Join(DotfilePath, "dotctl/config"))
_, err = fs.Create(path.Join(DotfilePath, "dotctl/config.yml")) if err != nil {
if err != nil { panic(fmt.Errorf("Unable to create config file %w", err))
panic(fmt.Errorf("Unable to create config file %w", err)) }
}
err = viper.WriteConfig()
if !testing { if err != nil && viper.Get("testing") != true {
err = viper.WriteConfig() log.Fatalf("Unable to write config on init: %s\n", err)
if err != nil && viper.Get("testing") != true { }
log.Fatalf("Unable to write config on init: %s\n", err)
} if (viper.Get("testing") != "true"){
_, err = git.PlainInit(DotfilePath, false)
_, err = git.PlainInit(DotfilePath, false) if err != nil {
if err != nil { log.Fatal(err)
log.Fatal(err) }
}
gitignoreContent := []byte (`
gitignoreContent := []byte(`
# ignore dotctl config for individual installations # ignore dotctl config for individual installations
dotctl/ dotctl/
@ -92,13 +92,13 @@ func runInitCommand(cmd *cobra.Command, args []string) {
*.tmp *.tmp
`) `)
err := afero.WriteFile(fs, filepath.Join(DotfilePath, ".gitignore"), gitignoreContent, 0644) err := afero.WriteFile(fs, filepath.Join(DotfilePath, ".gitignore"), gitignoreContent, 0644)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
fmt.Fprintf(cmd.OutOrStdout(), "Successfully created dotfiles repository at %s\n", DotfilePath) fmt.Fprintf(cmd.OutOrStdout(), "Successfully created dotfiles repository at %s\n", DotfilePath)
} }

@ -11,60 +11,76 @@ import (
) )
func init() { func init() {
RootCmd.AddCommand(linkCommand) RootCmd.AddCommand(linkCommand)
linkCommand.AddCommand(listCommand)
} }
var linkCommand = &cobra.Command{
Use: "link", var linkCommand = &cobra.Command {
Run: runLinkCommand, Use: "link",
Short: "generate symlinks according to config", Run: runLinkCommand,
Long: "runs through all configs in the dotctl config file and links them to configured symlinks", // TODO add longer description here Short: "generate symlinks according to config",
Long: "add longer description", // TODO add longer description here
} }
func runLinkCommand(cmd *cobra.Command, args []string) { func runLinkCommand(cmd *cobra.Command, args []string) {
fs := FileSystem fs := FileSystem
fmt.Println("Symlinking dotfiles...") fmt.Println("Symlinking dotfiles...")
dotfileRoot := viper.Get("dotfile-path").(string) dotfileRoot := viper.Get("dotfile-path").(string)
links := viper.GetStringMapString("links") links := viper.GetStringMapString("links")
for configName, configPath := range links { for configName, configPath := range links {
if configName == ".git" || configName == "dotctl" { if configName == ".git" || configName == "dotctl" {
continue continue
} }
dotPath := filepath.Join(dotfileRoot, configName) dotPath := filepath.Join(dotfileRoot, configName)
if configPath == "" { if configPath == ""{
fmt.Fprintf(cmd.OutOrStdout(), "Warning: could not find config for %s\n", configName) fmt.Fprintf(cmd.OutOrStdout(), "Warning: could not find config for %s\n", configName)
} }
// destination needs to be removed before symlink
if DryRun { // destination needs to be removed before symlink
log.Printf("Existing directory %s will be removed\n", configPath) if(DryRun) {
log.Printf("Existing directory %s will be removed\n", configPath)
} else {
fs.RemoveAll(configPath) } else {
} fs.RemoveAll(configPath)
}
testing := viper.Get("testing")
testing := viper.Get("testing")
if DryRun {
log.Printf("Will link %s -> %s\n", configPath, dotPath) if(DryRun) {
} else { log.Printf("Will link %s -> %s\n", configPath, dotPath)
if testing == true { } else {
fmt.Fprintf(cmd.OutOrStdout(), "%s,%s", configPath, dotPath) if(testing == true) {
} else { fmt.Fprintf(cmd.OutOrStdout(), "%s,%s", configPath, dotPath)
linkPaths(dotPath, configPath) } else {
} err := afero.OsFs.SymlinkIfPossible(afero.OsFs{}, dotPath, configPath)
} if err != nil {
} log.Fatalf("Cannot symlink %s: %s\n", configName, err.Error())
} else {
fmt.Printf("%s linked\n", configName)
}
}
}
}
}
var listCommand = &cobra.Command {
Use: "list",
Run: runListCommand,
Short: "list configs that should be symlinked",
Long: "add longer description", // TODO add longer description here
} }
func linkPaths(dotPath, configPath string) { func runListCommand(cmd *cobra.Command, args []string) {
err := afero.OsFs.SymlinkIfPossible(afero.OsFs{}, dotPath, configPath) links := viper.GetStringMapString("links")
if err != nil { fmt.Println("Configs added:")
log.Fatalf("Cannot symlink %s: %s\n", configPath, err.Error()) for configName, configPath := range links {
} else { fmt.Printf("%s: %s\n", configName, configPath)
fmt.Printf("%s linked to %s\n", configPath, dotPath) }
}
} }

@ -0,0 +1,44 @@
package cmd
import (
"bufio"
"fmt"
"log"
"os"
"strings"
"github.com/spf13/cobra"
)
func init() {
RootCmd.AddCommand(prettyCmd)
}
var prettyCmd = &cobra.Command {
Use: "pretty",
Run: func(cmd *cobra.Command, args []string) {
if (len(args) <= 0) {
log.Fatal("no arguments provided")
}
var filename = args[0]
f, err := os.Open(filename)
if err != nil {
log.Fatal(err)
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
formattedLine := strings.Replace(line, "\\n", "\n", -1)
formattedLine = strings.Replace(formattedLine, "\\t", "\t", -1)
fmt.Fprintf(cmd.OutOrStdout(), formattedLine)
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
},
}

@ -1,73 +0,0 @@
package cmd
import (
"fmt"
"path/filepath"
"github.com/Marcusk19/dotctl/tools"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func init() {
RootCmd.AddCommand(removeCommand)
}
var removeCommand = &cobra.Command{
Use: "rm",
Short: "remove dotfile link",
Long: "TODO: add longer description",
Run: runRemoveCommand,
}
func runRemoveCommand(cmd *cobra.Command, args []string) {
fs := FileSystem
if len(args) <= 0 {
fmt.Println("ERROR: missing specified config")
return
}
dotfile := args[0]
links := viper.GetStringMapString("links")
dotfileConfigPath := links[dotfile]
err := fs.Remove(dotfileConfigPath)
if err != nil {
fmt.Printf("ERROR: problem removing symlink %s: %s\n", dotfileConfigPath, err)
return
}
dotfileSavedPath := filepath.Join(DotfilePath, dotfile)
savedFile, err := fs.Open(dotfileSavedPath)
if err != nil {
fmt.Printf("ERROR: problem viewing saved dotfile(s): %s\n", err)
return
}
fileInfo, err := savedFile.Stat()
if err != nil {
fmt.Printf("ERROR: problem getting file info: %s\n", err)
return
}
if fileInfo.IsDir() {
err = tools.CopyDir(fs, dotfileSavedPath, dotfileConfigPath)
} else {
err = tools.CopyFile(fs, dotfileSavedPath, dotfileConfigPath)
}
if err != nil {
fmt.Printf("ERROR: problem copying over dotfile(s) %s\n", err)
return
}
delete(links, dotfile)
viper.Set("links", links)
err = viper.WriteConfig()
if err != nil {
fmt.Printf("ERROR: problem saving config: %s\n", err)
return
}
fmt.Printf("%s symlink removed, copied files over to %s\n", dotfile, dotfileConfigPath)
}

@ -13,6 +13,7 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
) )
var RootCmd = &cobra.Command{ var RootCmd = &cobra.Command{
Use: "dotctl", Use: "dotctl",
Short: "dotfile management", Short: "dotfile management",
@ -37,60 +38,63 @@ var DryRun bool
var FileSystem afero.Fs var FileSystem afero.Fs
func init() { func init() {
// define flags and config sections // define flags and config sections
// Cobra also supports local flags, which will only run // Cobra also supports local flags, which will only run
// when this action is called directly. // when this action is called directly.
defaultDotPath := os.Getenv("HOME") + "/dotfiles/" defaultDotPath := os.Getenv("HOME") + "/dotfiles/"
defaultConfPath := os.Getenv("HOME") + "/.config/" defaultConfPath := os.Getenv("HOME") + "/.config/"
RootCmd.PersistentFlags().StringVar( RootCmd.PersistentFlags().StringVar(
&DotfilePath, &DotfilePath,
"dotfile-path", "dotfile-path",
defaultDotPath, defaultDotPath,
"Path pointing to dotfiles directory", "Path pointing to dotfiles directory",
) )
RootCmd.PersistentFlags().StringVar( RootCmd.PersistentFlags().StringVar(
&ConfigPath, &ConfigPath,
"config-path", "config-path",
defaultConfPath, defaultConfPath,
"Path pointing to config directory", "Path pointing to config directory",
) )
RootCmd.PersistentFlags().BoolVarP(&DryRun, "dry-run", "d", false, "Only output which symlinks will be created") RootCmd.PersistentFlags().BoolVarP(&DryRun, "dry-run", "d", false, "Only output which symlinks will be created")
viper.BindPFlag("dotfile-path", RootCmd.PersistentFlags().Lookup("dotfile-path")) viper.BindPFlag("dotfile-path", RootCmd.PersistentFlags().Lookup("dotfile-path"))
viper.BindPFlag("config-path", RootCmd.PersistentFlags().Lookup("config-path")) viper.BindPFlag("config-path", RootCmd.PersistentFlags().Lookup("config-path"))
viper.BindEnv("testing") viper.BindEnv("testing")
viper.SetDefault("testing", false) viper.SetDefault("testing", false)
viper.SetConfigName("config.yml") viper.SetConfigName("config")
viper.SetConfigType("yaml") viper.SetConfigType("yaml")
viper.AddConfigPath("./tmp/dotfiles/dotctl") viper.AddConfigPath("./tmp/dotfiles/dotctl")
viper.AddConfigPath(filepath.Join(DotfilePath, "dotctl")) viper.AddConfigPath(filepath.Join(DotfilePath, "dotctl"))
viper.SetDefault("links", map[string]string{}) viper.SetDefault("links", map[string]string{})
err := viper.ReadInConfig() err := viper.ReadInConfig()
if err != nil { if err != nil {
fmt.Println("No config detected. You can generate one by using 'dotctl init'") fmt.Println("No config detected. You can generate one by using 'dotctl init'")
} }
FileSystem = UseFilesystem() FileSystem = UseFilesystem()
} }
func UseFilesystem() afero.Fs { func UseFilesystem() afero.Fs {
testing := viper.Get("testing") testing := viper.Get("testing")
if testing == "true" { if(testing == "true") {
return afero.NewMemMapFs() return afero.NewMemMapFs()
} else { } else {
return afero.NewOsFs() return afero.NewOsFs()
} }
} }
func CheckIfError(err error) { func CheckIfError(err error) {
if err != nil { if err != nil {
panic(err) panic(err)
} }
return return
} }

@ -1,62 +0,0 @@
package cmd
import (
"fmt"
"log"
"slices"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func init() {
RootCmd.AddCommand(statusCommand)
}
var statusCommand = &cobra.Command{
Use: "status",
Short: "View status of dotctl",
Long: "TODO: add longer description",
Run: runStatusCommand,
}
func runStatusCommand(cmd *cobra.Command, args []string) {
fs := FileSystem
links := viper.GetStringMapString("links")
var ignoredDirs = []string{".git", "dotctl", ".gitignore"}
dotfiles, err := afero.ReadDir(fs, viper.GetString("dotfile-path"))
if err != nil {
log.Fatalf("Cannot read dotfile dir: %s\n", err)
}
var linkedConfigs []string
var orphanedConfigs []string
fmt.Fprintln(cmd.OutOrStdout(), "Config directories currently in dotfile path:")
for _, dotfileDir := range dotfiles {
dirName := dotfileDir.Name()
if !slices.Contains(ignoredDirs, dirName) {
if links[dirName] != "" {
linkedConfigs = append(linkedConfigs, dirName, links[dirName])
} else {
orphanedConfigs = append(orphanedConfigs, dirName)
}
}
}
for i := 0; i < len(linkedConfigs); i += 2 {
fmt.Fprintf(cmd.OutOrStdout(), "%s (links to %s)\n", linkedConfigs[i], linkedConfigs[i+1])
}
fmt.Fprintln(cmd.OutOrStdout(), "================")
fmt.Fprintln(cmd.OutOrStdout(), "Orphaned configs")
for _, conf := range orphanedConfigs {
fmt.Fprintln(cmd.OutOrStdout(), conf)
}
}

@ -4,7 +4,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"log" "log"
"path"
"time" "time"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
@ -21,15 +20,15 @@ var remoteRepository string
func init() { func init() {
RootCmd.AddCommand(syncCommand) RootCmd.AddCommand(syncCommand)
syncCommand.Flags().StringVarP( syncCommand.Flags().StringVarP(
&remoteRepository, &remoteRepository,
"remote", "remote",
"r", "r",
"", "",
"URL of remote repository", "URL of remote repository",
) )
viper.BindPFlag("dotctl-origin", syncCommand.Flags().Lookup("remote")) viper.BindPFlag("dotctl-origin", syncCommand.Flags().Lookup("remote"))
} }
var syncCommand = &cobra.Command{ var syncCommand = &cobra.Command{
@ -40,29 +39,29 @@ var syncCommand = &cobra.Command{
} }
func validateInput(input string) error { func validateInput(input string) error {
if input == "" { if input == "" {
return errors.New("Missing input") return errors.New("Missing input")
} }
return nil return nil
} }
func gitAddFiles(worktree *git.Worktree, fs afero.Fs) error { func gitAddFiles(worktree *git.Worktree, fs afero.Fs) error {
dotfilepath := viper.GetString("dotfile-path") dotfilepath := viper.GetString("dotfile-path")
entries, err := afero.ReadDir(fs, dotfilepath) entries, err := afero.ReadDir(fs, dotfilepath)
if err != nil { if err != nil {
return err return err
} }
for _, entry := range entries { for _, entry := range(entries) {
if entry.Name() == "dotctl" { if(entry.Name() == "dotctl") {
continue continue
} }
_, err = worktree.Add(entry.Name()) _, err = worktree.Add(entry.Name())
if err != nil { if err != nil {
return err return err
} }
} }
return nil return nil
} }
func runSyncCommand(cmd *cobra.Command, args []string) { func runSyncCommand(cmd *cobra.Command, args []string) {
@ -91,116 +90,73 @@ func runSyncCommand(cmd *cobra.Command, args []string) {
w, err := r.Worktree() w, err := r.Worktree()
CheckIfError(err) CheckIfError(err)
username := promptui.Prompt{ username := promptui.Prompt{
Label: "username", Label: "username",
Validate: validateInput, Validate: validateInput,
} }
password := promptui.Prompt{ password := promptui.Prompt{
Label: "password", Label: "password",
Validate: validateInput, Validate: validateInput,
HideEntered: true, HideEntered: true,
Mask: '*', Mask: '*',
} }
usernameVal, err := username.Run() usernameVal, err := username.Run()
CheckIfError(err) CheckIfError(err)
passwordVal, err := password.Run()
CheckIfError(err)
fmt.Println("Pulling from remote") passwordVal, err := password.Run()
CheckIfError(err)
err = w.Pull(&git.PullOptions{ err = w.Pull(&git.PullOptions{
RemoteName: "origin", RemoteName: "origin",
Auth: &http.BasicAuth{ Auth: &http.BasicAuth {
Username: usernameVal, Username: usernameVal,
Password: passwordVal, Password: passwordVal,
}, },
}) })
if err != nil { if err != nil{
fmt.Println(err) fmt.Println(err)
} else { } else {
fmt.Fprintf(cmd.OutOrStdout(), "successfully pulled from %s", origin) fmt.Fprintf(cmd.OutOrStdout(), "successfully pulled from %s", origin)
} }
status, err := w.Status() err = gitAddFiles(w, FileSystem)
if err != nil { if err != nil {
log.Fatalln("Error getting status", err) log.Fatalf("Could not add files: %s\n", err)
} }
if !status.IsClean() { commitMessage := "backup " + time.Now().String()
fmt.Println("Changes detected, do you want to push them?")
confirm := promptui.Prompt{
Label: "commit and push changes",
IsConfirm: true,
}
_, err := confirm.Run()
if err != nil {
fmt.Println("Will not push changes")
return
}
fmt.Println("Pushing changes...")
err = gitAddFiles(w, FileSystem)
if err != nil {
log.Fatalf("Could not add files: %s\n", err)
return
}
commitMessage := "backup " + time.Now().String()
commit, err := w.Commit(commitMessage, &git.CommitOptions{
Author: &object.Signature{
Name: "dotctl CLI",
Email: "example@example.com",
When: time.Now(),
},
})
if err != nil {
log.Fatal(err.Error())
}
obj, err := r.CommitObject(commit)
if err != nil { commit, err := w.Commit(commitMessage, &git.CommitOptions{
log.Fatalf("Cannot commit: %s", err) Author: &object.Signature{
} Name: "dotctl CLI",
Email: "example@example.com",
When: time.Now(),
},
})
fmt.Println(obj) if err != nil {
log.Fatal(err.Error())
}
err = r.Push(&git.PushOptions{ obj, err := r.CommitObject(commit)
RemoteName: "origin",
Auth: &http.BasicAuth{
Username: usernameVal,
Password: passwordVal,
},
})
CheckIfError(err)
}
// a pull deletes the dotctl config from the filesystem, need to recreate it if err != nil {
rewriteConfig() log.Fatalf("Cannot commit: %s",err)
} }
func rewriteConfig() { fmt.Println(obj)
fs := UseFilesystem()
err := fs.MkdirAll(path.Join(DotfilePath, "dotctl"), 0755)
if err != nil {
log.Fatalf("Unable to create dotfile structure: %s", error.Error(err))
}
_, err = fs.Create(path.Join(DotfilePath, "dotctl/config")) err = r.Push(&git.PushOptions{
if err != nil { RemoteName: "origin",
panic(fmt.Errorf("Unable to create config file %w", err)) Auth: &http.BasicAuth {
} Username: usernameVal,
Password: passwordVal,
},
})
CheckIfError(err)
viper.WriteConfig()
err = viper.WriteConfig()
if err != nil {
fmt.Println("Error: could not write config: ", err)
}
} }

@ -3,7 +3,6 @@ module github.com/Marcusk19/dotctl
go 1.21.0 go 1.21.0
require ( require (
github.com/carlmjohnson/versioninfo v0.22.5
github.com/go-git/go-git/v5 v5.11.0 github.com/go-git/go-git/v5 v5.11.0
github.com/manifoldco/promptui v0.9.0 github.com/manifoldco/promptui v0.9.0
github.com/spf13/afero v1.11.0 github.com/spf13/afero v1.11.0
@ -17,7 +16,7 @@ require (
github.com/Microsoft/go-winio v0.6.1 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
github.com/cloudflare/circl v1.3.7 // indirect github.com/cloudflare/circl v1.3.3 // indirect
github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emirpasic/gods v1.18.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect
@ -45,14 +44,13 @@ require (
github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect
go.uber.org/atomic v1.9.0 // indirect go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect
golang.org/x/crypto v0.31.0 // indirect golang.org/x/crypto v0.16.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/mod v0.17.0 // indirect golang.org/x/mod v0.12.0 // indirect
golang.org/x/net v0.25.0 // indirect golang.org/x/net v0.19.0 // indirect
golang.org/x/sync v0.10.0 // indirect golang.org/x/sys v0.15.0 // indirect
golang.org/x/sys v0.28.0 // indirect golang.org/x/text v0.14.0 // indirect
golang.org/x/text v0.21.0 // indirect golang.org/x/tools v0.13.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect

@ -10,17 +10,14 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuW
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQjMRvwtKgwwc=
github.com/carlmjohnson/versioninfo v0.22.5/go.mod h1:QT9mph3wcVfISUKd0i9sZfVrPviHuSF+cUtLjm2WSf8=
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
@ -131,14 +128,14 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@ -146,13 +143,13 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -167,15 +164,15 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@ -183,14 +180,14 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

@ -1,27 +1,11 @@
/* /*
Copyright © 2024 NAME HERE <EMAIL ADDRESS> Copyright © 2024 NAME HERE <EMAIL ADDRESS>
*/ */
package main package main
import ( import "github.com/Marcusk19/dotctl/cmd"
"fmt"
"time"
"github.com/Marcusk19/dotctl/cmd"
"github.com/carlmjohnson/versioninfo"
)
var (
version = "dev"
commit = "none"
date = "unknown"
)
func SetVersionInfo(version, commit, date string) {
cmd.RootCmd.Version = fmt.Sprintf("%s [Built on %s from Git Sha %s]", version, date, commit)
}
func main() { func main() {
SetVersionInfo(versioninfo.Version, versioninfo.Revision, versioninfo.LastCommit.Format(time.RFC3339))
cmd.Execute() cmd.Execute()
} }

@ -9,76 +9,78 @@ import (
) )
func init() { func init() {
tools.SetTestFs() tools.SetTestFs()
} }
func TestCopyFile(t *testing.T) { func TestCopyFile(t *testing.T) {
fs := afero.NewMemMapFs() fs := afero.NewMemMapFs()
fs.MkdirAll("test/src", 0755) fs.MkdirAll("test/src", 0755)
fs.MkdirAll("test/dest", 0755) fs.MkdirAll("test/dest", 0755)
err := afero.WriteFile(fs, "test/src/a.txt", []byte("file a"), 0644) err := afero.WriteFile(fs, "test/src/a.txt", []byte("file a"), 0644)
if err != nil { if err != nil {
t.Errorf("problem creating source file: %s", err.Error()) t.Errorf("problem creating source file: %s", err.Error())
} }
err = tools.CopyFile(fs, "test/src/a.txt", "test/dest/a.txt") err = tools.CopyFile(fs, "test/src/a.txt", "test/dest/a.txt")
if err != nil { if err != nil {
t.Error(err.Error()) t.Error(err.Error())
} }
_, err = fs.Stat("test/dest/a.txt") _, err = fs.Stat("test/dest/a.txt")
if os.IsNotExist(err) { if os.IsNotExist(err) {
t.Errorf("expected destination file does not exist") t.Errorf("expected destination file does not exist")
} }
result, err := afero.ReadFile(fs, "test/dest/a.txt") result, err := afero.ReadFile(fs, "test/dest/a.txt")
if err != nil { if err != nil {
t.Error(err.Error()) t.Error(err.Error())
} }
if string(result) != "file a" { if string(result) != "file a" {
t.Errorf("expected 'file a' got '%s'", string(result)) t.Errorf("expected 'file a' got '%s'", string(result))
} }
} }
func TestCopyDir(t *testing.T) { func TestCopyDir(t *testing.T) {
fs := afero.NewMemMapFs() fs := afero.NewMemMapFs()
fs.MkdirAll("test/src/dirA", 0755) fs.MkdirAll("test/src/dirA", 0755)
fs.MkdirAll("test/dest/", 0755) fs.MkdirAll("test/dest/", 0755)
fs.Mkdir("test/src/dirA/dirB", 0755) fs.Mkdir("test/src/dirA/dirB", 0755)
err := afero.WriteFile(fs, "test/src/dirA/a.txt", []byte("file a"), 0644) err := afero.WriteFile(fs, "test/src/dirA/a.txt", []byte("file a"), 0644)
if err != nil { if err != nil {
t.Error(err.Error()) t.Error(err.Error())
} }
err = afero.WriteFile(fs, "test/src/dirA/dirB/b.txt", []byte("file b"), 0644) err = afero.WriteFile(fs, "test/src/dirA/dirB/b.txt", []byte("file b"), 0644)
if err != nil { if err != nil {
t.Error(err.Error()) t.Error(err.Error())
} }
err = tools.CopyDir(fs, "test/src", "test/dest") err = tools.CopyDir(fs, "test/src", "test/dest")
if err != nil { if err != nil {
t.Error(err.Error()) t.Error(err.Error())
} }
result, err := afero.ReadFile(fs, "test/dest/dirA/a.txt") result, err := afero.ReadFile(fs, "test/dest/dirA/a.txt")
if err != nil { if err != nil {
t.Error(err.Error()) t.Error(err.Error())
} }
if string(result) != "file a" { if string(result) != "file a" {
t.Errorf("expected 'file a' got '%s'", string(result)) t.Errorf("expected 'file a' got '%s'", string(result))
} }
result, err = afero.ReadFile(fs, "test/dest/dirA/dirB/b.txt") result, err = afero.ReadFile(fs, "test/dest/dirA/dirB/b.txt")
if err != nil { if err != nil {
t.Error(err.Error()) t.Error(err.Error())
} }
if string(result) != "file b" { if string(result) != "file b" {
t.Errorf("expected 'file b' got '%s'", string(result)) t.Errorf("expected 'file b' got '%s'", string(result))
} }
} }

@ -2,7 +2,6 @@ package test
import ( import (
"bytes" "bytes"
"os"
"path/filepath" "path/filepath"
"testing" "testing"
@ -12,24 +11,24 @@ import (
) )
func TestInitCommand(t *testing.T) { func TestInitCommand(t *testing.T) {
viper.Set("testing", true) viper.Set("testing", true)
fs := cmd.FileSystem fs := cmd.FileSystem
dotctl := cmd.RootCmd dotctl := cmd.RootCmd
actual := new(bytes.Buffer) actual := new(bytes.Buffer)
dotctl.SetOut(actual) dotctl.SetOut(actual)
dotctl.SetErr(actual) dotctl.SetErr(actual)
dotctl.SetArgs([]string{"init"}) dotctl.SetArgs([]string{"init", "--dotfile-path=dotctl_test/dotfiles"})
dotctl.Execute() dotctl.Execute()
homedir := os.Getenv("HOME") homedir := "dotctl_test/"
_, err := afero.ReadFile(fs, filepath.Join(homedir, "dotfiles/dotctl/config.yml")) _, err := afero.ReadFile(fs, filepath.Join(homedir, "dotfiles/dotctl/config"))
if err != nil { if err != nil {
t.Error(err.Error()) t.Error(err.Error())
} }
} }

@ -8,36 +8,49 @@ import (
"testing" "testing"
"github.com/Marcusk19/dotctl/cmd" "github.com/Marcusk19/dotctl/cmd"
"github.com/spf13/afero"
"github.com/spf13/viper" "github.com/spf13/viper"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestLinkCommand(t *testing.T) { func TestLinkCommand(t *testing.T) {
viper.Set("testing", true) oldDotfilePath := viper.GetString("dotfile-path")
cmd.FileSystem = afero.NewMemMapFs() setUpTesting()
fs := cmd.FileSystem dotctl := cmd.RootCmd
homedir := os.Getenv("HOME") actual := new(bytes.Buffer)
dotctl.SetOut(actual)
dotctl.SetErr(actual)
dotctl.SetArgs([]string{"link"})
dotctl.Execute()
fs.MkdirAll(filepath.Join(homedir, "dotfiles/dotctl"), 0755) homedir := os.Getenv("HOME")
links := map[string]string{ someconfig := filepath.Join(homedir, ".config/someconfig/")
"someconfig": filepath.Join(homedir, ".config/someconfig"), somedot := filepath.Join(homedir, "dotfiles/someconfig/")
}
viper.Set("links", links)
dotctl := cmd.RootCmd expected := fmt.Sprintf("%s,%s", someconfig, somedot)
actual := new(bytes.Buffer)
dotctl.SetOut(actual) assert.Equal(t, expected, actual.String(), "actual differs from expected")
dotctl.SetErr(actual)
dotctl.SetArgs([]string{"link"})
dotctl.Execute() tearDownTesting(oldDotfilePath)
}
func setUpTesting() {
viper.Set("testing", true)
someconfig := filepath.Join(homedir, ".config/someconfig/") fs := cmd.FileSystem
somedot := filepath.Join(homedir, "dotfiles/someconfig/") homedir := os.Getenv("HOME")
fakeLinks := map[string]string {"someconfig": filepath.Join(homedir, ".config/someconfig")}
viper.Set("links", fakeLinks)
fs.MkdirAll(filepath.Join(homedir, "dotfiles/dotctl"), 0755)
fs.Create(filepath.Join(homedir, "dotfiles/dotctl/config"))
expected := fmt.Sprintf("%s,%s", someconfig, somedot) viper.Set("dotfile-path", filepath.Join(homedir, "dotfiles"))
viper.Set("someconfig", filepath.Join(homedir, ".config/someconfig/"))
}
assert.Equal(t, expected, actual.String(), "actual differs from expected") func tearDownTesting(oldDotfilePath string) {
viper.Set("dotfile-path", oldDotfilePath)
viper.WriteConfig()
} }

@ -0,0 +1,22 @@
package test
import (
"bytes"
"testing"
"github.com/Marcusk19/dotctl/cmd"
"github.com/stretchr/testify/assert"
)
func TestPrettyCommand(t *testing.T) {
dotctl := cmd.RootCmd
actual := new(bytes.Buffer)
dotctl.SetOut(actual)
dotctl.SetErr(actual)
dotctl.SetArgs([]string{"pretty", "fixtures/test_pretty.txt"})
dotctl.Execute()
expected := "The end of this sentence should start a newline. \nThe next sentence should be indented below this one.\n\tHello this is the end of the text"
assert.Equal(t, expected, actual.String(), "actual value differs from expected")
}

@ -1,45 +0,0 @@
package test
import (
"bytes"
"os"
"path/filepath"
"testing"
"github.com/Marcusk19/dotctl/cmd"
"github.com/spf13/afero"
"github.com/spf13/viper"
)
func TestStatusCommand(t *testing.T) {
cmd.FileSystem = afero.NewMemMapFs()
viper.Set("testing", true)
fs := cmd.FileSystem
homedir := os.Getenv("HOME")
fs.MkdirAll(filepath.Join(homedir, "dotfiles/dotctl"), 0755)
fs.MkdirAll(filepath.Join(homedir, "dotfiles/someconfig"), 0755)
fs.MkdirAll(filepath.Join(homedir, "dotfiles/somelinkedconfig"), 0755)
var links = map[string]string{
"somelinkedconfig": "configpath",
}
viper.Set("links", links)
dotctl := cmd.RootCmd
actual := new(bytes.Buffer)
dotctl.SetOut(actual)
dotctl.SetErr(actual)
dotctl.SetArgs([]string{"status"})
dotctl.Execute()
// expected := "Config directories currently in dotfile path:\n" +
// "someconfig\nsomelinkedconfig - configpath\n"
// assert.Equal(t, expected, actual.String(), "actual differs from expected")
}

@ -9,69 +9,70 @@ import (
"github.com/spf13/afero" "github.com/spf13/afero"
) )
func CopyFile(os afero.Fs, srcFile, destFile string) error { func CopyFile(os afero.Fs, srcFile, destFile string) error{
// helper function to copy files over // helper function to copy files over
// ignore pre-existing git files // ignore pre-existing git files
if strings.Contains(srcFile, ".git") { if strings.Contains(srcFile, ".git") {
return nil return nil
} }
sourceFileStat, err := os.Stat(srcFile) sourceFileStat, err := os.Stat(srcFile)
if err != nil { if err != nil {
return err return err
} }
if !sourceFileStat.Mode().IsRegular() { if !sourceFileStat.Mode().IsRegular() {
return fmt.Errorf("%s is not a regular file", srcFile) return fmt.Errorf("%s is not a regular file", srcFile)
} }
source, err := os.Open(srcFile)
if err != nil {
return err
}
defer source.Close()
destination, err := os.Create(destFile) source, err := os.Open(srcFile)
if err != nil { if err != nil {
fmt.Printf("Error creating destination file %s\n", destFile) return err
return err }
} defer source.Close()
defer destination.Close()
_, err = io.Copy(destination, source) destination, err := os.Create(destFile)
if err != nil {
fmt.Printf("Error creating destination file %s\n", destFile)
return err
}
defer destination.Close()
return err _, err = io.Copy(destination, source)
return err
} }
func CopyDir(os afero.Fs, srcDir, destDir string) error { func CopyDir(os afero.Fs, srcDir, destDir string) error {
os.Mkdir(destDir, 0755) os.Mkdir(destDir, 0755)
entries, err := afero.ReadDir(os, srcDir) entries, err := afero.ReadDir(os, srcDir)
if err != nil { if err != nil {
return err return err
} }
for _, entry := range entries { for _, entry := range(entries) {
if entry.IsDir() { if entry.IsDir() {
if entry.Name() == ".git" { if entry.Name() == ".git" {
continue continue
} }
subDir := filepath.Join(srcDir, entry.Name()) subDir := filepath.Join(srcDir, entry.Name())
destSubDir := filepath.Join(destDir, entry.Name()) destSubDir := filepath.Join(destDir, entry.Name())
err := os.MkdirAll(destSubDir, entry.Mode().Perm()) err := os.MkdirAll(destSubDir, entry.Mode().Perm())
if err != nil { if err != nil {
return err return err
} }
CopyDir(os, subDir, destSubDir) CopyDir(os, subDir, destSubDir)
continue continue
} }
sourcePath := filepath.Join(srcDir, entry.Name()) sourcePath := filepath.Join(srcDir, entry.Name())
destPath := filepath.Join(destDir, entry.Name()) destPath := filepath.Join(destDir, entry.Name())
err := CopyFile(os, sourcePath, destPath) err := CopyFile(os, sourcePath, destPath)
if err != nil { if err != nil {
return err return err
} }
} }
return nil return nil
} }

@ -8,8 +8,9 @@ import (
var AppFs afero.Fs = afero.NewOsFs() var AppFs afero.Fs = afero.NewOsFs()
func SetTestFs() { func SetTestFs() {
log.Println("setting test fs") log.Println("setting test fs")
testFs := afero.NewMemMapFs() testFs := afero.NewMemMapFs()
AppFs = testFs AppFs = testFs
} }

Loading…
Cancel
Save