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:
TESTING=true go test -v ./test
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
A cli tool to manage your dotfiles
dotfile management
## About
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
updating them with git.
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.
## 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
@ -18,20 +30,19 @@ 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_
## 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.
dotctl comes with a `sync` command that performs the following operations for the dotfiles directory:
```bash
make sandbox # creates the directory and copies over from ~/.config
make clean # removes directory
```
1. pulls changes from configured upstream git repo
2. commits and pushes any changes detected in the dotfile repo
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 (
"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() {
RootCmd.AddCommand(addCommand)
addCommand.Flags().BoolVar(&absolutePath, "absolute", false, "absolute path of config")
RootCmd.AddCommand(addCommand)
}
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 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 absolutePath bool
func runAddCommand(cmd *cobra.Command, args []string) {
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)
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)
}

@ -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
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 (`
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(`
# 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,76 +11,60 @@ import (
)
func init() {
RootCmd.AddCommand(linkCommand)
linkCommand.AddCommand(listCommand)
RootCmd.AddCommand(linkCommand)
}
var linkCommand = &cobra.Command {
Use: "link",
Run: runLinkCommand,
Short: "generate symlinks according to config",
Long: "add longer description", // TODO add longer description here
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
}
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 {
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
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)
}
}
}
}
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)
}
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)
}
}

@ -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"
)
var RootCmd = &cobra.Command{
Use: "dotctl",
Short: "dotfile management",
@ -38,63 +37,60 @@ 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")
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.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()
}
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
}

@ -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"
"fmt"
"log"
"path"
"time"
"github.com/go-git/go-git/v5"
@ -20,15 +21,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{
@ -39,29 +40,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) {
@ -90,73 +91,116 @@ 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)
usernameVal, err := username.Run()
CheckIfError(err)
passwordVal, err := password.Run()
CheckIfError(err)
passwordVal, err := password.Run()
CheckIfError(err)
fmt.Println("Pulling from remote")
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)
}
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)
}
err = gitAddFiles(w, FileSystem)
if err != nil {
log.Fatalf("Could not add files: %s\n", 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{
Author: &object.Signature{
Name: "dotctl CLI",
Email: "example@example.com",
When: time.Now(),
},
})
if err != nil {
log.Fatal(err.Error())
}
if err != nil {
log.Fatal(err.Error())
}
obj, err := r.CommitObject(commit)
obj, err := r.CommitObject(commit)
if err != nil {
log.Fatalf("Cannot commit: %s", err)
}
if err != nil {
log.Fatalf("Cannot commit: %s",err)
}
fmt.Println(obj)
fmt.Println(obj)
err = r.Push(&git.PushOptions{
RemoteName: "origin",
Auth: &http.BasicAuth{
Username: usernameVal,
Password: passwordVal,
},
})
CheckIfError(err)
}
err = r.Push(&git.PushOptions{
RemoteName: "origin",
Auth: &http.BasicAuth {
Username: usernameVal,
Password: passwordVal,
},
})
CheckIfError(err)
viper.WriteConfig()
// 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))
}
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
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
@ -16,7 +17,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.3 // indirect
github.com/cloudflare/circl v1.3.7 // 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
@ -44,13 +45,14 @@ 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.16.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // 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
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
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,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/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=
@ -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.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.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
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/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.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/mod v0.12.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/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=
@ -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.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.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
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/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.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
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/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=
@ -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.5.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.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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/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.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
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.3/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.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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
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/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.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
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/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,11 +1,27 @@
/*
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
*/
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() {
SetVersionInfo(versioninfo.Version, versioninfo.Revision, versioninfo.LastCommit.Format(time.RFC3339))
cmd.Execute()
}

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

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

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