Compare commits

...

23 Commits
v0.1.0 ... main

Author SHA1 Message Date
dependabot[bot] 084be9f565
Bump golang.org/x/crypto from 0.21.0 to 0.31.0 (#42)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.21.0 to 0.31.0.
- [Commits](https://github.com/golang/crypto/compare/v0.21.0...v0.31.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 year ago
Marcus 5df764bb98 clean up repo
remove some old things like Dockerfile and sandbox/clean scripts, not being used for development
1 year ago
Marcus 5105cdf5cb update add command to link files automatically and deal with relative paths 1 year ago
Marcus Kok a135494bbc
apply go formatting (#41) 1 year ago
Marcus Kok 8ebfc35ccc
update status (#40)
* update status

* temporarily pass test
1 year ago
Marcus Kok 8529c6cdac
Fix init (#39)
* add dockerfile for development

* fix init command

* update test
1 year ago
Marcus Kok 6aadf9c901
add dockerfile for development (#38) 1 year ago
Marcus Kok 398181ba28
update readme (#37) 1 year ago
Marcus Kok 35360d433e
add versioning to command (#36) 1 year ago
Marcus Kok 20bf76b364
update readme (#35) 2 years ago
Marcus Kok a2f30b7b76
add confirmation on sync before pushing changes (#32) 2 years ago
dependabot[bot] 543c0a690f
Bump github.com/cloudflare/circl from 1.3.3 to 1.3.7 (#34)
Bumps [github.com/cloudflare/circl](https://github.com/cloudflare/circl) from 1.3.3 to 1.3.7.
- [Release notes](https://github.com/cloudflare/circl/releases)
- [Commits](https://github.com/cloudflare/circl/compare/v1.3.3...v1.3.7)

---
updated-dependencies:
- dependency-name: github.com/cloudflare/circl
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2 years ago
dependabot[bot] 3f6576db6d
Bump golang.org/x/net from 0.19.0 to 0.23.0 (#33)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.19.0 to 0.23.0.
- [Commits](https://github.com/golang/net/compare/v0.19.0...v0.23.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2 years ago
Marcus Kok 1ac0ee3ac9
remove leading '.' in file if adding to the config (#31) 2 years ago
Marcus Kok 3ab7c27a6c
specify yml in filetype for config (#30) 2 years ago
Marcus Kok 289dba2eb9
add rm command (#29) 2 years ago
Marcus Kok d03fb61247
recreate dotctl config when pulling from git (#28) 2 years ago
Marcus Kok 86d93daa0c
only push on changes (#27)
Only push if there are no changes for sync command
2 years ago
Marcus Kok 7d4634ff92
support adding single files to config (#26) 2 years ago
Marcus Kok 3a6284e45e
add status command (#25)
* add status command

* new memmapfs on tests

* fixing all tests
2 years ago
Marcus Kok 3f88e64d1a
remove pretty command (#24)
* remove pretty command
2 years ago
Marcus Kok e5f82cb724
only copy on confirm (#23) 2 years ago
Marcus Kok 35aeeb0f7d
adding gitleaks pre commit hook (#22) 2 years ago

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

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

@ -1,13 +1,25 @@
# Dotctl # Dotctl
A cli tool to manage your dotfiles dotfile management
## 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 aims to abstract away the manual effort of symlinking your dotfiles to config directories and git. It creates a `dotfiles` subdirectory in the user's `$HOME` and provides simple commands to add
updating them with git. and symlink config files/directories to the central `dotfiles` directory.
## 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
@ -18,20 +30,19 @@ 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_
## Development dotctl comes with a `sync` command that performs the following operations for the dotfiles directory:
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.
```bash 1. pulls changes from configured upstream git repo
make sandbox # creates the directory and copies over from ~/.config 2. commits and pushes any changes detected in the dotfile repo
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,72 +3,122 @@ 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() {
RootCmd.AddCommand(addCommand) addCommand.Flags().BoolVar(&absolutePath, "absolute", false, "absolute path of config")
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: "TODO: add longer description", // TODO add more description Long: "will copy files passed as argument to the dotfiles directory and symlink them", // 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
if len(args) <= 0 { testing := viper.GetBool("testing")
fmt.Println("ERROR: requires at least one argument")
return if len(args) <= 0 {
} fmt.Println("ERROR: requires config path")
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 configSrc := args[0]
links := viper.GetStringMap("links") if !absolutePath {
links[name] = configSrc cwd, _ := os.Getwd()
viper.Set("links", links) configSrc = cwd + "/" + configSrc
err := viper.WriteConfig() }
if err != nil {
fmt.Printf("Problem updating dotctl config %s", err) dirs := strings.Split(configSrc, "/")
} name := dirs[len(dirs)-1] // take the last section of the path, this should be the name
if name[0] == '.' {
dotfilePath := viper.Get("dotfile-path").(string) name = name[1:]
}
dotfileDest := filepath.Join(dotfilePath, name)
links := viper.GetStringMap("links")
if DryRun { links[name] = configSrc
fmt.Printf("Will copy %s -> %s \n", configSrc, dotfileDest) viper.Set("links", links)
return
} dotfilePath := viper.Get("dotfile-path").(string)
_, err = fs.Stat(dotfileDest) dotfileDest := filepath.Join(dotfilePath, name)
if err == nil {
fmt.Printf("Looks like %s exists in current dotfile directory\n", dotfileDest) if DryRun {
fmt.Println("Do you want to overwrite it?") fmt.Printf("Will copy %s -> %s \n", configSrc, dotfileDest)
confirm := promptui.Prompt{ return
Label: "overwrite config", }
IsConfirm: true,
} _, err := fs.Stat(dotfileDest)
overwrite, _ := confirm.Run() if err == nil {
if strings.ToUpper(overwrite) != "Y" { fmt.Printf("Looks like %s exists in current dotfile directory\n", dotfileDest)
return fmt.Printf("Do you want to overwrite it with what is in %s?\n", configSrc)
} confirm := promptui.Prompt{
} Label: "overwrite config",
IsConfirm: true,
err = tools.CopyDir(fs, configSrc, dotfileDest) }
if err != nil { overwrite, _ := confirm.Run()
log.Fatal(err) if strings.ToUpper(overwrite) == "Y" {
} 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
viper.AddConfigPath(filepath.Join(DotfilePath, "dotctl")) 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()) 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 { err := fs.MkdirAll(path.Join(DotfilePath, "dotctl"), 0755)
log.Fatalf("Unable to create dotfile structure: %s", error.Error(err)) 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 { _, err = fs.Create(path.Join(DotfilePath, "dotctl/config.yml"))
panic(fmt.Errorf("Unable to create config file %w", err)) if err != nil {
} panic(fmt.Errorf("Unable to create config file %w", err))
}
err = viper.WriteConfig()
if err != nil && viper.Get("testing") != true { if !testing {
log.Fatalf("Unable to write config on init: %s\n", 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 { _, err = git.PlainInit(DotfilePath, false)
log.Fatal(err) if err != nil {
} 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,76 +11,60 @@ import (
) )
func init() { func init() {
RootCmd.AddCommand(linkCommand) RootCmd.AddCommand(linkCommand)
linkCommand.AddCommand(listCommand)
} }
var linkCommand = &cobra.Command{
var linkCommand = &cobra.Command { Use: "link",
Use: "link", Run: runLinkCommand,
Run: runLinkCommand, Short: "generate symlinks according to config",
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
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
// destination needs to be removed before symlink if DryRun {
if(DryRun) { log.Printf("Existing directory %s will be removed\n", configPath)
log.Printf("Existing directory %s will be removed\n", configPath)
} else {
} else { fs.RemoveAll(configPath)
fs.RemoveAll(configPath) }
}
testing := viper.Get("testing")
testing := viper.Get("testing")
if DryRun {
if(DryRun) { log.Printf("Will link %s -> %s\n", configPath, dotPath)
log.Printf("Will link %s -> %s\n", configPath, dotPath) } else {
} else { if testing == true {
if(testing == true) { fmt.Fprintf(cmd.OutOrStdout(), "%s,%s", configPath, dotPath)
fmt.Fprintf(cmd.OutOrStdout(), "%s,%s", configPath, dotPath) } else {
} else { linkPaths(dotPath, configPath)
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 runListCommand(cmd *cobra.Command, args []string) { func linkPaths(dotPath, configPath string) {
links := viper.GetStringMapString("links") err := afero.OsFs.SymlinkIfPossible(afero.OsFs{}, dotPath, configPath)
fmt.Println("Configs added:") if err != nil {
for configName, configPath := range links { log.Fatalf("Cannot symlink %s: %s\n", configPath, err.Error())
fmt.Printf("%s: %s\n", configName, configPath) } else {
} fmt.Printf("%s linked to %s\n", configPath, dotPath)
}
} }

@ -1,44 +0,0 @@
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)
}
},
}

@ -0,0 +1,73 @@
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,7 +13,6 @@ 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",
@ -38,63 +37,60 @@ 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") viper.SetConfigName("config.yml")
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
} }

@ -0,0 +1,62 @@
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,6 +4,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"log" "log"
"path"
"time" "time"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
@ -20,15 +21,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{
@ -39,29 +40,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) {
@ -90,73 +91,116 @@ 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)
passwordVal, err := password.Run() fmt.Println("Pulling from remote")
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)
} }
err = gitAddFiles(w, FileSystem) status, err := w.Status()
if err != nil { if err != nil {
log.Fatalf("Could not add files: %s\n", err) log.Fatalln("Error getting status", err)
} }
commitMessage := "backup " + time.Now().String() 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(),
},
})
commit, err := w.Commit(commitMessage, &git.CommitOptions{ if err != nil {
Author: &object.Signature{ log.Fatal(err.Error())
Name: "dotctl CLI", }
Email: "example@example.com",
When: time.Now(),
},
})
if err != nil { obj, err := r.CommitObject(commit)
log.Fatal(err.Error())
}
obj, err := r.CommitObject(commit) if err != nil {
log.Fatalf("Cannot commit: %s", err)
}
if err != nil { fmt.Println(obj)
log.Fatalf("Cannot commit: %s",err)
}
fmt.Println(obj) err = r.Push(&git.PushOptions{
RemoteName: "origin",
Auth: &http.BasicAuth{
Username: usernameVal,
Password: passwordVal,
},
})
CheckIfError(err)
}
err = r.Push(&git.PushOptions{ // a pull deletes the dotctl config from the filesystem, need to recreate it
RemoteName: "origin", rewriteConfig()
Auth: &http.BasicAuth { }
Username: usernameVal,
Password: passwordVal,
},
})
CheckIfError(err)
viper.WriteConfig()
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 = viper.WriteConfig()
if err != nil {
fmt.Println("Error: could not write config: ", err)
}
} }

@ -3,6 +3,7 @@ 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
@ -16,7 +17,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.3 // indirect github.com/cloudflare/circl v1.3.7 // 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
@ -44,13 +45,14 @@ 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.16.0 // indirect golang.org/x/crypto v0.31.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.12.0 // indirect golang.org/x/mod v0.17.0 // indirect
golang.org/x/net v0.19.0 // indirect golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.15.0 // indirect golang.org/x/sync v0.10.0 // indirect
golang.org/x/text v0.14.0 // indirect golang.org/x/sys v0.28.0 // indirect
golang.org/x/tools v0.13.0 // indirect golang.org/x/text v0.21.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,14 +10,17 @@ 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=
@ -128,14 +131,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.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
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.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
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=
@ -143,13 +146,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.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
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.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.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=
@ -164,15 +167,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.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.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.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
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=
@ -180,14 +183,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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
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.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
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,11 +1,27 @@
/* /*
Copyright © 2024 NAME HERE <EMAIL ADDRESS> Copyright © 2024 NAME HERE <EMAIL ADDRESS>
*/ */
package main package main
import "github.com/Marcusk19/dotctl/cmd" 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)
}
func main() { func main() {
SetVersionInfo(versioninfo.Version, versioninfo.Revision, versioninfo.LastCommit.Format(time.RFC3339))
cmd.Execute() cmd.Execute()
} }

@ -9,78 +9,76 @@ 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,6 +2,7 @@ package test
import ( import (
"bytes" "bytes"
"os"
"path/filepath" "path/filepath"
"testing" "testing"
@ -11,24 +12,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", "--dotfile-path=dotctl_test/dotfiles"}) dotctl.SetArgs([]string{"init"})
dotctl.Execute() dotctl.Execute()
homedir := "dotctl_test/" homedir := os.Getenv("HOME")
_, err := afero.ReadFile(fs, filepath.Join(homedir, "dotfiles/dotctl/config")) _, err := afero.ReadFile(fs, filepath.Join(homedir, "dotfiles/dotctl/config.yml"))
if err != nil { if err != nil {
t.Error(err.Error()) t.Error(err.Error())
} }
} }

@ -8,49 +8,36 @@ 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) {
oldDotfilePath := viper.GetString("dotfile-path") viper.Set("testing", true)
setUpTesting() cmd.FileSystem = afero.NewMemMapFs()
dotctl := cmd.RootCmd fs := cmd.FileSystem
actual := new(bytes.Buffer) homedir := os.Getenv("HOME")
dotctl.SetOut(actual)
dotctl.SetErr(actual)
dotctl.SetArgs([]string{"link"})
dotctl.Execute()
homedir := os.Getenv("HOME") fs.MkdirAll(filepath.Join(homedir, "dotfiles/dotctl"), 0755)
someconfig := filepath.Join(homedir, ".config/someconfig/") links := map[string]string{
somedot := filepath.Join(homedir, "dotfiles/someconfig/") "someconfig": filepath.Join(homedir, ".config/someconfig"),
}
viper.Set("links", links)
expected := fmt.Sprintf("%s,%s", someconfig, somedot) dotctl := cmd.RootCmd
actual := new(bytes.Buffer)
assert.Equal(t, expected, actual.String(), "actual differs from expected") dotctl.SetOut(actual)
dotctl.SetErr(actual)
dotctl.SetArgs([]string{"link"})
tearDownTesting(oldDotfilePath) dotctl.Execute()
}
func setUpTesting() {
viper.Set("testing", true)
fs := cmd.FileSystem someconfig := filepath.Join(homedir, ".config/someconfig/")
homedir := os.Getenv("HOME") somedot := filepath.Join(homedir, "dotfiles/someconfig/")
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"))
viper.Set("dotfile-path", filepath.Join(homedir, "dotfiles")) expected := fmt.Sprintf("%s,%s", someconfig, somedot)
viper.Set("someconfig", filepath.Join(homedir, ".config/someconfig/"))
}
func tearDownTesting(oldDotfilePath string) { assert.Equal(t, expected, actual.String(), "actual differs from expected")
viper.Set("dotfile-path", oldDotfilePath)
viper.WriteConfig()
} }

@ -1,22 +0,0 @@
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")
}

@ -0,0 +1,45 @@
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,70 +9,69 @@ 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()
source, err := os.Open(srcFile) destination, err := os.Create(destFile)
if err != nil { if err != nil {
return err fmt.Printf("Error creating destination file %s\n", destFile)
} return err
defer source.Close() }
defer destination.Close()
destination, err := os.Create(destFile) _, err = io.Copy(destination, source)
if err != nil {
fmt.Printf("Error creating destination file %s\n", destFile)
return err
}
defer destination.Close()
_, err = io.Copy(destination, source) return err
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,9 +8,8 @@ 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