Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature add file #1

Merged
merged 6 commits into from
Sep 23, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down
12 changes: 9 additions & 3 deletions docopt.go
Original file line number Diff line number Diff line change
Expand Up @@ -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=<filepath>]
dotbro add [options] <filename>
dotbro -h | --help
dotbro --version

Options:
Common options:
-c --config=<filepath> 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:
<filename> File to add.

Other options:
-h --help Show this helpful info.
-V --version Show version.
`

Expand Down
73 changes: 73 additions & 0 deletions fileutils.go
Original file line number Diff line number Diff line change
@@ -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
}
121 changes: 121 additions & 0 deletions fileutils_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
22 changes: 22 additions & 0 deletions linker.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"os"
"path"
"path/filepath"
)

// processDest inspects destination path, and reports whether symlink and backup
Expand Down Expand Up @@ -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
Expand Down
59 changes: 59 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package main

import (
"fmt"
"os"
"path"
"path/filepath"
)

Expand Down Expand Up @@ -47,6 +49,22 @@ func main() {
outVerbose("Destination dir: %s", config.Directories.Destination)
}

// Select action

switch {
case args["add"] == true:
filename := args["<filename>"].(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)
Expand Down Expand Up @@ -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 {
Expand Down