diff --git a/.travis.yml b/.travis.yml index 5b26459..8326088 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,7 @@ language: go go: - - tip + - tip +before_install: + - go get github.com/mattn/goveralls +script: + - $HOME/gopath/bin/goveralls -service=travis-ci diff --git a/README.md b/README.md index 59f473d..169f6f3 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,12 @@ Dotbro remembers path to this file and use it in further runs. Dotbro cleans broken symlinks in your `$HOME` (or your another destination path). +#### Add command + +Dotbro can automate routine of adding files to your dotfiles repo with one single +command. It does a backup copy, moves the file and creates a symlink to your file. +After that you only need to add this file to your dotbro config (*I'm working on automation of this*) and commit that file to your repo. + # Configuration Configuration can be either TOML or JSON file. @@ -174,6 +180,10 @@ So just run: dotbro +To move a file to your dotfiles, perform an `add` command: + + dotbro add ./path-to-file + # Issues If you experience any problems, please submit an issue and attach dotbro log file, diff --git a/docopt.go b/docopt.go index e9d6cdd..6dd4b3e 100644 --- a/docopt.go +++ b/docopt.go @@ -2,22 +2,28 @@ package main import "github.com/docopt/docopt-go" -const version = "0.1.0" +const version = "0.2.0" func parseArguments() (map[string]interface{}, error) { usage := `dotbro - simple yet effective dotfiles manager. Usage: dotbro [options] [--config=] + dotbro add [options] dotbro -h | --help dotbro --version -Options: +Common options: -c --config= Dotbro's configuration file in JSON or TOML format. - -h --help Show this helpful info. -q --quiet Quiet mode. Do not print any output, except warnings and errors. -v --verbose Verbose mode. Detailed output. + +Add options: + File to add. + +Other options: + -h --help Show this helpful info. -V --version Show version. ` diff --git a/fileutils.go b/fileutils.go new file mode 100644 index 0000000..6831b54 --- /dev/null +++ b/fileutils.go @@ -0,0 +1,73 @@ +package main + +import ( + "fmt" + "io" + "os" + "path" +) + +// Copy copies a file from src to dst. +func Copy(src, dst string) error { + sfi, err := os.Lstat(src) + if err != nil { + return err + } + + if !sfi.Mode().IsRegular() { + return fmt.Errorf("Non-regular source file %s (%q)", sfi.Name(), sfi.Mode().String()) + } + + dfi, err := os.Stat(dst) + if err != nil { + if !os.IsNotExist(err) { + return err + } + // file not exists - do not do anything + } else { + // file exists - check it + if !(dfi.Mode().IsRegular()) { + return fmt.Errorf("Non-regular destination file %s (%q)", dfi.Name(), dfi.Mode().String()) + } + } + + err = copyFileContents(src, dst) + return err +} + +// copyFileContents copies the contents of the file named src to the file named +// by dst. The file will be created if it does not already exist. If the +// destination file exists, all it's contents will be replaced by the contents +// of the source file. +func copyFileContents(src, dst string) (err error) { + in, err := os.Open(src) + if err != nil { + return + } + + defer in.Close() + + err = os.MkdirAll(path.Dir(dst), 0755) + if err != nil { + return err + } + + out, err := os.Create(dst) + if err != nil { + return + } + + defer func() { + cerr := out.Close() + if err == nil { + err = cerr + } + }() + + if _, err = io.Copy(out, in); err != nil { + return err + } + + err = out.Sync() + return err +} diff --git a/fileutils_test.go b/fileutils_test.go new file mode 100644 index 0000000..a1b15e1 --- /dev/null +++ b/fileutils_test.go @@ -0,0 +1,121 @@ +package main + +import ( + "io/ioutil" + "os" + "path" + "testing" +) + +func TestCopy(t *testing.T) { + testCopyPositive(t) + testCopyNegativeLstat(t) + testCopyNegativeSymlink(t) +} + +func testCopyPositive(t *testing.T) { + // set up + + src := "/tmp/dotbro/fileutils/original.txt" + content := []byte("Some Content") + + if err := os.MkdirAll(path.Dir(src), 0755); err != nil { + t.Fatal(err) + } + + if err := ioutil.WriteFile(src, content, 0755); err != nil { + t.Fatal(err) + } + + // test + + dest := "/tmp/dotbro/fileutils/copy.txt" + if err := Copy(src, dest); err != nil { + t.Error(err) + } + + copyContent, err := ioutil.ReadFile(dest) + if err != nil { + t.Error(err) + } + + if string(copyContent) != string(content) { + t.Error(err) + } + + // tear down + + if err := os.Remove(src); err != nil { + t.Error(err) + } + + if err := os.Remove(dest); err != nil { + t.Error(err) + } +} + +func testCopyNegativeLstat(t *testing.T) { + // set up + + src := "/tmp/dotbro/fileutils/original.txt" + + if err := os.MkdirAll(path.Dir(src), 0755); err != nil { + t.Fatal(err) + } + + // no read permissions + if err := ioutil.WriteFile(src, nil, 0333); err != nil { + t.Fatal(err) + } + + // test + + dest := "/tmp/dotbro/fileutils/copy.txt" + err := Copy(dest, dest) + if err == nil { + t.Error("No error!") + } + + // tear down + + if err := os.Remove(src); err != nil { + t.Error(err) + } +} + +func testCopyNegativeSymlink(t *testing.T) { + // set up + + original := "/tmp/dotbro/fileutils/original.txt" + + if err := os.MkdirAll(path.Dir(original), 0755); err != nil { + t.Fatal(err) + } + + if err := ioutil.WriteFile(original, nil, 0755); err != nil { + t.Fatal(err) + } + + symlink := "/tmp/dotbro/fileutils/symlink" + if err := os.Symlink(original, symlink); err != nil { + t.Fatal(err) + } + + // test + + dest := "/tmp/dotbro/fileutils/symlink-copy.txt" + err := Copy(symlink, dest) + if err == nil { + t.Error("No error!") + } + + // tear down + + if err := os.Remove(original); err != nil { + t.Error(err) + } + + if err := os.Remove(symlink); err != nil { + t.Error(err) + } +} diff --git a/linker.go b/linker.go index 554c8b2..4dca0b3 100644 --- a/linker.go +++ b/linker.go @@ -3,6 +3,7 @@ package main import ( "os" "path" + "path/filepath" ) // processDest inspects destination path, and reports whether symlink and backup @@ -66,6 +67,27 @@ func backup(dest string, destAbs string, backupDir string) error { return err } +func backupCopy(filename, backupDir string) error { + rel := path.Base(filename) + abs, err := filepath.Abs(filename) + if err != nil { + return err + } + + backupPath := backupDir + "/" + rel + + // Create subdirectories, if need + dir := path.Dir(backupPath) + if err = os.MkdirAll(dir, 0755); err != nil { + return err + } + + outVerbose(" → backup %s to %s", abs, backupPath) + + err = Copy(filename, backupPath) + return err +} + // setSymlink symlinks scrAbs to destAbs func setSymlink(srcAbs string, destAbs string) error { var err error diff --git a/main.go b/main.go index ef45261..af99385 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,9 @@ package main import ( + "fmt" "os" + "path" "path/filepath" ) @@ -47,6 +49,22 @@ func main() { outVerbose("Destination dir: %s", config.Directories.Destination) } + // Select action + + switch { + case args["add"] == true: + filename := args[""].(string) + if err = addAction(filename, config); err != nil { + outError("%s", err) + exit(1) + } + + outInfo("`%s` was successfully added to your dotfiles!", filename) + exit(0) + } + + // Default action: install + err = cleanDeadSymlinks(config.Directories.Destination) if err != nil { outError("Error cleaning dead symlinks: %s", err) @@ -77,6 +95,47 @@ func main() { exit(0) } +func addAction(filename string, config Configuration) error { + fileInfo, err := os.Lstat(filename) + if err != nil { + if os.IsNotExist(err) { + return fmt.Errorf("%s: no such file or directory", filename) + } + return err + } + + if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink { + return fmt.Errorf("Cannot add file %s - it is a symlink", filename) + } + + if fileInfo.Mode().IsDir() { + return fmt.Errorf("Cannot add dir %s - directories are not supported yet.", filename) + } + + outVerbose("Adding file `%s` to dotfiles root `%s`", filename, config.Directories.Dotfiles) + + // backup file + err = backupCopy(filename, config.Directories.Backup) + if err != nil { + return fmt.Errorf("Cannot backup file %s: %s", filename, err) + } + + // move file to dotfiles root + newPath := config.Directories.Dotfiles + "/" + path.Base(filename) + if err = os.Rename(filename, newPath); err != nil { + return err + } + + // Add a symlink to the moved file + if err = setSymlink(newPath, filename); err != nil { + return err + } + + // TODO: write to config file + + return nil +} + func getConfigPath(args map[string]interface{}) string { var configPath string if args["--config"] == nil {