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,57 +3,45 @@ 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)
}
var addCommand = &cobra.Command{
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
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")
fmt.Println("ERROR: requires at least one argument")
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:]
}
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)
@ -64,61 +52,23 @@ func runAddCommand(cmd *cobra.Command, args []string) {
return
}
_, err := fs.Stat(dotfileDest)
_, 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)
fmt.Println("Do you want to overwrite it?")
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)
if strings.ToUpper(overwrite) != "Y" {
return
}
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,6 +14,7 @@ import (
"github.com/spf13/viper"
)
func init() {
RootCmd.AddCommand(initCommand)
}
@ -23,7 +24,7 @@ func copyExistingConfigs(programs []string, fs afero.Fs) {
destRoot := DotfilePath
configRoot := ConfigPath
for _, program := range programs {
for _, program := range(programs) {
// TODO: do something here
err := tools.CopyDir(fs, filepath.Join(configRoot, program), filepath.Join(destRoot, program))
if err != nil {
@ -36,14 +37,14 @@ 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 {
for _, program := range(programs) {
if err := fs.MkdirAll(path.Join(dotfileRoot, program), os.ModePerm); err != nil {
log.Fatal(err)
}
}
}
var initCommand = &cobra.Command{
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",
@ -54,10 +55,9 @@ 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" {
if(viper.Get("testing") == true && fs.Name() != "MemMapFS") {
log.Fatalf("wrong filesystem, got %s", fs.Name())
}
@ -66,23 +66,23 @@ func runInitCommand(cmd *cobra.Command, args []string) {
log.Fatalf("Unable to create dotfile structure: %s", error.Error(err))
}
_, err = fs.Create(path.Join(DotfilePath, "dotctl/config.yml"))
_, err = fs.Create(path.Join(DotfilePath, "dotctl/config"))
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)
}
if (viper.Get("testing") != "true"){
_, err = git.PlainInit(DotfilePath, false)
if err != nil {
log.Fatal(err)
}
gitignoreContent := []byte(`
gitignoreContent := []byte (`
# ignore dotctl config for individual installations
dotctl/

@ -12,15 +12,18 @@ import (
func init() {
RootCmd.AddCommand(linkCommand)
linkCommand.AddCommand(listCommand)
}
var linkCommand = &cobra.Command{
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
Long: "add longer description", // TODO add longer description here
}
func runLinkCommand(cmd *cobra.Command, args []string) {
fs := FileSystem
fmt.Println("Symlinking dotfiles...")
@ -34,12 +37,13 @@ func runLinkCommand(cmd *cobra.Command, args []string) {
}
dotPath := filepath.Join(dotfileRoot, configName)
if configPath == "" {
if configPath == ""{
fmt.Fprintf(cmd.OutOrStdout(), "Warning: could not find config for %s\n", configName)
}
// destination needs to be removed before symlink
if DryRun {
if(DryRun) {
log.Printf("Existing directory %s will be removed\n", configPath)
} else {
@ -48,23 +52,35 @@ func runLinkCommand(cmd *cobra.Command, args []string) {
testing := viper.Get("testing")
if DryRun {
if(DryRun) {
log.Printf("Will link %s -> %s\n", configPath, dotPath)
} else {
if testing == true {
if(testing == true) {
fmt.Fprintf(cmd.OutOrStdout(), "%s,%s", configPath, dotPath)
} 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)
}
}
}
}
}
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)
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) {
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",
@ -39,6 +40,7 @@ var FileSystem afero.Fs
func init() {
// 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/"
@ -62,7 +64,7 @@ func init() {
viper.BindEnv("testing")
viper.SetDefault("testing", false)
viper.SetConfigName("config.yml")
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath("./tmp/dotfiles/dotctl")
viper.AddConfigPath(filepath.Join(DotfilePath, "dotctl"))
@ -81,7 +83,7 @@ func init() {
func UseFilesystem() afero.Fs {
testing := viper.Get("testing")
if testing == "true" {
if(testing == "true") {
return afero.NewMemMapFs()
} else {
return afero.NewOsFs()
@ -94,3 +96,5 @@ func CheckIfError(err error) {
}
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"
@ -53,8 +52,8 @@ func gitAddFiles(worktree *git.Worktree, fs afero.Fs) error {
if err != nil {
return err
}
for _, entry := range entries {
if entry.Name() == "dotctl" {
for _, entry := range(entries) {
if(entry.Name() == "dotctl") {
continue
}
_, err = worktree.Add(entry.Name())
@ -109,46 +108,23 @@ func runSyncCommand(cmd *cobra.Command, args []string) {
passwordVal, err := password.Run()
CheckIfError(err)
fmt.Println("Pulling from remote")
err = w.Pull(&git.PullOptions{
RemoteName: "origin",
Auth: &http.BasicAuth{
Auth: &http.BasicAuth {
Username: usernameVal,
Password: passwordVal,
},
})
if err != nil {
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()
@ -168,39 +144,19 @@ func runSyncCommand(cmd *cobra.Command, args []string) {
obj, err := r.CommitObject(commit)
if err != nil {
log.Fatalf("Cannot commit: %s", err)
log.Fatalf("Cannot commit: %s",err)
}
fmt.Println(obj)
err = r.Push(&git.PushOptions{
RemoteName: "origin",
Auth: &http.BasicAuth{
Auth: &http.BasicAuth {
Username: usernameVal,
Password: passwordVal,
},
})
CheckIfError(err)
}
// a pull deletes the dotctl config from the filesystem, need to recreate it
rewriteConfig()
}
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))
}
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()
}

@ -12,6 +12,7 @@ func init() {
tools.SetTestFs()
}
func TestCopyFile(t *testing.T) {
fs := afero.NewMemMapFs()
@ -82,3 +83,4 @@ func TestCopyDir(t *testing.T) {
}
}

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

@ -8,23 +8,14 @@ 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")
fs.MkdirAll(filepath.Join(homedir, "dotfiles/dotctl"), 0755)
links := map[string]string{
"someconfig": filepath.Join(homedir, ".config/someconfig"),
}
viper.Set("links", links)
func TestLinkCommand(t *testing.T) {
oldDotfilePath := viper.GetString("dotfile-path")
setUpTesting()
dotctl := cmd.RootCmd
actual := new(bytes.Buffer)
@ -34,10 +25,32 @@ func TestLinkCommand(t *testing.T) {
dotctl.Execute()
homedir := os.Getenv("HOME")
someconfig := filepath.Join(homedir, ".config/someconfig/")
somedot := filepath.Join(homedir, "dotfiles/someconfig/")
expected := fmt.Sprintf("%s,%s", someconfig, somedot)
assert.Equal(t, expected, actual.String(), "actual differs from expected")
tearDownTesting(oldDotfilePath)
}
func setUpTesting() {
viper.Set("testing", true)
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"))
viper.Set("dotfile-path", filepath.Join(homedir, "dotfiles"))
viper.Set("someconfig", filepath.Join(homedir, ".config/someconfig/"))
}
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,7 +9,7 @@ import (
"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
// ignore pre-existing git files
if strings.Contains(srcFile, ".git") {
@ -25,6 +25,7 @@ func CopyFile(os afero.Fs, srcFile, destFile string) error {
return fmt.Errorf("%s is not a regular file", srcFile)
}
source, err := os.Open(srcFile)
if err != nil {
return err
@ -50,7 +51,7 @@ func CopyDir(os afero.Fs, srcDir, destDir string) error {
return err
}
for _, entry := range entries {
for _, entry := range(entries) {
if entry.IsDir() {
if entry.Name() == ".git" {
continue

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

Loading…
Cancel
Save