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:
TESTING=true go test -v ./test
clean:
rm -rf test/dotctl_test 2> /dev/null
rm -rf tmp 2> /dev/null
install:
go build
cp dotctl /usr/local/bin
sandbox:
mkdir -p ./tmp/ 2> /dev/null
cp -r ~/.config/ ./tmp/config 2> /dev/null
pre-commit-hooks:
pre-commit autoupdate
pre-commit install
unit-test:
TESTING=true go test -v ./test
rm -rf test/dotctl_test 2> /dev/null

@ -1,25 +1,13 @@
# Dotctl
dotfile management
A cli tool to manage your dotfiles
## About
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
and symlink config files/directories to the central `dotfiles` directory.
git. It aims to abstract away the manual effort of symlinking your dotfiles to config directories and
updating them with git.
## Installation
### 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
```
- TBD
## Usage
@ -30,19 +18,20 @@ dotctl init
dotctl add ~/.config/nvim
# create symlinks
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
2. commits and pushes any changes detected in the dotfile repo
```bash
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 (
"fmt"
"log"
"os"
"path/filepath"
"strings"
"github.com/Marcusk19/dotctl/tools"
"github.com/manifoldco/promptui"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func init() {
addCommand.Flags().BoolVar(&absolutePath, "absolute", false, "absolute path of config")
RootCmd.AddCommand(addCommand)
RootCmd.AddCommand(addCommand)
}
var addCommand = &cobra.Command{
Use: "add",
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
Run: runAddCommand,
var addCommand = &cobra.Command {
Use: "add",
Short: "Adds config to be tracked by dotctl",
Long: "TODO: add longer description", // TODO add more description
Run: runAddCommand,
}
var absolutePath bool
func runAddCommand(cmd *cobra.Command, args []string) {
fs := FileSystem
testing := viper.GetBool("testing")
if len(args) <= 0 {
fmt.Println("ERROR: requires config path")
return
}
configSrc := args[0]
if !absolutePath {
cwd, _ := os.Getwd()
configSrc = cwd + "/" + configSrc
}
dirs := strings.Split(configSrc, "/")
name := dirs[len(dirs)-1] // take the last section of the path, this should be the name
if name[0] == '.' {
name = name[1:]
}
links := viper.GetStringMap("links")
links[name] = configSrc
viper.Set("links", links)
dotfilePath := viper.Get("dotfile-path").(string)
dotfileDest := filepath.Join(dotfilePath, name)
if DryRun {
fmt.Printf("Will copy %s -> %s \n", configSrc, dotfileDest)
return
}
_, err := fs.Stat(dotfileDest)
if err == nil {
fmt.Printf("Looks like %s exists in current dotfile directory\n", dotfileDest)
fmt.Printf("Do you want to overwrite it with what is in %s?\n", configSrc)
confirm := promptui.Prompt{
Label: "overwrite config",
IsConfirm: true,
}
overwrite, _ := confirm.Run()
if strings.ToUpper(overwrite) == "Y" {
addConfigToDir(fs, 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)
fs := FileSystem
if len(args) <= 0 {
fmt.Println("ERROR: requires at least one argument")
return
}
configSrc := args[0]
dirs := strings.Split(configSrc, "/")
name := dirs[len(dirs) - 1] // take the last section of the path, this should be the name
links := viper.GetStringMap("links")
links[name] = configSrc
viper.Set("links", links)
err := viper.WriteConfig()
if err != nil {
fmt.Printf("Problem updating dotctl config %s", err)
}
dotfilePath := viper.Get("dotfile-path").(string)
dotfileDest := filepath.Join(dotfilePath, name)
if DryRun {
fmt.Printf("Will copy %s -> %s \n", configSrc, dotfileDest)
return
}
_, err = fs.Stat(dotfileDest)
if err == nil {
fmt.Printf("Looks like %s exists in current dotfile directory\n", dotfileDest)
fmt.Println("Do you want to overwrite it?")
confirm := promptui.Prompt{
Label: "overwrite config",
IsConfirm: true,
}
overwrite, _ := confirm.Run()
if strings.ToUpper(overwrite) != "Y" {
return
}
}
err = tools.CopyDir(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"
)
func init() {
RootCmd.AddCommand(initCommand)
RootCmd.AddCommand(initCommand)
}
func copyExistingConfigs(programs []string, fs afero.Fs) {
// takes list of programs and backs up configs for them
destRoot := DotfilePath
configRoot := ConfigPath
for _, program := range programs {
// TODO: do something here
err := tools.CopyDir(fs, filepath.Join(configRoot, program), filepath.Join(destRoot, program))
if err != nil {
log.Fatalf("Problem copying %s", err.Error())
}
}
// takes list of programs and backs up configs for them
destRoot := DotfilePath
configRoot := ConfigPath
for _, program := range(programs) {
// TODO: do something here
err := tools.CopyDir(fs, filepath.Join(configRoot, program), filepath.Join(destRoot, program))
if err != nil {
log.Fatalf("Problem copying %s", err.Error())
}
}
}
func createDotfileStructure(programs []string, fs afero.Fs) {
// takes list of programs and creates dotfiles for them
dotfileRoot := DotfilePath
fmt.Printf("creating dotfile directory structure at %s\n", dotfileRoot)
for _, program := range programs {
if err := fs.MkdirAll(path.Join(dotfileRoot, program), os.ModePerm); err != nil {
log.Fatal(err)
}
}
// takes list of programs and creates dotfiles for them
dotfileRoot := DotfilePath
fmt.Printf("creating dotfile directory structure at %s\n", dotfileRoot)
for _, program := range(programs) {
if err := fs.MkdirAll(path.Join(dotfileRoot, program), os.ModePerm); err != nil {
log.Fatal(err)
}
}
}
var initCommand = &cobra.Command{
Use: "init",
Short: "Copy configs to dotfile directory",
Long: "Searches existing config directory for configs and then copies them to dotfile directory",
Run: runInitCommand,
var initCommand = &cobra.Command {
Use: "init",
Short: "Copy configs to dotfile directory",
Long: "Searches existing config directory for configs and then copies them to dotfile directory",
Run: runInitCommand,
}
func runInitCommand(cmd *cobra.Command, args []string) {
fs := FileSystem
// if user has passed a dotfile path flag need to add it to
// viper's search path for a config file
testing := viper.GetBool("testing")
viper.AddConfigPath(filepath.Join(DotfilePath, "dotctl"))
if viper.Get("testing") == true && fs.Name() != "MemMapFS" {
log.Fatalf("wrong filesystem, got %s", fs.Name())
}
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.yml"))
if err != nil {
panic(fmt.Errorf("Unable to create config file %w", err))
}
if !testing {
err = viper.WriteConfig()
if err != nil && viper.Get("testing") != true {
log.Fatalf("Unable to write config on init: %s\n", err)
}
_, err = git.PlainInit(DotfilePath, false)
if err != nil {
log.Fatal(err)
}
gitignoreContent := []byte(`
fs := FileSystem
// if user has passed a dotfile path flag need to add it to
// viper's search path for a config file
viper.AddConfigPath(filepath.Join(DotfilePath, "dotctl"))
if(viper.Get("testing") == true && fs.Name() != "MemMapFS") {
log.Fatalf("wrong filesystem, got %s", fs.Name())
}
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"))
if err != nil {
panic(fmt.Errorf("Unable to create config file %w", err))
}
err = viper.WriteConfig()
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)
if err != nil {
log.Fatal(err)
}
gitignoreContent := []byte (`
# ignore dotctl config for individual installations
dotctl/
@ -92,13 +92,13 @@ func runInitCommand(cmd *cobra.Command, args []string) {
*.tmp
`)
err := afero.WriteFile(fs, filepath.Join(DotfilePath, ".gitignore"), gitignoreContent, 0644)
err := afero.WriteFile(fs, filepath.Join(DotfilePath, ".gitignore"), gitignoreContent, 0644)
if err != nil {
log.Fatal(err)
}
if err != nil {
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() {
RootCmd.AddCommand(linkCommand)
RootCmd.AddCommand(linkCommand)
linkCommand.AddCommand(listCommand)
}
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", // TODO add longer description here
var linkCommand = &cobra.Command {
Use: "link",
Run: runLinkCommand,
Short: "generate symlinks according to config",
Long: "add longer description", // TODO add longer description here
}
func runLinkCommand(cmd *cobra.Command, args []string) {
fs := FileSystem
fmt.Println("Symlinking dotfiles...")
dotfileRoot := viper.Get("dotfile-path").(string)
links := viper.GetStringMapString("links")
for configName, configPath := range links {
if configName == ".git" || configName == "dotctl" {
continue
}
dotPath := filepath.Join(dotfileRoot, configName)
if configPath == "" {
fmt.Fprintf(cmd.OutOrStdout(), "Warning: could not find config for %s\n", configName)
}
// destination needs to be removed before symlink
if DryRun {
log.Printf("Existing directory %s will be removed\n", configPath)
} else {
fs.RemoveAll(configPath)
}
testing := viper.Get("testing")
if DryRun {
log.Printf("Will link %s -> %s\n", configPath, dotPath)
} else {
if testing == true {
fmt.Fprintf(cmd.OutOrStdout(), "%s,%s", configPath, dotPath)
} else {
linkPaths(dotPath, configPath)
}
}
}
fs := FileSystem
fmt.Println("Symlinking dotfiles...")
dotfileRoot := viper.Get("dotfile-path").(string)
links := viper.GetStringMapString("links")
for configName, configPath := range links {
if configName == ".git" || configName == "dotctl" {
continue
}
dotPath := filepath.Join(dotfileRoot, configName)
if configPath == ""{
fmt.Fprintf(cmd.OutOrStdout(), "Warning: could not find config for %s\n", configName)
}
// destination needs to be removed before symlink
if(DryRun) {
log.Printf("Existing directory %s will be removed\n", configPath)
} else {
fs.RemoveAll(configPath)
}
testing := viper.Get("testing")
if(DryRun) {
log.Printf("Will link %s -> %s\n", configPath, dotPath)
} else {
if(testing == true) {
fmt.Fprintf(cmd.OutOrStdout(), "%s,%s", configPath, dotPath)
} 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) {
err := afero.OsFs.SymlinkIfPossible(afero.OsFs{}, dotPath, configPath)
if err != nil {
log.Fatalf("Cannot symlink %s: %s\n", configPath, err.Error())
} else {
fmt.Printf("%s linked to %s\n", configPath, dotPath)
}
func runListCommand(cmd *cobra.Command, args []string) {
links := viper.GetStringMapString("links")
fmt.Println("Configs added:")
for configName, configPath := range links {
fmt.Printf("%s: %s\n", configName, configPath)
}
}

@ -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"
)
var RootCmd = &cobra.Command{
Use: "dotctl",
Short: "dotfile management",
@ -37,60 +38,63 @@ var DryRun bool
var FileSystem afero.Fs
func init() {
// define flags and config sections
// define flags and config sections
// Cobra also supports local flags, which will only run
// when this action is called directly.
defaultDotPath := os.Getenv("HOME") + "/dotfiles/"
defaultConfPath := os.Getenv("HOME") + "/.config/"
RootCmd.PersistentFlags().StringVar(
&DotfilePath,
"dotfile-path",
defaultDotPath,
"Path pointing to dotfiles directory",
)
RootCmd.PersistentFlags().StringVar(
&ConfigPath,
"config-path",
defaultConfPath,
"Path pointing to config directory",
)
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("config-path", RootCmd.PersistentFlags().Lookup("config-path"))
viper.BindEnv("testing")
viper.SetDefault("testing", false)
viper.SetConfigName("config.yml")
viper.SetConfigType("yaml")
viper.AddConfigPath("./tmp/dotfiles/dotctl")
viper.AddConfigPath(filepath.Join(DotfilePath, "dotctl"))
viper.SetDefault("links", map[string]string{})
err := viper.ReadInConfig()
if err != nil {
fmt.Println("No config detected. You can generate one by using 'dotctl init'")
}
FileSystem = UseFilesystem()
defaultDotPath := os.Getenv("HOME") + "/dotfiles/"
defaultConfPath := os.Getenv("HOME") + "/.config/"
RootCmd.PersistentFlags().StringVar(
&DotfilePath,
"dotfile-path",
defaultDotPath,
"Path pointing to dotfiles directory",
)
RootCmd.PersistentFlags().StringVar(
&ConfigPath,
"config-path",
defaultConfPath,
"Path pointing to config directory",
)
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("config-path", RootCmd.PersistentFlags().Lookup("config-path"))
viper.BindEnv("testing")
viper.SetDefault("testing", false)
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath("./tmp/dotfiles/dotctl")
viper.AddConfigPath(filepath.Join(DotfilePath, "dotctl"))
viper.SetDefault("links", map[string]string{})
err := viper.ReadInConfig()
if err != nil {
fmt.Println("No config detected. You can generate one by using 'dotctl init'")
}
FileSystem = UseFilesystem()
}
func UseFilesystem() afero.Fs {
testing := viper.Get("testing")
if testing == "true" {
return afero.NewMemMapFs()
} else {
return afero.NewOsFs()
}
testing := viper.Get("testing")
if(testing == "true") {
return afero.NewMemMapFs()
} else {
return afero.NewOsFs()
}
}
func CheckIfError(err error) {
if err != nil {
panic(err)
}
return
if err != nil {
panic(err)
}
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"
"fmt"
"log"
"path"
"time"
"github.com/go-git/go-git/v5"
@ -21,15 +20,15 @@ var remoteRepository string
func init() {
RootCmd.AddCommand(syncCommand)
syncCommand.Flags().StringVarP(
&remoteRepository,
"remote",
"r",
"",
"URL of remote repository",
)
viper.BindPFlag("dotctl-origin", syncCommand.Flags().Lookup("remote"))
syncCommand.Flags().StringVarP(
&remoteRepository,
"remote",
"r",
"",
"URL of remote repository",
)
viper.BindPFlag("dotctl-origin", syncCommand.Flags().Lookup("remote"))
}
var syncCommand = &cobra.Command{
@ -40,29 +39,29 @@ var syncCommand = &cobra.Command{
}
func validateInput(input string) error {
if input == "" {
return errors.New("Missing input")
}
if input == "" {
return errors.New("Missing input")
}
return nil
return nil
}
func gitAddFiles(worktree *git.Worktree, fs afero.Fs) error {
dotfilepath := viper.GetString("dotfile-path")
entries, err := afero.ReadDir(fs, dotfilepath)
if err != nil {
return err
}
for _, entry := range entries {
if entry.Name() == "dotctl" {
continue
}
_, err = worktree.Add(entry.Name())
if err != nil {
return err
}
}
return nil
dotfilepath := viper.GetString("dotfile-path")
entries, err := afero.ReadDir(fs, dotfilepath)
if err != nil {
return err
}
for _, entry := range(entries) {
if(entry.Name() == "dotctl") {
continue
}
_, err = worktree.Add(entry.Name())
if err != nil {
return err
}
}
return nil
}
func runSyncCommand(cmd *cobra.Command, args []string) {
@ -91,116 +90,73 @@ func runSyncCommand(cmd *cobra.Command, args []string) {
w, err := r.Worktree()
CheckIfError(err)
username := promptui.Prompt{
Label: "username",
Validate: validateInput,
}
username := promptui.Prompt{
Label: "username",
Validate: validateInput,
}
password := promptui.Prompt{
Label: "password",
Validate: validateInput,
HideEntered: true,
Mask: '*',
}
password := promptui.Prompt{
Label: "password",
Validate: validateInput,
HideEntered: true,
Mask: '*',
}
usernameVal, err := username.Run()
CheckIfError(err)
passwordVal, err := password.Run()
CheckIfError(err)
usernameVal, err := username.Run()
CheckIfError(err)
fmt.Println("Pulling from remote")
passwordVal, err := password.Run()
CheckIfError(err)
err = w.Pull(&git.PullOptions{
RemoteName: "origin",
Auth: &http.BasicAuth{
Username: usernameVal,
Password: passwordVal,
},
Auth: &http.BasicAuth {
Username: usernameVal,
Password: passwordVal,
},
})
if err != nil {
fmt.Println(err)
} else {
fmt.Fprintf(cmd.OutOrStdout(), "successfully pulled from %s", origin)
}
status, err := w.Status()
if err != nil {
log.Fatalln("Error getting status", err)
}
if !status.IsClean() {
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{
fmt.Println(err)
} else {
fmt.Fprintf(cmd.OutOrStdout(), "successfully pulled from %s", origin)
}
if err != nil {
log.Fatal(err.Error())
}
err = gitAddFiles(w, FileSystem)
if err != nil {
log.Fatalf("Could not add files: %s\n", err)
}
commitMessage := "backup " + time.Now().String()
obj, err := r.CommitObject(commit)
commit, err := w.Commit(commitMessage, &git.CommitOptions{
Author: &object.Signature{
Name: "dotctl CLI",
Email: "example@example.com",
When: time.Now(),
},
})
if err != nil {
log.Fatalf("Cannot commit: %s", err)
}
if err != nil {
log.Fatal(err.Error())
}
fmt.Println(obj)
obj, err := r.CommitObject(commit)
err = r.Push(&git.PushOptions{
RemoteName: "origin",
Auth: &http.BasicAuth{
Username: usernameVal,
Password: passwordVal,
},
})
CheckIfError(err)
}
if err != nil {
log.Fatalf("Cannot commit: %s",err)
}
// a pull deletes the dotctl config from the filesystem, need to recreate it
rewriteConfig()
}
fmt.Println(obj)
func rewriteConfig() {
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"))
if err != nil {
panic(fmt.Errorf("Unable to create config file %w", err))
}
err = r.Push(&git.PushOptions{
RemoteName: "origin",
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
require (
github.com/carlmjohnson/versioninfo v0.22.5
github.com/go-git/go-git/v5 v5.11.0
github.com/manifoldco/promptui v0.9.0
github.com/spf13/afero v1.11.0
@ -17,7 +16,7 @@ require (
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // 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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emirpasic/gods v1.18.1 // indirect
@ -45,14 +44,13 @@ require (
github.com/xanzy/ssh-agent v0.3.3 // indirect
go.uber.org/atomic 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/mod v0.17.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.13.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // 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/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
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/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/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/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.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/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
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.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.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
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/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
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.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
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.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.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
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-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.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
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.5.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.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
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.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
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.3/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.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.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
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-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.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.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
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=
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=

@ -1,27 +1,11 @@
/*
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
*/
package main
import (
"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)
}
import "github.com/Marcusk19/dotctl/cmd"
func main() {
SetVersionInfo(versioninfo.Version, versioninfo.Revision, versioninfo.LastCommit.Format(time.RFC3339))
cmd.Execute()
}

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

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

@ -8,36 +8,49 @@ import (
"testing"
"github.com/Marcusk19/dotctl/cmd"
"github.com/spf13/afero"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
)
func TestLinkCommand(t *testing.T) {
viper.Set("testing", true)
cmd.FileSystem = afero.NewMemMapFs()
fs := cmd.FileSystem
homedir := os.Getenv("HOME")
oldDotfilePath := viper.GetString("dotfile-path")
setUpTesting()
dotctl := cmd.RootCmd
actual := new(bytes.Buffer)
dotctl.SetOut(actual)
dotctl.SetErr(actual)
dotctl.SetArgs([]string{"link"})
dotctl.Execute()
fs.MkdirAll(filepath.Join(homedir, "dotfiles/dotctl"), 0755)
links := map[string]string{
"someconfig": filepath.Join(homedir, ".config/someconfig"),
}
viper.Set("links", links)
homedir := os.Getenv("HOME")
someconfig := filepath.Join(homedir, ".config/someconfig/")
somedot := filepath.Join(homedir, "dotfiles/someconfig/")
dotctl := cmd.RootCmd
actual := new(bytes.Buffer)
expected := fmt.Sprintf("%s,%s", someconfig, somedot)
dotctl.SetOut(actual)
dotctl.SetErr(actual)
dotctl.SetArgs([]string{"link"})
assert.Equal(t, expected, actual.String(), "actual differs from expected")
dotctl.Execute()
tearDownTesting(oldDotfilePath)
}
func setUpTesting() {
viper.Set("testing", true)
someconfig := filepath.Join(homedir, ".config/someconfig/")
somedot := filepath.Join(homedir, "dotfiles/someconfig/")
fs := cmd.FileSystem
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"
)
func CopyFile(os afero.Fs, srcFile, destFile string) error {
// helper function to copy files over
// ignore pre-existing git files
if strings.Contains(srcFile, ".git") {
return nil
}
func CopyFile(os afero.Fs, srcFile, destFile string) error{
// helper function to copy files over
// ignore pre-existing git files
if strings.Contains(srcFile, ".git") {
return nil
}
sourceFileStat, err := os.Stat(srcFile)
if err != nil {
return err
}
sourceFileStat, err := os.Stat(srcFile)
if err != nil {
return err
}
if !sourceFileStat.Mode().IsRegular() {
return fmt.Errorf("%s is not a regular file", srcFile)
}
if !sourceFileStat.Mode().IsRegular() {
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)
if err != nil {
fmt.Printf("Error creating destination file %s\n", destFile)
return err
}
defer destination.Close()
source, err := os.Open(srcFile)
if err != nil {
return err
}
defer source.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 {
os.Mkdir(destDir, 0755)
entries, err := afero.ReadDir(os, srcDir)
if err != nil {
return err
}
os.Mkdir(destDir, 0755)
entries, err := afero.ReadDir(os, srcDir)
if err != nil {
return err
}
for _, entry := range entries {
if entry.IsDir() {
if entry.Name() == ".git" {
continue
}
subDir := filepath.Join(srcDir, entry.Name())
destSubDir := filepath.Join(destDir, entry.Name())
err := os.MkdirAll(destSubDir, entry.Mode().Perm())
if err != nil {
return err
}
CopyDir(os, subDir, destSubDir)
continue
}
sourcePath := filepath.Join(srcDir, entry.Name())
destPath := filepath.Join(destDir, entry.Name())
for _, entry := range(entries) {
if entry.IsDir() {
if entry.Name() == ".git" {
continue
}
subDir := filepath.Join(srcDir, entry.Name())
destSubDir := filepath.Join(destDir, entry.Name())
err := os.MkdirAll(destSubDir, entry.Mode().Perm())
if err != nil {
return err
}
CopyDir(os, subDir, destSubDir)
continue
}
sourcePath := filepath.Join(srcDir, entry.Name())
destPath := filepath.Join(destDir, entry.Name())
err := CopyFile(os, sourcePath, destPath)
if err != nil {
return err
}
}
err := CopyFile(os, sourcePath, destPath)
if err != nil {
return err
}
}
return nil
return nil
}

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

Loading…
Cancel
Save