Compare commits
118 Commits
v1.1.1
...
904c1be192
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
904c1be192 | ||
|
|
f443e96f9e | ||
|
|
0076588e1f | ||
|
|
919f033c8b | ||
|
|
dbc77a1b34 | ||
|
|
d02d118b28 | ||
|
|
a952c4f6bf | ||
|
|
e733dff818 | ||
|
|
41584dec6a | ||
|
|
005209703e | ||
|
|
ee4388b0f4 | ||
|
|
a44a514007 | ||
|
|
9431cb78af | ||
|
|
46c52769a6 | ||
|
|
fdb57cd846 | ||
|
|
ff387280d5 | ||
|
|
f09c62d922 | ||
|
|
dd7d52a25d | ||
|
|
f1630ece79 | ||
|
|
11ee8cdc0d | ||
|
|
ceb08cbe22 | ||
|
|
057e1cfc59 | ||
|
|
89f3000d8b | ||
|
|
36e3cb6bbf | ||
|
|
9ebae75e7d | ||
|
|
35e1909790 | ||
|
|
3b633ed326 | ||
|
|
fdf2da84dd | ||
|
|
e561566b46 | ||
|
|
dcef34c17d | ||
|
|
72d11c4a47 | ||
|
|
c2457bae9f | ||
|
|
001bd32bb3 | ||
|
|
7080321081 | ||
|
|
9d9cf66de6 | ||
|
|
9e9a940825 | ||
|
|
257e974c38 | ||
|
|
615e31428c | ||
|
|
8c2a1d0f84 | ||
|
|
62c934774b | ||
|
|
d3d6974b7b | ||
|
|
474d69da0b | ||
|
|
db6a513d1d | ||
|
|
ae343c4cab | ||
|
|
a2b365fb6f | ||
|
|
4cb2006f41 | ||
|
|
66347d307f | ||
|
|
8f92a1b4f0 | ||
|
|
7a2df591c0 | ||
|
|
4923265dea | ||
|
|
79421580e9 | ||
|
|
cabde9e5f1 | ||
|
|
0d60ae9d1a | ||
|
|
d5317b8e17 | ||
|
|
3b8a5b4be4 | ||
|
|
6590a1eeff | ||
|
|
693ae5f05e | ||
|
|
da3002f199 | ||
|
|
feaaab2fa4 | ||
|
|
59f75711a4 | ||
|
|
f24030b51f | ||
|
|
71bb120a12 | ||
|
|
85f46e01b1 | ||
|
|
c5b24b9b38 | ||
|
|
68460af45e | ||
|
|
5614b6b8b3 | ||
|
|
570b063632 | ||
|
|
1d398587d0 | ||
|
|
085853faaa | ||
|
|
21b4e344a9 | ||
|
|
a6194dfe8b | ||
|
|
5692194fa2 | ||
|
|
11745098c2 | ||
|
|
b1bb0c996c | ||
|
|
a62039da50 | ||
|
|
4bfd1c60c2 | ||
|
|
f0e11abb5b | ||
|
|
ed397bdaf8 | ||
|
|
2f5e20d963 | ||
|
|
cc83a4e1fa | ||
|
|
dcc5cb0bc1 | ||
|
|
978416d1e4 | ||
|
|
1c12c73e4b | ||
|
|
1016002638 | ||
|
|
6431a864ad | ||
|
|
42f661cfbf | ||
|
|
7632591681 | ||
|
|
a9a5b81dc5 | ||
|
|
721c10cffd | ||
|
|
332aad8ad0 | ||
|
|
171b4c1fb8 | ||
|
|
60d4458bbc | ||
|
|
9ad171ab78 | ||
|
|
5918746059 | ||
|
|
4641843ffd | ||
|
|
1a181b907c | ||
|
|
fb7595d254 | ||
|
|
c8f0999035 | ||
|
|
46faec7857 | ||
|
|
e35d3fe6ba | ||
|
|
ba620e0f7f | ||
|
|
5700f55dc3 | ||
|
|
2c92010093 | ||
|
|
03490531d8 | ||
|
|
7bd9759e81 | ||
|
|
a808f56caf | ||
|
|
b7e2b45e69 | ||
|
|
63c45d7c3a | ||
|
|
096067ac62 | ||
|
|
8d6bf4c0c5 | ||
|
|
882b862780 | ||
|
|
e06a5d6300 | ||
|
|
7451e8c739 | ||
|
|
f034f773c5 | ||
|
|
681fd98dc3 | ||
|
|
e57b139e32 | ||
|
|
b64bfe2bb6 | ||
|
|
ee04b5788a |
12
.gitignore
vendored
12
.gitignore
vendored
@@ -46,3 +46,15 @@ pkg
|
|||||||
|
|
||||||
Gemfile.lock
|
Gemfile.lock
|
||||||
vendor/
|
vendor/
|
||||||
|
|
||||||
|
homesick*.gem
|
||||||
|
|
||||||
|
# Go scaffolding artifacts
|
||||||
|
dist/
|
||||||
|
*.test
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# rbenv configuration
|
||||||
|
.ruby-version
|
||||||
|
|
||||||
|
.github/*
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
language: ruby
|
language: ruby
|
||||||
rvm:
|
rvm:
|
||||||
- 2.1.0
|
- 2.5.0
|
||||||
- 2.0.0
|
- 2.4.0
|
||||||
- 1.9.3
|
- 2.3.3
|
||||||
|
- 2.2.6
|
||||||
|
sudo: false
|
||||||
|
|||||||
@@ -1,8 +1,34 @@
|
|||||||
#1.1.0
|
# 1.1.6
|
||||||
|
* Makesure the FileUtils is imported correctly to avoid a potential error
|
||||||
|
* Fixes an issue where comparing a diff would not use the content of the new file
|
||||||
|
* Small documentation fixes
|
||||||
|
|
||||||
|
# 1.1.5
|
||||||
|
* Fixed problem with version number being incorrect.
|
||||||
|
|
||||||
|
# 1.1.4
|
||||||
|
* Make sure symlink conflicts are explicitly communicated to a user and symlinks are not silently overwritten
|
||||||
|
* Use real paths of symlinks when linking a castle into home
|
||||||
|
* Fix a problem when in a diff when asking a user to resolve a conflict
|
||||||
|
* Some code refactoring and fixes
|
||||||
|
|
||||||
|
# 1.1.3
|
||||||
|
* Allow a destination to be passed when cloning a castle
|
||||||
|
* Make sure `homesick edit` opens default editor in the root of the given castle
|
||||||
|
* Fixed bug when diffing edited files
|
||||||
|
* Fixed crashing bug when attempting to diff directories
|
||||||
|
* Ensure that messages are escaped correctly on `git commit all`
|
||||||
|
|
||||||
|
# 1.1.2
|
||||||
|
* Added '--force' option to the rc command to bypass confirmation checks when running a .homesickrc file
|
||||||
|
* Added a check to make sure that a minimum of Git 1.8.0 is installed. This stops Homesick failing silently if Git is not installed.
|
||||||
|
* Code refactoring and fixes.
|
||||||
|
|
||||||
|
# 1.1.0
|
||||||
* Added exec and exec_all commands to run commands inside one or all clones castles.
|
* Added exec and exec_all commands to run commands inside one or all clones castles.
|
||||||
* Code refactoring.
|
* Code refactoring.
|
||||||
|
|
||||||
#1.0.0
|
# 1.0.0
|
||||||
* Removed support for Ruby 1.8.7
|
* Removed support for Ruby 1.8.7
|
||||||
* Added a version command
|
* Added a version command
|
||||||
|
|
||||||
@@ -34,11 +60,11 @@
|
|||||||
* Introduce .homesick_subdir #39
|
* Introduce .homesick_subdir #39
|
||||||
|
|
||||||
# 0.8.1
|
# 0.8.1
|
||||||
*Fixed `homesick list` bug on ruby 2.0 #37
|
* Fixed `homesick list` bug on ruby 2.0 #37
|
||||||
|
|
||||||
# 0.8.0
|
# 0.8.0
|
||||||
* Introduce commit & push command
|
* Introduce commit & push command
|
||||||
* commit changes in castle and push to remote
|
* commit changes in castle and push to remote
|
||||||
* Enable recursive submodule update
|
* Enable recursive submodule update
|
||||||
* Git add when track
|
* Git add when track
|
||||||
|
|
||||||
|
|||||||
38
Gemfile
38
Gemfile
@@ -1,26 +1,36 @@
|
|||||||
require 'rbconfig'
|
|
||||||
source 'https://rubygems.org'
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
|
this_ruby = Gem::Version.new(RUBY_VERSION)
|
||||||
|
ruby_230 = Gem::Version.new('2.3.0')
|
||||||
|
|
||||||
# Add dependencies required to use your gem here.
|
# Add dependencies required to use your gem here.
|
||||||
gem "thor", ">= 0.14.0"
|
gem 'thor', '>= 0.14.0'
|
||||||
|
|
||||||
# Add dependencies to develop your gem here.
|
# Add dependencies to develop your gem here.
|
||||||
# Include everything needed to run rake, tests, features, etc.
|
# Include everything needed to run rake, tests, features, etc.
|
||||||
group :development do
|
group :development do
|
||||||
gem "rake", ">= 0.8.7"
|
gem 'capture-output', '~> 1.0.0'
|
||||||
gem "rspec", "~> 2.10"
|
|
||||||
gem "guard"
|
|
||||||
gem "guard-rspec"
|
|
||||||
gem "rb-readline", "~> 0.5.0"
|
|
||||||
gem "jeweler", ">= 1.6.2"
|
|
||||||
#gem "simplecov"
|
|
||||||
gem 'coveralls', require: false
|
gem 'coveralls', require: false
|
||||||
gem "test_construct"
|
gem 'guard'
|
||||||
gem "capture-output", "~> 1.0.0"
|
gem 'guard-rspec'
|
||||||
if RbConfig::CONFIG['host_os'] =~ /linux|freebsd|openbsd|sunos|solaris/
|
gem 'jeweler', '>= 1.6.2', '< 2.2' if this_ruby < ruby_230
|
||||||
|
gem 'jeweler', '>= 1.6.2' if this_ruby >= ruby_230
|
||||||
|
gem 'rake', '>= 0.8.7'
|
||||||
|
gem 'rb-readline', '~> 0.5.0'
|
||||||
|
gem 'rspec', '~> 3.5.0'
|
||||||
|
gem 'rubocop'
|
||||||
|
gem 'test_construct'
|
||||||
|
|
||||||
|
install_if -> { RUBY_PLATFORM =~ /linux|freebsd|openbsd|sunos|solaris/ } do
|
||||||
gem 'libnotify'
|
gem 'libnotify'
|
||||||
end
|
end
|
||||||
if RUBY_VERSION >= '1.9.2'
|
|
||||||
gem "rubocop"
|
install_if -> { RUBY_PLATFORM =~ /darwin/ } do
|
||||||
|
gem 'terminal-notifier-guard', '~> 1.7.0'
|
||||||
|
end
|
||||||
|
|
||||||
|
install_if -> { this_ruby < ruby_230 } do
|
||||||
|
gem 'listen', '< 3'
|
||||||
|
gem 'rack', '~> 2.0.6'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
# homesick
|
# homesick
|
||||||
|
|
||||||
[](http://badge.fury.io/rb/homesick)
|
[](http://badge.fury.io/rb/homesick)
|
||||||
[](https://travis-ci.org/technicalpickles/homesick)
|
[](https://travis-ci.org/technicalpickles/homesick)
|
||||||
[](https://gemnasium.com/technicalpickles/homesick)
|
[](https://gemnasium.com/technicalpickles/homesick)
|
||||||
[](https://coveralls.io/r/technicalpickles/homesick)
|
[](https://coveralls.io/r/technicalpickles/homesick)
|
||||||
[](https://codeclimate.com/github/technicalpickles/homesick)
|
[](https://codeclimate.com/github/technicalpickles/homesick)
|
||||||
[](https://gitter.im/technicalpickles/homesick)
|
[](https://gitter.im/technicalpickles/homesick)
|
||||||
|
|
||||||
Your home directory is your castle. Don't leave your dotfiles behind.
|
Your home directory is your castle. Don't leave your dotfiles behind.
|
||||||
|
|
||||||
@@ -13,8 +13,8 @@ Homesick is sorta like [rip](http://github.com/defunkt/rip), but for dotfiles. I
|
|||||||
|
|
||||||
We call a repository that is compatible with homesick to be a 'castle'. To act as a castle, a repository must be organized like so:
|
We call a repository that is compatible with homesick to be a 'castle'. To act as a castle, a repository must be organized like so:
|
||||||
|
|
||||||
* Contains a 'home' directory
|
- Contains a 'home' directory
|
||||||
* 'home' contains any number of files and directories that begin with '.'
|
- 'home' contains any number of files and directories that begin with '.'
|
||||||
|
|
||||||
To get started, install homesick first:
|
To get started, install homesick first:
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ Alternatively, if it's on github, there's a slightly shorter way:
|
|||||||
|
|
||||||
With the castle cloned, you can now link its contents into your home dir:
|
With the castle cloned, you can now link its contents into your home dir:
|
||||||
|
|
||||||
homesick symlink pickled-vim
|
homesick link pickled-vim
|
||||||
|
|
||||||
You can remove symlinks anytime when you don't need them anymore
|
You can remove symlinks anytime when you don't need them anymore
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ If you need to add further configuration steps you can add these in a file calle
|
|||||||
|
|
||||||
homesick rc CASTLE
|
homesick rc CASTLE
|
||||||
|
|
||||||
The contents of the .homesickrc file must be valid Ruby code as the file will be executed with Ruby's eval construct. The .homesickrc is also passed the current homesick object during its execution and this is available within the .homesickrc file as the 'self' variable.
|
The contents of the .homesickrc file must be valid Ruby code as the file will be executed with Ruby's eval construct. The .homesickrc is also passed the current homesick object during its execution and this is available within the .homesickrc file as the 'self' variable. As the rc operation can be destructive the command normally asks for confirmation before proceeding. You can bypass this by passing the '--force' option, for example `homesick rc --force CASTLE`.
|
||||||
|
|
||||||
If you're not sure what castles you have around, you can easily list them:
|
If you're not sure what castles you have around, you can easily list them:
|
||||||
|
|
||||||
@@ -82,9 +82,52 @@ If you ever want to see what version of homesick you have type:
|
|||||||
|
|
||||||
homesick version|-v|--version
|
homesick version|-v|--version
|
||||||
|
|
||||||
|
## Docker Behavior Validation (Ruby vs Go)
|
||||||
|
|
||||||
|
To preserve behavior while migrating from Ruby to Go, this repository includes a containerized behavior suite that runs Homesick commands in a clean environment and validates filesystem and git outcomes.
|
||||||
|
|
||||||
|
The suite creates a dedicated git repository inside the container to act as a test castle fixture, then validates behavior for:
|
||||||
|
|
||||||
|
- clone
|
||||||
|
- link / unlink
|
||||||
|
- track
|
||||||
|
- list / show_path
|
||||||
|
- status / diff
|
||||||
|
- version
|
||||||
|
|
||||||
|
Run against the current Ruby implementation:
|
||||||
|
|
||||||
|
./script/run-behavior-suite-docker.sh
|
||||||
|
|
||||||
|
Show full command output (including internal Homesick status lines) when needed:
|
||||||
|
|
||||||
|
./script/run-behavior-suite-docker.sh --verbose
|
||||||
|
|
||||||
|
This runner now builds an Alpine-based container and installs runtime dependencies with `apk`, so behavior validation is not tied to a Ruby base image.
|
||||||
|
|
||||||
|
Run against a future Go binary (example path):
|
||||||
|
|
||||||
|
HOMESICK_CMD="/workspace/dist/homesick" ./script/run-behavior-suite-docker.sh
|
||||||
|
|
||||||
|
The command under test is controlled with the `HOMESICK_CMD` environment variable. By running the same suite for both implementations, you can verify parity at the behavior level.
|
||||||
|
|
||||||
|
## Go Scaffold
|
||||||
|
|
||||||
|
Initial Go scaffolding now lives under [cmd/homesick](cmd/homesick) and [internal/homesick](internal/homesick).
|
||||||
|
|
||||||
|
Build the Go binary:
|
||||||
|
|
||||||
|
just go-build
|
||||||
|
|
||||||
|
Run behavior validation against the Go binary:
|
||||||
|
|
||||||
|
HOMESICK_CMD="/workspace/dist/homesick-go" just behavior-go
|
||||||
|
|
||||||
|
At this stage, the Go implementation includes a subset of commands (`clone`, `list`, `show_path`, `status`, `diff`, `version`) and intentionally reports clear errors for commands that are not ported yet.
|
||||||
|
|
||||||
## .homesick_subdir
|
## .homesick_subdir
|
||||||
|
|
||||||
`homesick symlink` basically makes symlink to only first depth in `castle/home`. If you want to link nested files/directories, please use .homesick_subdir.
|
`homesick link` basically makes symlink to only first depth in `castle/home`. If you want to link nested files/directories, please use .homesick_subdir.
|
||||||
|
|
||||||
For example, when you have castle like this:
|
For example, when you have castle like this:
|
||||||
|
|
||||||
@@ -114,7 +157,7 @@ castle/.homesick_subdir
|
|||||||
|
|
||||||
.config
|
.config
|
||||||
|
|
||||||
and run `homesick symlink CASTLE`. The result is:
|
and run `homesick link CASTLE`. The result is:
|
||||||
|
|
||||||
~
|
~
|
||||||
|-- .config
|
|-- .config
|
||||||
@@ -134,7 +177,7 @@ Or `homesick track NESTED_FILE CASTLE` adds a line automatically. For example:
|
|||||||
castle/.homesick_subdir
|
castle/.homesick_subdir
|
||||||
|
|
||||||
.config
|
.config
|
||||||
.emacs.d
|
.emacs.d
|
||||||
|
|
||||||
home directory
|
home directory
|
||||||
|
|
||||||
@@ -164,17 +207,17 @@ and castle
|
|||||||
|
|
||||||
Homesick is tested on the following Ruby versions:
|
Homesick is tested on the following Ruby versions:
|
||||||
|
|
||||||
* 1.9.3
|
- 2.2.6
|
||||||
* 2.0.0
|
- 2.3.3
|
||||||
* 2.1.0
|
- 2.4.0
|
||||||
|
|
||||||
## Note on Patches/Pull Requests
|
## Note on Patches/Pull Requests
|
||||||
|
|
||||||
* Fork the project.
|
- Fork the project.
|
||||||
* Make your feature addition or bug fix.
|
- Make your feature addition or bug fix.
|
||||||
* Add tests for it. This is important so I don't break it in a future version unintentionally.
|
- Add tests for it. This is important so I don't break it in a future version unintentionally.
|
||||||
* Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
- Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
||||||
* Send me a pull request. Bonus points for topic branches.
|
- Send me a pull request. Bonus points for topic branches.
|
||||||
|
|
||||||
## Need homesick without the ruby dependency?
|
## Need homesick without the ruby dependency?
|
||||||
|
|
||||||
12
cmd/homesick/main.go
Normal file
12
cmd/homesick/main.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.hrafn.xyz/aether/gosick/internal/homesick/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
exitCode := cli.Run(os.Args[1:], os.Stdout, os.Stderr)
|
||||||
|
os.Exit(exitCode)
|
||||||
|
}
|
||||||
13
docker/behavior/Dockerfile
Normal file
13
docker/behavior/Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
FROM alpine:3.21
|
||||||
|
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
bash \
|
||||||
|
ca-certificates \
|
||||||
|
git \
|
||||||
|
ruby \
|
||||||
|
ruby-thor
|
||||||
|
|
||||||
|
WORKDIR /workspace
|
||||||
|
COPY . /workspace
|
||||||
|
|
||||||
|
ENTRYPOINT ["/workspace/test/behavior/behavior_suite.sh"]
|
||||||
34
go.mod
Normal file
34
go.mod
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
module git.hrafn.xyz/aether/gosick
|
||||||
|
|
||||||
|
go 1.26
|
||||||
|
|
||||||
|
toolchain go1.26.1
|
||||||
|
|
||||||
|
require github.com/stretchr/testify v1.10.0
|
||||||
|
|
||||||
|
require github.com/go-git/go-git/v5 v5.14.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
dario.cat/mergo v1.0.0 // indirect
|
||||||
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
|
github.com/ProtonMail/go-crypto v1.1.5 // indirect
|
||||||
|
github.com/cloudflare/circl v1.6.0 // indirect
|
||||||
|
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
|
github.com/go-git/go-billy/v5 v5.6.2 // indirect
|
||||||
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||||
|
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||||
|
github.com/pjbgf/sha1cd v0.3.2 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||||
|
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||||
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
|
golang.org/x/crypto v0.35.0 // indirect
|
||||||
|
golang.org/x/net v0.35.0 // indirect
|
||||||
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
102
go.sum
Normal file
102
go.sum
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||||
|
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||||
|
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
|
github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4=
|
||||||
|
github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||||
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||||
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||||
|
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/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
|
||||||
|
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||||
|
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
|
||||||
|
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||||
|
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||||
|
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||||
|
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||||
|
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||||
|
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||||
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||||
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||||
|
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
|
||||||
|
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
|
||||||
|
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||||
|
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||||
|
github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60=
|
||||||
|
github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k=
|
||||||
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||||
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||||
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||||
|
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
||||||
|
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||||
|
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
||||||
|
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
|
||||||
|
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
|
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||||
|
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||||
|
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||||
|
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
|
||||||
|
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||||
|
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||||
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
|
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||||
|
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||||
|
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||||
|
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||||
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||||
|
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||||
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
|
golang.org/x/sys v0.30.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.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||||
|
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||||
|
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
107
homesick.gemspec
107
homesick.gemspec
@@ -2,19 +2,19 @@
|
|||||||
# DO NOT EDIT THIS FILE DIRECTLY
|
# DO NOT EDIT THIS FILE DIRECTLY
|
||||||
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
||||||
# -*- encoding: utf-8 -*-
|
# -*- encoding: utf-8 -*-
|
||||||
# stub: homesick 1.1.1 ruby lib
|
# stub: homesick 1.1.6 ruby lib
|
||||||
|
|
||||||
Gem::Specification.new do |s|
|
Gem::Specification.new do |s|
|
||||||
s.name = "homesick"
|
s.name = "homesick".freeze
|
||||||
s.version = "1.1.1"
|
s.version = "1.1.6"
|
||||||
|
|
||||||
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
|
||||||
s.require_paths = ["lib"]
|
s.require_paths = ["lib".freeze]
|
||||||
s.authors = ["Joshua Nichols", "Yusuke Murata"]
|
s.authors = ["Joshua Nichols".freeze, "Yusuke Murata".freeze]
|
||||||
s.date = "2014-05-22"
|
s.date = "2017-12-20"
|
||||||
s.description = "\n Your home directory is your castle. Don't leave your dotfiles behind.\n \n\n Homesick is sorta like rip, but for dotfiles. It uses git to clone a repository containing dotfiles, and saves them in ~/.homesick. It then allows you to symlink all the dotfiles into place with a single command. \n\n "
|
s.description = "\n Your home directory is your castle. Don't leave your dotfiles behind.\n \n\n Homesick is sorta like rip, but for dotfiles. It uses git to clone a repository containing dotfiles, and saves them in ~/.homesick. It then allows you to symlink all the dotfiles into place with a single command. \n\n ".freeze
|
||||||
s.email = ["josh@technicalpickles.com", "info@muratayusuke.com"]
|
s.email = ["josh@technicalpickles.com".freeze, "info@muratayusuke.com".freeze]
|
||||||
s.executables = ["homesick"]
|
s.executables = ["homesick".freeze]
|
||||||
s.extra_rdoc_files = [
|
s.extra_rdoc_files = [
|
||||||
"ChangeLog.markdown",
|
"ChangeLog.markdown",
|
||||||
"LICENSE",
|
"LICENSE",
|
||||||
@@ -37,58 +37,69 @@ Gem::Specification.new do |s|
|
|||||||
"lib/homesick/actions/file_actions.rb",
|
"lib/homesick/actions/file_actions.rb",
|
||||||
"lib/homesick/actions/git_actions.rb",
|
"lib/homesick/actions/git_actions.rb",
|
||||||
"lib/homesick/cli.rb",
|
"lib/homesick/cli.rb",
|
||||||
"lib/homesick/shell.rb",
|
|
||||||
"lib/homesick/utils.rb",
|
"lib/homesick/utils.rb",
|
||||||
"lib/homesick/version.rb",
|
"lib/homesick/version.rb",
|
||||||
"spec/homesick_cli_spec.rb",
|
"spec/homesick_cli_spec.rb",
|
||||||
"spec/spec.opts",
|
"spec/spec.opts",
|
||||||
"spec/spec_helper.rb"
|
"spec/spec_helper.rb"
|
||||||
]
|
]
|
||||||
s.homepage = "http://github.com/technicalpickles/homesick"
|
s.homepage = "http://github.com/technicalpickles/homesick".freeze
|
||||||
s.licenses = ["MIT"]
|
s.licenses = ["MIT".freeze]
|
||||||
s.rubygems_version = "2.2.2"
|
s.rubygems_version = "2.6.11".freeze
|
||||||
s.summary = "Your home directory is your castle. Don't leave your dotfiles behind."
|
s.summary = "Your home directory is your castle. Don't leave your dotfiles behind.".freeze
|
||||||
|
|
||||||
if s.respond_to? :specification_version then
|
if s.respond_to? :specification_version then
|
||||||
s.specification_version = 4
|
s.specification_version = 4
|
||||||
|
|
||||||
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
||||||
s.add_runtime_dependency(%q<thor>, [">= 0.14.0"])
|
s.add_runtime_dependency(%q<thor>.freeze, [">= 0.14.0"])
|
||||||
s.add_development_dependency(%q<rake>, [">= 0.8.7"])
|
s.add_development_dependency(%q<capture-output>.freeze, ["~> 1.0.0"])
|
||||||
s.add_development_dependency(%q<rspec>, ["~> 2.10"])
|
s.add_development_dependency(%q<coveralls>.freeze, [">= 0"])
|
||||||
s.add_development_dependency(%q<guard>, [">= 0"])
|
s.add_development_dependency(%q<guard>.freeze, [">= 0"])
|
||||||
s.add_development_dependency(%q<guard-rspec>, [">= 0"])
|
s.add_development_dependency(%q<guard-rspec>.freeze, [">= 0"])
|
||||||
s.add_development_dependency(%q<rb-readline>, ["~> 0.5.0"])
|
s.add_development_dependency(%q<jeweler>.freeze, [">= 1.6.2"])
|
||||||
s.add_development_dependency(%q<jeweler>, [">= 1.6.2"])
|
s.add_development_dependency(%q<rake>.freeze, [">= 0.8.7"])
|
||||||
s.add_development_dependency(%q<coveralls>, [">= 0"])
|
s.add_development_dependency(%q<rb-readline>.freeze, ["~> 0.5.0"])
|
||||||
s.add_development_dependency(%q<test_construct>, [">= 0"])
|
s.add_development_dependency(%q<rspec>.freeze, ["~> 3.5.0"])
|
||||||
s.add_development_dependency(%q<capture-output>, ["~> 1.0.0"])
|
s.add_development_dependency(%q<rubocop>.freeze, [">= 0"])
|
||||||
s.add_development_dependency(%q<rubocop>, [">= 0"])
|
s.add_development_dependency(%q<test_construct>.freeze, [">= 0"])
|
||||||
|
s.add_development_dependency(%q<libnotify>.freeze, [">= 0"])
|
||||||
|
s.add_development_dependency(%q<terminal-notifier-guard>.freeze, ["~> 1.7.0"])
|
||||||
|
s.add_development_dependency(%q<listen>.freeze, ["< 3"])
|
||||||
|
s.add_development_dependency(%q<rack>.freeze, ["< 2"])
|
||||||
else
|
else
|
||||||
s.add_dependency(%q<thor>, [">= 0.14.0"])
|
s.add_dependency(%q<thor>.freeze, [">= 0.14.0"])
|
||||||
s.add_dependency(%q<rake>, [">= 0.8.7"])
|
s.add_dependency(%q<capture-output>.freeze, ["~> 1.0.0"])
|
||||||
s.add_dependency(%q<rspec>, ["~> 2.10"])
|
s.add_dependency(%q<coveralls>.freeze, [">= 0"])
|
||||||
s.add_dependency(%q<guard>, [">= 0"])
|
s.add_dependency(%q<guard>.freeze, [">= 0"])
|
||||||
s.add_dependency(%q<guard-rspec>, [">= 0"])
|
s.add_dependency(%q<guard-rspec>.freeze, [">= 0"])
|
||||||
s.add_dependency(%q<rb-readline>, ["~> 0.5.0"])
|
s.add_dependency(%q<jeweler>.freeze, [">= 1.6.2"])
|
||||||
s.add_dependency(%q<jeweler>, [">= 1.6.2"])
|
s.add_dependency(%q<rake>.freeze, [">= 0.8.7"])
|
||||||
s.add_dependency(%q<coveralls>, [">= 0"])
|
s.add_dependency(%q<rb-readline>.freeze, ["~> 0.5.0"])
|
||||||
s.add_dependency(%q<test_construct>, [">= 0"])
|
s.add_dependency(%q<rspec>.freeze, ["~> 3.5.0"])
|
||||||
s.add_dependency(%q<capture-output>, ["~> 1.0.0"])
|
s.add_dependency(%q<rubocop>.freeze, [">= 0"])
|
||||||
s.add_dependency(%q<rubocop>, [">= 0"])
|
s.add_dependency(%q<test_construct>.freeze, [">= 0"])
|
||||||
|
s.add_dependency(%q<libnotify>.freeze, [">= 0"])
|
||||||
|
s.add_dependency(%q<terminal-notifier-guard>.freeze, ["~> 1.7.0"])
|
||||||
|
s.add_dependency(%q<listen>.freeze, ["< 3"])
|
||||||
|
s.add_dependency(%q<rack>.freeze, ["< 2"])
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
s.add_dependency(%q<thor>, [">= 0.14.0"])
|
s.add_dependency(%q<thor>.freeze, [">= 0.14.0"])
|
||||||
s.add_dependency(%q<rake>, [">= 0.8.7"])
|
s.add_dependency(%q<capture-output>.freeze, ["~> 1.0.0"])
|
||||||
s.add_dependency(%q<rspec>, ["~> 2.10"])
|
s.add_dependency(%q<coveralls>.freeze, [">= 0"])
|
||||||
s.add_dependency(%q<guard>, [">= 0"])
|
s.add_dependency(%q<guard>.freeze, [">= 0"])
|
||||||
s.add_dependency(%q<guard-rspec>, [">= 0"])
|
s.add_dependency(%q<guard-rspec>.freeze, [">= 0"])
|
||||||
s.add_dependency(%q<rb-readline>, ["~> 0.5.0"])
|
s.add_dependency(%q<jeweler>.freeze, [">= 1.6.2"])
|
||||||
s.add_dependency(%q<jeweler>, [">= 1.6.2"])
|
s.add_dependency(%q<rake>.freeze, [">= 0.8.7"])
|
||||||
s.add_dependency(%q<coveralls>, [">= 0"])
|
s.add_dependency(%q<rb-readline>.freeze, ["~> 0.5.0"])
|
||||||
s.add_dependency(%q<test_construct>, [">= 0"])
|
s.add_dependency(%q<rspec>.freeze, ["~> 3.5.0"])
|
||||||
s.add_dependency(%q<capture-output>, ["~> 1.0.0"])
|
s.add_dependency(%q<rubocop>.freeze, [">= 0"])
|
||||||
s.add_dependency(%q<rubocop>, [">= 0"])
|
s.add_dependency(%q<test_construct>.freeze, [">= 0"])
|
||||||
|
s.add_dependency(%q<libnotify>.freeze, [">= 0"])
|
||||||
|
s.add_dependency(%q<terminal-notifier-guard>.freeze, ["~> 1.7.0"])
|
||||||
|
s.add_dependency(%q<listen>.freeze, ["< 3"])
|
||||||
|
s.add_dependency(%q<rack>.freeze, ["< 2"])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
129
internal/homesick/cli/cli.go
Normal file
129
internal/homesick/cli/cli.go
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.hrafn.xyz/aether/gosick/internal/homesick/core"
|
||||||
|
"git.hrafn.xyz/aether/gosick/internal/homesick/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Run(args []string, stdout io.Writer, stderr io.Writer) int {
|
||||||
|
app, err := core.New(stdout, stderr)
|
||||||
|
if err != nil {
|
||||||
|
_, _ = fmt.Fprintf(stderr, "error: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) == 0 {
|
||||||
|
printHelp(stdout)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
command := args[0]
|
||||||
|
switch command {
|
||||||
|
case "-v", "--version", "version":
|
||||||
|
if err := app.Version(version.String); err != nil {
|
||||||
|
_, _ = fmt.Fprintf(stderr, "error: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
case "help", "--help", "-h":
|
||||||
|
printHelp(stdout)
|
||||||
|
return 0
|
||||||
|
case "clone":
|
||||||
|
if len(args) < 2 {
|
||||||
|
_, _ = fmt.Fprintln(stderr, "error: clone requires URI")
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
destination := ""
|
||||||
|
if len(args) > 2 {
|
||||||
|
destination = args[2]
|
||||||
|
}
|
||||||
|
if err := app.Clone(args[1], destination); err != nil {
|
||||||
|
_, _ = fmt.Fprintf(stderr, "error: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
case "list":
|
||||||
|
if err := app.List(); err != nil {
|
||||||
|
_, _ = fmt.Fprintf(stderr, "error: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
case "show_path":
|
||||||
|
castle := defaultCastle(args)
|
||||||
|
if err := app.ShowPath(castle); err != nil {
|
||||||
|
_, _ = fmt.Fprintf(stderr, "error: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
case "status":
|
||||||
|
castle := defaultCastle(args)
|
||||||
|
if err := app.Status(castle); err != nil {
|
||||||
|
_, _ = fmt.Fprintf(stderr, "error: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
case "diff":
|
||||||
|
castle := defaultCastle(args)
|
||||||
|
if err := app.Diff(castle); err != nil {
|
||||||
|
_, _ = fmt.Fprintf(stderr, "error: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
case "link":
|
||||||
|
castle := defaultCastle(args)
|
||||||
|
if err := app.LinkCastle(castle); err != nil {
|
||||||
|
_, _ = fmt.Fprintf(stderr, "error: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
case "unlink":
|
||||||
|
castle := defaultCastle(args)
|
||||||
|
if err := app.Unlink(castle); err != nil {
|
||||||
|
_, _ = fmt.Fprintf(stderr, "error: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
case "track", "pull", "push", "commit", "destroy", "cd", "open", "exec", "exec_all", "rc", "generate":
|
||||||
|
_, _ = fmt.Fprintf(stderr, "error: %s is not implemented in Go yet\n", command)
|
||||||
|
return 2
|
||||||
|
default:
|
||||||
|
_, _ = fmt.Fprintf(stderr, "error: unknown command %q\n\n", command)
|
||||||
|
printHelp(stderr)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultCastle(args []string) string {
|
||||||
|
if len(args) > 1 && strings.TrimSpace(args[1]) != "" {
|
||||||
|
return args[1]
|
||||||
|
}
|
||||||
|
return "dotfiles"
|
||||||
|
}
|
||||||
|
|
||||||
|
func printHelp(w io.Writer) {
|
||||||
|
_, _ = fmt.Fprintln(w, "homesick (Go scaffold)")
|
||||||
|
_, _ = fmt.Fprintln(w, "")
|
||||||
|
_, _ = fmt.Fprintln(w, "Implemented commands:")
|
||||||
|
_, _ = fmt.Fprintln(w, " clone URI [CASTLE_NAME]")
|
||||||
|
_, _ = fmt.Fprintln(w, " list")
|
||||||
|
_, _ = fmt.Fprintln(w, " show_path [CASTLE]")
|
||||||
|
_, _ = fmt.Fprintln(w, " status [CASTLE]")
|
||||||
|
_, _ = fmt.Fprintln(w, " diff [CASTLE]")
|
||||||
|
_, _ = fmt.Fprintln(w, " link [CASTLE]")
|
||||||
|
_, _ = fmt.Fprintln(w, " unlink [CASTLE]")
|
||||||
|
_, _ = fmt.Fprintln(w, " version | -v | --version")
|
||||||
|
_, _ = fmt.Fprintln(w, "")
|
||||||
|
_, _ = fmt.Fprintln(w, "Not implemented yet:")
|
||||||
|
_, _ = fmt.Fprintln(w, " track, pull, push, commit, destroy, cd, open, exec, exec_all, rc, generate")
|
||||||
|
_, _ = fmt.Fprintln(w, "")
|
||||||
|
_, _ = fmt.Fprintln(w, "Build: go build -o dist/homesick-go ./cmd/homesick")
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
_ = os.Setenv("GIT_TERMINAL_PROMPT", "0")
|
||||||
|
}
|
||||||
103
internal/homesick/core/clone_test.go
Normal file
103
internal/homesick/core/clone_test.go
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package core_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.hrafn.xyz/aether/gosick/internal/homesick/core"
|
||||||
|
git "github.com/go-git/go-git/v5"
|
||||||
|
"github.com/go-git/go-git/v5/config"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing/object"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CloneSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
tmpDir string
|
||||||
|
homeDir string
|
||||||
|
reposDir string
|
||||||
|
app *core.App
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCloneSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(CloneSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CloneSuite) SetupTest() {
|
||||||
|
s.tmpDir = s.T().TempDir()
|
||||||
|
s.homeDir = filepath.Join(s.tmpDir, "home")
|
||||||
|
s.reposDir = filepath.Join(s.homeDir, ".homesick", "repos")
|
||||||
|
require.NoError(s.T(), os.MkdirAll(s.reposDir, 0o755))
|
||||||
|
|
||||||
|
s.app = &core.App{
|
||||||
|
HomeDir: s.homeDir,
|
||||||
|
ReposDir: s.reposDir,
|
||||||
|
Stdout: io.Discard,
|
||||||
|
Stderr: io.Discard,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CloneSuite) createBareRemote(name string) string {
|
||||||
|
remotePath := filepath.Join(s.tmpDir, name+".git")
|
||||||
|
_, err := git.PlainInit(remotePath, true)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
|
||||||
|
workPath := filepath.Join(s.tmpDir, name+"-work")
|
||||||
|
repo, err := git.PlainInit(workPath, false)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
|
||||||
|
castleFile := filepath.Join(workPath, "home", ".vimrc")
|
||||||
|
require.NoError(s.T(), os.MkdirAll(filepath.Dir(castleFile), 0o755))
|
||||||
|
require.NoError(s.T(), os.WriteFile(castleFile, []byte("set number\n"), 0o644))
|
||||||
|
|
||||||
|
wt, err := repo.Worktree()
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
_, err = wt.Add("home/.vimrc")
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
|
||||||
|
_, err = wt.Commit("initial", &git.CommitOptions{Author: &object.Signature{
|
||||||
|
Name: "Behavior Test",
|
||||||
|
Email: "behavior@test.local",
|
||||||
|
When: time.Now(),
|
||||||
|
}})
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
|
||||||
|
_, err = repo.CreateRemote(&config.RemoteConfig{Name: "origin", URLs: []string{remotePath}})
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
require.NoError(s.T(), repo.Push(&git.PushOptions{RemoteName: "origin"}))
|
||||||
|
|
||||||
|
return remotePath
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CloneSuite) TestClone_FileURLWorks() {
|
||||||
|
remotePath := s.createBareRemote("castle")
|
||||||
|
|
||||||
|
err := s.app.Clone("file://"+remotePath, "parity-castle")
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
require.FileExists(s.T(), filepath.Join(s.reposDir, "parity-castle", "home", ".vimrc"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CloneSuite) TestClone_DerivesDestinationWhenMissing() {
|
||||||
|
remotePath := s.createBareRemote("dotfiles")
|
||||||
|
|
||||||
|
err := s.app.Clone("file://"+remotePath, "")
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
require.DirExists(s.T(), filepath.Join(s.reposDir, "dotfiles"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CloneSuite) TestClone_LocalPathSymlinksDirectory() {
|
||||||
|
localCastle := filepath.Join(s.tmpDir, "local-castle")
|
||||||
|
require.NoError(s.T(), os.MkdirAll(filepath.Join(localCastle, "home"), 0o755))
|
||||||
|
|
||||||
|
err := s.app.Clone(localCastle, "")
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
|
||||||
|
destination := filepath.Join(s.reposDir, "local-castle")
|
||||||
|
info, err := os.Lstat(destination)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
require.True(s.T(), info.Mode()&os.ModeSymlink != 0)
|
||||||
|
}
|
||||||
440
internal/homesick/core/core.go
Normal file
440
internal/homesick/core/core.go
Normal file
@@ -0,0 +1,440 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
git "github.com/go-git/go-git/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type App struct {
|
||||||
|
HomeDir string
|
||||||
|
ReposDir string
|
||||||
|
Stdout io.Writer
|
||||||
|
Stderr io.Writer
|
||||||
|
Verbose bool
|
||||||
|
Force bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(stdout io.Writer, stderr io.Writer) (*App, error) {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("resolve home directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &App{
|
||||||
|
HomeDir: home,
|
||||||
|
ReposDir: filepath.Join(home, ".homesick", "repos"),
|
||||||
|
Stdout: stdout,
|
||||||
|
Stderr: stderr,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) Version(version string) error {
|
||||||
|
_, err := fmt.Fprintln(a.Stdout, version)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) ShowPath(castle string) error {
|
||||||
|
_, err := fmt.Fprintln(a.Stdout, filepath.Join(a.ReposDir, castle))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) Clone(uri string, destination string) error {
|
||||||
|
if uri == "" {
|
||||||
|
return errors.New("clone requires URI")
|
||||||
|
}
|
||||||
|
|
||||||
|
if destination == "" {
|
||||||
|
destination = deriveDestination(uri)
|
||||||
|
}
|
||||||
|
if destination == "" {
|
||||||
|
return fmt.Errorf("unable to derive destination from uri %q", uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(a.ReposDir, 0o755); err != nil {
|
||||||
|
return fmt.Errorf("create repos directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
destinationPath := filepath.Join(a.ReposDir, destination)
|
||||||
|
|
||||||
|
if fi, err := os.Stat(uri); err == nil && fi.IsDir() {
|
||||||
|
if err := os.Symlink(uri, destinationPath); err != nil {
|
||||||
|
return fmt.Errorf("symlink local castle: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := git.PlainClone(destinationPath, false, &git.CloneOptions{
|
||||||
|
URL: uri,
|
||||||
|
RecurseSubmodules: git.DefaultSubmoduleRecursionDepth,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("clone %q into %q failed: %w", uri, destinationPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) List() error {
|
||||||
|
if err := os.MkdirAll(a.ReposDir, 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var castles []string
|
||||||
|
err := filepath.WalkDir(a.ReposDir, func(path string, d fs.DirEntry, walkErr error) error {
|
||||||
|
if walkErr != nil {
|
||||||
|
return walkErr
|
||||||
|
}
|
||||||
|
if !d.IsDir() || d.Name() != ".git" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
castleRoot := filepath.Dir(path)
|
||||||
|
rel, err := filepath.Rel(a.ReposDir, castleRoot)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
castles = append(castles, rel)
|
||||||
|
return filepath.SkipDir
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(castles)
|
||||||
|
for _, castle := range castles {
|
||||||
|
castleRoot := filepath.Join(a.ReposDir, castle)
|
||||||
|
remote, remoteErr := gitOutput(castleRoot, "config", "remote.origin.url")
|
||||||
|
if remoteErr != nil {
|
||||||
|
remote = ""
|
||||||
|
}
|
||||||
|
_, writeErr := fmt.Fprintf(a.Stdout, "%s %s\n", castle, strings.TrimSpace(remote))
|
||||||
|
if writeErr != nil {
|
||||||
|
return writeErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) Status(castle string) error {
|
||||||
|
return runGit(filepath.Join(a.ReposDir, castle), "status")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) Diff(castle string) error {
|
||||||
|
return runGit(filepath.Join(a.ReposDir, castle), "diff")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) Link(castle string) error {
|
||||||
|
if strings.TrimSpace(castle) == "" {
|
||||||
|
castle = "dotfiles"
|
||||||
|
}
|
||||||
|
return a.LinkCastle(castle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) LinkCastle(castle string) error {
|
||||||
|
castleHome := filepath.Join(a.ReposDir, castle, "home")
|
||||||
|
info, err := os.Stat(castleHome)
|
||||||
|
if err != nil || !info.IsDir() {
|
||||||
|
return fmt.Errorf("could not symlink %s, expected %s to exist and contain dotfiles", castle, castleHome)
|
||||||
|
}
|
||||||
|
|
||||||
|
subdirs, err := readSubdirs(filepath.Join(a.ReposDir, castle, ".homesick_subdir"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.linkEach(castleHome, castleHome, subdirs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, subdir := range subdirs {
|
||||||
|
base := filepath.Join(castleHome, subdir)
|
||||||
|
if _, err := os.Stat(base); err != nil {
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.linkEach(castleHome, base, subdirs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) Unlink(castle string) error {
|
||||||
|
if strings.TrimSpace(castle) == "" {
|
||||||
|
castle = "dotfiles"
|
||||||
|
}
|
||||||
|
return a.UnlinkCastle(castle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) UnlinkCastle(castle string) error {
|
||||||
|
castleHome := filepath.Join(a.ReposDir, castle, "home")
|
||||||
|
info, err := os.Stat(castleHome)
|
||||||
|
if err != nil || !info.IsDir() {
|
||||||
|
return fmt.Errorf("could not symlink %s, expected %s to exist and contain dotfiles", castle, castleHome)
|
||||||
|
}
|
||||||
|
|
||||||
|
subdirs, err := readSubdirs(filepath.Join(a.ReposDir, castle, ".homesick_subdir"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.unlinkEach(castleHome, castleHome, subdirs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, subdir := range subdirs {
|
||||||
|
base := filepath.Join(castleHome, subdir)
|
||||||
|
if _, err := os.Stat(base); err != nil {
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.unlinkEach(castleHome, base, subdirs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) Track(string, string) error {
|
||||||
|
return errors.New("track is not implemented in Go yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) linkEach(castleHome string, baseDir string, subdirs []string) error {
|
||||||
|
entries, err := os.ReadDir(baseDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
name := entry.Name()
|
||||||
|
if name == "." || name == ".." {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
source := filepath.Join(baseDir, name)
|
||||||
|
ignore, err := matchesIgnoredDir(castleHome, source, subdirs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ignore {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
relDir, err := filepath.Rel(castleHome, baseDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
destination := filepath.Join(a.HomeDir, relDir, name)
|
||||||
|
if relDir == "." {
|
||||||
|
destination = filepath.Join(a.HomeDir, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.linkPath(source, destination); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) unlinkEach(castleHome string, baseDir string, subdirs []string) error {
|
||||||
|
entries, err := os.ReadDir(baseDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
name := entry.Name()
|
||||||
|
if name == "." || name == ".." {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
source := filepath.Join(baseDir, name)
|
||||||
|
ignore, err := matchesIgnoredDir(castleHome, source, subdirs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ignore {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
relDir, err := filepath.Rel(castleHome, baseDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
destination := filepath.Join(a.HomeDir, relDir, name)
|
||||||
|
if relDir == "." {
|
||||||
|
destination = filepath.Join(a.HomeDir, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := unlinkPath(destination); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func unlinkPath(destination string) error {
|
||||||
|
info, err := os.Lstat(destination)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.Mode()&os.ModeSymlink == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.Remove(destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) linkPath(source string, destination string) error {
|
||||||
|
absSource, err := filepath.Abs(source)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(destination), 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := os.Lstat(destination)
|
||||||
|
if err == nil {
|
||||||
|
if info.Mode()&os.ModeSymlink != 0 {
|
||||||
|
target, readErr := os.Readlink(destination)
|
||||||
|
if readErr == nil && target == absSource {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !a.Force {
|
||||||
|
return fmt.Errorf("%s exists", destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rmErr := os.RemoveAll(destination); rmErr != nil {
|
||||||
|
return rmErr
|
||||||
|
}
|
||||||
|
} else if !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Symlink(absSource, destination); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readSubdirs(path string) ([]string, error) {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(string(data), "\n")
|
||||||
|
result := make([]string, 0, len(lines))
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if trimmed == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result = append(result, filepath.Clean(trimmed))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchesIgnoredDir(castleHome string, candidate string, subdirs []string) (bool, error) {
|
||||||
|
absCandidate, err := filepath.Abs(candidate)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ignoreSet := map[string]struct{}{}
|
||||||
|
for _, subdir := range subdirs {
|
||||||
|
clean := filepath.Clean(subdir)
|
||||||
|
for clean != "." && clean != string(filepath.Separator) {
|
||||||
|
ignoreSet[filepath.Join(castleHome, clean)] = struct{}{}
|
||||||
|
next := filepath.Dir(clean)
|
||||||
|
if next == clean {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
clean = next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ok := ignoreSet[absCandidate]
|
||||||
|
return ok, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runGit(dir string, args ...string) error {
|
||||||
|
cmd := exec.Command("git", args...)
|
||||||
|
cmd.Dir = dir
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("git %s failed: %w", strings.Join(args, " "), err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func gitOutput(dir string, args ...string) (string, error) {
|
||||||
|
cmd := exec.Command("git", args...)
|
||||||
|
cmd.Dir = dir
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(out), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func deriveDestination(uri string) string {
|
||||||
|
candidate := strings.TrimSpace(uri)
|
||||||
|
candidate = strings.TrimPrefix(candidate, "https://github.com/")
|
||||||
|
candidate = strings.TrimPrefix(candidate, "http://github.com/")
|
||||||
|
candidate = strings.TrimPrefix(candidate, "git://github.com/")
|
||||||
|
|
||||||
|
if strings.HasPrefix(candidate, "file://") {
|
||||||
|
candidate = strings.TrimPrefix(candidate, "file://")
|
||||||
|
}
|
||||||
|
|
||||||
|
candidate = strings.TrimSuffix(candidate, ".git")
|
||||||
|
candidate = strings.TrimSuffix(candidate, "/")
|
||||||
|
if candidate == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(candidate, "/")
|
||||||
|
last := parts[len(parts)-1]
|
||||||
|
if strings.Contains(last, ":") {
|
||||||
|
a := strings.Split(last, ":")
|
||||||
|
last = a[len(a)-1]
|
||||||
|
}
|
||||||
|
return last
|
||||||
|
}
|
||||||
24
internal/homesick/core/core_test.go
Normal file
24
internal/homesick/core/core_test.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestDeriveDestination(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
uri string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{name: "github short", uri: "technicalpickles/pickled-vim", want: "pickled-vim"},
|
||||||
|
{name: "https", uri: "https://github.com/technicalpickles/pickled-vim.git", want: "pickled-vim"},
|
||||||
|
{name: "git ssh", uri: "git@github.com:technicalpickles/pickled-vim.git", want: "pickled-vim"},
|
||||||
|
{name: "file", uri: "file:///tmp/dotfiles.git", want: "dotfiles"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := deriveDestination(tt.uri); got != tt.want {
|
||||||
|
t.Fatalf("deriveDestination(%q) = %q, want %q", tt.uri, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
118
internal/homesick/core/link_test.go
Normal file
118
internal/homesick/core/link_test.go
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
package core_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.hrafn.xyz/aether/gosick/internal/homesick/core"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LinkSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
tmpDir string
|
||||||
|
homeDir string
|
||||||
|
reposDir string
|
||||||
|
app *core.App
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLinkSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(LinkSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LinkSuite) SetupTest() {
|
||||||
|
s.tmpDir = s.T().TempDir()
|
||||||
|
s.homeDir = filepath.Join(s.tmpDir, "home")
|
||||||
|
s.reposDir = filepath.Join(s.homeDir, ".homesick", "repos")
|
||||||
|
|
||||||
|
require.NoError(s.T(), os.MkdirAll(s.reposDir, 0o755))
|
||||||
|
|
||||||
|
s.app = &core.App{
|
||||||
|
HomeDir: s.homeDir,
|
||||||
|
ReposDir: s.reposDir,
|
||||||
|
Stdout: io.Discard,
|
||||||
|
Stderr: io.Discard,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LinkSuite) createCastle(castle string) string {
|
||||||
|
castleHome := filepath.Join(s.reposDir, castle, "home")
|
||||||
|
require.NoError(s.T(), os.MkdirAll(castleHome, 0o755))
|
||||||
|
return castleHome
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LinkSuite) writeFile(path string, content string) {
|
||||||
|
require.NoError(s.T(), os.MkdirAll(filepath.Dir(path), 0o755))
|
||||||
|
require.NoError(s.T(), os.WriteFile(path, []byte(content), 0o644))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LinkSuite) TestLink_SymlinksTopLevelFiles() {
|
||||||
|
castleHome := s.createCastle("glencairn")
|
||||||
|
dotfile := filepath.Join(castleHome, ".vimrc")
|
||||||
|
s.writeFile(dotfile, "set number\n")
|
||||||
|
|
||||||
|
err := s.app.Link("glencairn")
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
|
||||||
|
homePath := filepath.Join(s.homeDir, ".vimrc")
|
||||||
|
info, err := os.Lstat(homePath)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
require.True(s.T(), info.Mode()&os.ModeSymlink != 0)
|
||||||
|
|
||||||
|
target, err := os.Readlink(homePath)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
require.Equal(s.T(), dotfile, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LinkSuite) TestLink_RespectsHomesickSubdir() {
|
||||||
|
castleHome := s.createCastle("glencairn")
|
||||||
|
configDir := filepath.Join(castleHome, ".config")
|
||||||
|
appDir := filepath.Join(configDir, "myapp")
|
||||||
|
s.writeFile(filepath.Join(appDir, "config.toml"), "ok=true\n")
|
||||||
|
s.writeFile(filepath.Join(s.reposDir, "glencairn", ".homesick_subdir"), ".config\n")
|
||||||
|
|
||||||
|
err := s.app.Link("glencairn")
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
|
||||||
|
configInfo, err := os.Lstat(filepath.Join(s.homeDir, ".config"))
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
require.False(s.T(), configInfo.Mode()&os.ModeSymlink != 0)
|
||||||
|
|
||||||
|
homeApp := filepath.Join(s.homeDir, ".config", "myapp")
|
||||||
|
appInfo, err := os.Lstat(homeApp)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
require.True(s.T(), appInfo.Mode()&os.ModeSymlink != 0)
|
||||||
|
|
||||||
|
target, err := os.Readlink(homeApp)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
require.Equal(s.T(), appDir, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LinkSuite) TestLink_ForceReplacesExistingFile() {
|
||||||
|
castleHome := s.createCastle("glencairn")
|
||||||
|
dotfile := filepath.Join(castleHome, ".zshrc")
|
||||||
|
s.writeFile(dotfile, "export EDITOR=vim\n")
|
||||||
|
s.writeFile(filepath.Join(s.homeDir, ".zshrc"), "existing\n")
|
||||||
|
|
||||||
|
s.app.Force = true
|
||||||
|
err := s.app.Link("glencairn")
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
|
||||||
|
homePath := filepath.Join(s.homeDir, ".zshrc")
|
||||||
|
info, err := os.Lstat(homePath)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
require.True(s.T(), info.Mode()&os.ModeSymlink != 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LinkSuite) TestLink_NoForceErrorsOnConflict() {
|
||||||
|
castleHome := s.createCastle("glencairn")
|
||||||
|
dotfile := filepath.Join(castleHome, ".gitconfig")
|
||||||
|
s.writeFile(dotfile, "[user]\n")
|
||||||
|
s.writeFile(filepath.Join(s.homeDir, ".gitconfig"), "local\n")
|
||||||
|
|
||||||
|
err := s.app.Link("glencairn")
|
||||||
|
require.Error(s.T(), err)
|
||||||
|
}
|
||||||
101
internal/homesick/core/track_test.go
Normal file
101
internal/homesick/core/track_test.go
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
package core_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.hrafn.xyz/aether/gosick/internal/homesick/core"
|
||||||
|
git "github.com/go-git/go-git/v5"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TrackSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
tmpDir string
|
||||||
|
homeDir string
|
||||||
|
reposDir string
|
||||||
|
app *core.App
|
||||||
|
}
|
||||||
|
|
||||||
|
//NB: this has nothing to do with jogging
|
||||||
|
func TestTrackSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(TrackSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TrackSuite) SetupTest() {
|
||||||
|
s.tmpDir = s.T().TempDir()
|
||||||
|
s.homeDir = filepath.Join(s.tmpDir, "home")
|
||||||
|
s.reposDir = filepath.Join(s.homeDir, ".homesick", "repos")
|
||||||
|
require.NoError(s.T(), os.MkdirAll(s.reposDir, 0o755))
|
||||||
|
|
||||||
|
s.app = &core.App{
|
||||||
|
HomeDir: s.homeDir,
|
||||||
|
ReposDir: s.reposDir,
|
||||||
|
Stdout: io.Discard,
|
||||||
|
Stderr: io.Discard,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TrackSuite) createCastleRepo(castle string) string {
|
||||||
|
castleRoot := filepath.Join(s.reposDir, castle)
|
||||||
|
require.NoError(s.T(), os.MkdirAll(filepath.Join(castleRoot, "home"), 0o755))
|
||||||
|
_, err := git.PlainInit(castleRoot, false)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
return castleRoot
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TrackSuite) writeFile(path string, content string) {
|
||||||
|
require.NoError(s.T(), os.MkdirAll(filepath.Dir(path), 0o755))
|
||||||
|
require.NoError(s.T(), os.WriteFile(path, []byte(content), 0o644))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TrackSuite) TestTrack_AfterRelinkTracksFileAndUpdatesSubdir() {
|
||||||
|
castleRoot := s.createCastleRepo("parity-castle")
|
||||||
|
castleHome := filepath.Join(castleRoot, "home")
|
||||||
|
|
||||||
|
s.writeFile(filepath.Join(castleHome, ".vimrc"), "set number\n")
|
||||||
|
s.writeFile(filepath.Join(castleRoot, ".homesick_subdir"), ".config\n")
|
||||||
|
s.writeFile(filepath.Join(castleHome, ".config", "myapp", "config.toml"), "ok=true\n")
|
||||||
|
|
||||||
|
require.NoError(s.T(), s.app.Link("parity-castle"))
|
||||||
|
require.NoError(s.T(), s.app.Unlink("parity-castle"))
|
||||||
|
require.NoError(s.T(), s.app.Link("parity-castle"))
|
||||||
|
|
||||||
|
toolPath := filepath.Join(s.homeDir, ".local", "bin", "tool")
|
||||||
|
s.writeFile(toolPath, "#!/usr/bin/env bash\n")
|
||||||
|
|
||||||
|
require.NoError(s.T(), s.app.Track(toolPath, "parity-castle"))
|
||||||
|
|
||||||
|
expectedTarget := filepath.Join(castleHome, ".local", "bin", "tool")
|
||||||
|
info, err := os.Lstat(toolPath)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
require.True(s.T(), info.Mode()&os.ModeSymlink != 0)
|
||||||
|
|
||||||
|
target, err := os.Readlink(toolPath)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
require.Equal(s.T(), expectedTarget, target)
|
||||||
|
|
||||||
|
subdirData, err := os.ReadFile(filepath.Join(castleRoot, ".homesick_subdir"))
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
require.Contains(s.T(), string(subdirData), ".local/bin\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TrackSuite) TestTrack_DefaultCastleName() {
|
||||||
|
castleRoot := s.createCastleRepo("dotfiles")
|
||||||
|
castleHome := filepath.Join(castleRoot, "home")
|
||||||
|
|
||||||
|
filePath := filepath.Join(s.homeDir, ".tmux.conf")
|
||||||
|
s.writeFile(filePath, "set -g mouse on\n")
|
||||||
|
|
||||||
|
require.NoError(s.T(), s.app.Track(filePath, ""))
|
||||||
|
|
||||||
|
expectedTarget := filepath.Join(castleHome, ".tmux.conf")
|
||||||
|
require.FileExists(s.T(), expectedTarget)
|
||||||
|
|
||||||
|
linkTarget, err := os.Readlink(filePath)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
require.Equal(s.T(), expectedTarget, linkTarget)
|
||||||
|
}
|
||||||
106
internal/homesick/core/unlink_test.go
Normal file
106
internal/homesick/core/unlink_test.go
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
package core_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.hrafn.xyz/aether/gosick/internal/homesick/core"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UnlinkSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
tmpDir string
|
||||||
|
homeDir string
|
||||||
|
reposDir string
|
||||||
|
app *core.App
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnlinkSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(UnlinkSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UnlinkSuite) SetupTest() {
|
||||||
|
s.tmpDir = s.T().TempDir()
|
||||||
|
s.homeDir = filepath.Join(s.tmpDir, "home")
|
||||||
|
s.reposDir = filepath.Join(s.homeDir, ".homesick", "repos")
|
||||||
|
|
||||||
|
require.NoError(s.T(), os.MkdirAll(s.reposDir, 0o755))
|
||||||
|
|
||||||
|
s.app = &core.App{
|
||||||
|
HomeDir: s.homeDir,
|
||||||
|
ReposDir: s.reposDir,
|
||||||
|
Stdout: io.Discard,
|
||||||
|
Stderr: io.Discard,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UnlinkSuite) createCastle(castle string) string {
|
||||||
|
castleHome := filepath.Join(s.reposDir, castle, "home")
|
||||||
|
require.NoError(s.T(), os.MkdirAll(castleHome, 0o755))
|
||||||
|
return castleHome
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UnlinkSuite) writeFile(path string, content string) {
|
||||||
|
require.NoError(s.T(), os.MkdirAll(filepath.Dir(path), 0o755))
|
||||||
|
require.NoError(s.T(), os.WriteFile(path, []byte(content), 0o644))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UnlinkSuite) TestUnlink_RemovesTopLevelSymlinks() {
|
||||||
|
castleHome := s.createCastle("glencairn")
|
||||||
|
dotfile := filepath.Join(castleHome, ".vimrc")
|
||||||
|
s.writeFile(dotfile, "set number\n")
|
||||||
|
|
||||||
|
require.NoError(s.T(), s.app.Link("glencairn"))
|
||||||
|
require.NoError(s.T(), s.app.Unlink("glencairn"))
|
||||||
|
|
||||||
|
require.NoFileExists(s.T(), filepath.Join(s.homeDir, ".vimrc"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UnlinkSuite) TestUnlink_RemovesNonDotfileSymlinks() {
|
||||||
|
castleHome := s.createCastle("glencairn")
|
||||||
|
binFile := filepath.Join(castleHome, "bin")
|
||||||
|
s.writeFile(binFile, "#!/usr/bin/env bash\n")
|
||||||
|
|
||||||
|
require.NoError(s.T(), s.app.Link("glencairn"))
|
||||||
|
require.NoError(s.T(), s.app.Unlink("glencairn"))
|
||||||
|
|
||||||
|
require.NoFileExists(s.T(), filepath.Join(s.homeDir, "bin"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UnlinkSuite) TestUnlink_RespectsHomesickSubdir() {
|
||||||
|
castleHome := s.createCastle("glencairn")
|
||||||
|
appDir := filepath.Join(castleHome, ".config", "myapp")
|
||||||
|
s.writeFile(filepath.Join(appDir, "config.toml"), "ok=true\n")
|
||||||
|
s.writeFile(filepath.Join(s.reposDir, "glencairn", ".homesick_subdir"), ".config\n")
|
||||||
|
|
||||||
|
require.NoError(s.T(), s.app.Link("glencairn"))
|
||||||
|
require.NoError(s.T(), s.app.Unlink("glencairn"))
|
||||||
|
|
||||||
|
require.DirExists(s.T(), filepath.Join(s.homeDir, ".config"))
|
||||||
|
require.NoFileExists(s.T(), filepath.Join(s.homeDir, ".config", "myapp"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UnlinkSuite) TestUnlink_DefaultCastleName() {
|
||||||
|
castleHome := s.createCastle("dotfiles")
|
||||||
|
dotfile := filepath.Join(castleHome, ".zshrc")
|
||||||
|
s.writeFile(dotfile, "export EDITOR=vim\n")
|
||||||
|
|
||||||
|
require.NoError(s.T(), s.app.Link("dotfiles"))
|
||||||
|
require.NoError(s.T(), s.app.Unlink(""))
|
||||||
|
|
||||||
|
require.NoFileExists(s.T(), filepath.Join(s.homeDir, ".zshrc"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UnlinkSuite) TestUnlink_IgnoresNonSymlinkDestination() {
|
||||||
|
castleHome := s.createCastle("glencairn")
|
||||||
|
dotfile := filepath.Join(castleHome, ".gitconfig")
|
||||||
|
s.writeFile(dotfile, "[user]\n")
|
||||||
|
s.writeFile(filepath.Join(s.homeDir, ".gitconfig"), "local\n")
|
||||||
|
|
||||||
|
require.NoError(s.T(), s.app.Unlink("glencairn"))
|
||||||
|
require.FileExists(s.T(), filepath.Join(s.homeDir, ".gitconfig"))
|
||||||
|
}
|
||||||
3
internal/homesick/version/version.go
Normal file
3
internal/homesick/version/version.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package version
|
||||||
|
|
||||||
|
const String = "1.1.6"
|
||||||
27
justfile
Normal file
27
justfile
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
set shell := ["bash", "-eu", "-o", "pipefail", "-c"]
|
||||||
|
|
||||||
|
default:
|
||||||
|
@just --list
|
||||||
|
|
||||||
|
go-build:
|
||||||
|
@mkdir -p dist
|
||||||
|
go build -o dist/homesick-go ./cmd/homesick
|
||||||
|
|
||||||
|
go-build-linux:
|
||||||
|
@mkdir -p dist
|
||||||
|
GOOS=linux GOARCH="$(go env GOARCH)" go build -o dist/homesick-go ./cmd/homesick
|
||||||
|
|
||||||
|
go-test:
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
behavior-ruby:
|
||||||
|
./script/run-behavior-suite-docker.sh
|
||||||
|
|
||||||
|
behavior-go: go-build-linux
|
||||||
|
HOMESICK_CMD="/workspace/dist/homesick-go" ./script/run-behavior-suite-docker.sh
|
||||||
|
|
||||||
|
behavior-go-verbose: go-build-linux
|
||||||
|
HOMESICK_CMD="/workspace/dist/homesick-go" ./script/run-behavior-suite-docker.sh --verbose
|
||||||
|
|
||||||
|
behavior-ruby-verbose:
|
||||||
|
./script/run-behavior-suite-docker.sh --verbose
|
||||||
@@ -1,15 +1,29 @@
|
|||||||
# -*- encoding : utf-8 -*-
|
|
||||||
require 'homesick/shell'
|
|
||||||
require 'homesick/actions/file_actions'
|
require 'homesick/actions/file_actions'
|
||||||
require 'homesick/actions/git_actions'
|
require 'homesick/actions/git_actions'
|
||||||
require 'homesick/version'
|
require 'homesick/version'
|
||||||
require 'homesick/utils'
|
require 'homesick/utils'
|
||||||
require 'homesick/cli'
|
require 'homesick/cli'
|
||||||
|
require 'fileutils'
|
||||||
|
|
||||||
# Homesick's top-level module
|
# Homesick's top-level module
|
||||||
module Homesick
|
module Homesick
|
||||||
GITHUB_NAME_REPO_PATTERN = /\A([A-Za-z0-9_-]+\/[A-Za-z0-9_-]+)\Z/
|
GITHUB_NAME_REPO_PATTERN = %r{\A([A-Za-z0-9_-]+/[A-Za-z0-9_-]+)\Z}.freeze
|
||||||
SUBDIR_FILENAME = '.homesick_subdir'
|
SUBDIR_FILENAME = '.homesick_subdir'.freeze
|
||||||
|
|
||||||
DEFAULT_CASTLE_NAME = 'dotfiles'
|
DEFAULT_CASTLE_NAME = 'dotfiles'.freeze
|
||||||
|
QUIETABLE = [:say_status].freeze
|
||||||
|
|
||||||
|
PRETENDABLE = [:system].freeze
|
||||||
|
|
||||||
|
QUIETABLE.each do |method_name|
|
||||||
|
define_method(method_name) do |*args|
|
||||||
|
super(*args) unless options[:quiet]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
PRETENDABLE.each do |method_name|
|
||||||
|
define_method(method_name) do |*args|
|
||||||
|
super(*args) unless options[:pretend]
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,20 +1,14 @@
|
|||||||
# -*- encoding : utf-8 -*-
|
|
||||||
module Homesick
|
module Homesick
|
||||||
module Actions
|
module Actions
|
||||||
# File-related helper methods for Homesick
|
# File-related helper methods for Homesick
|
||||||
module FileActions
|
module FileActions
|
||||||
def mv(source, destination, config = {})
|
protected
|
||||||
|
|
||||||
|
def mv(source, destination)
|
||||||
source = Pathname.new(source)
|
source = Pathname.new(source)
|
||||||
destination = Pathname.new(destination + source.basename)
|
destination = Pathname.new(destination + source.basename)
|
||||||
|
say_status :conflict, "#{destination} exists", :red if destination.exist? && (options[:force] || shell.file_collision(destination) { source })
|
||||||
if destination.exist?
|
FileUtils.mv source, destination unless options[:pretend]
|
||||||
say_status :conflict, "#{destination} exists", :red
|
|
||||||
|
|
||||||
FileUtils.mv source, destination if (options[:force] || shell.file_collision(destination) { source }) && !options[:pretend]
|
|
||||||
else
|
|
||||||
# this needs some sort of message here.
|
|
||||||
FileUtils.mv source, destination unless options[:pretend]
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def rm_rf(dir)
|
def rm_rf(dir)
|
||||||
@@ -26,7 +20,7 @@ module Homesick
|
|||||||
target = Pathname.new(target)
|
target = Pathname.new(target)
|
||||||
|
|
||||||
if target.symlink?
|
if target.symlink?
|
||||||
say_status :unlink, "#{target.expand_path}", :green
|
say_status :unlink, target.expand_path.to_s, :green
|
||||||
FileUtils.rm_rf target
|
FileUtils.rm_rf target
|
||||||
else
|
else
|
||||||
say_status :conflict, "#{target} is not a symlink", :red
|
say_status :conflict, "#{target} is not a symlink", :red
|
||||||
@@ -43,48 +37,41 @@ module Homesick
|
|||||||
FileUtils.rm_r dir
|
FileUtils.rm_r dir
|
||||||
end
|
end
|
||||||
|
|
||||||
def ln_s(source, destination, config = {})
|
def ln_s(source, destination)
|
||||||
source = Pathname.new(source)
|
source = Pathname.new(source).realpath
|
||||||
destination = Pathname.new(destination)
|
destination = Pathname.new(destination)
|
||||||
FileUtils.mkdir_p destination.dirname
|
FileUtils.mkdir_p destination.dirname
|
||||||
|
|
||||||
action = if destination.symlink? && destination.readlink == source
|
action = :success
|
||||||
:identical
|
action = :identical if destination.symlink? && destination.readlink == source
|
||||||
elsif destination.symlink?
|
action = :symlink_conflict if destination.symlink?
|
||||||
:symlink_conflict
|
action = :conflict if destination.exist?
|
||||||
elsif destination.exist?
|
|
||||||
:conflict
|
|
||||||
else
|
|
||||||
:success
|
|
||||||
end
|
|
||||||
|
|
||||||
handle_symlink_action action, source, destination
|
handle_symlink_action action, source, destination
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_symlink_action(action, source, destination)
|
def handle_symlink_action(action, source, destination)
|
||||||
case action
|
if action == :identical
|
||||||
when :identical
|
|
||||||
say_status :identical, destination.expand_path, :blue
|
say_status :identical, destination.expand_path, :blue
|
||||||
when :symlink_conflict
|
return
|
||||||
say_status :conflict,
|
end
|
||||||
"#{destination} exists and points to #{destination.readlink}",
|
message = generate_symlink_message action, source, destination
|
||||||
:red
|
if %i[symlink_conflict conflict].include?(action)
|
||||||
|
say_status :conflict, message, :red
|
||||||
FileUtils.rm destination
|
if collision_accepted?(destination, source)
|
||||||
FileUtils.ln_s source, destination, force: true unless options[:pretend]
|
|
||||||
when :conflict
|
|
||||||
say_status :conflict, "#{destination} exists", :red
|
|
||||||
|
|
||||||
if collision_accepted?(destination)
|
|
||||||
FileUtils.rm_r destination, force: true unless options[:pretend]
|
FileUtils.rm_r destination, force: true unless options[:pretend]
|
||||||
FileUtils.ln_s source, destination, force: true unless options[:pretend]
|
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
say_status :symlink,
|
say_status :symlink, message, :green
|
||||||
"#{source.expand_path} to #{destination.expand_path}",
|
|
||||||
:green
|
|
||||||
FileUtils.ln_s source, destination unless options[:pretend]
|
|
||||||
end
|
end
|
||||||
|
FileUtils.ln_s source, destination, force: true unless options[:pretend]
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_symlink_message(action, source, destination)
|
||||||
|
message = "#{source.expand_path} to #{destination.expand_path}"
|
||||||
|
message = "#{destination} exists and points to #{destination.readlink}" if action == :symlink_conflict
|
||||||
|
message = "#{destination} exists" if action == :conflict
|
||||||
|
message
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,14 +1,34 @@
|
|||||||
# -*- encoding : utf-8 -*-
|
|
||||||
module Homesick
|
module Homesick
|
||||||
module Actions
|
module Actions
|
||||||
# Git-related helper methods for Homesick
|
# Git-related helper methods for Homesick
|
||||||
module GitActions
|
module GitActions
|
||||||
|
# Information on the minimum git version required for Homesick
|
||||||
|
MIN_VERSION = {
|
||||||
|
major: 1,
|
||||||
|
minor: 8,
|
||||||
|
patch: 0
|
||||||
|
}.freeze
|
||||||
|
STRING = MIN_VERSION.values.join('.')
|
||||||
|
|
||||||
|
def git_version_correct?
|
||||||
|
info = `git --version`.scan(/(\d+)\.(\d+)\.(\d+)/).flatten.map(&:to_i)
|
||||||
|
return false unless info.count == 3
|
||||||
|
|
||||||
|
current_version = Hash[%i[major minor patch].zip(info)]
|
||||||
|
major_equals = current_version.eql?(MIN_VERSION)
|
||||||
|
major_greater = current_version[:major] > MIN_VERSION[:major]
|
||||||
|
minor_greater = current_version[:major] == MIN_VERSION[:major] && current_version[:minor] > MIN_VERSION[:minor]
|
||||||
|
patch_greater = current_version[:major] == MIN_VERSION[:major] && current_version[:minor] == MIN_VERSION[:minor] && current_version[:patch] >= MIN_VERSION[:patch]
|
||||||
|
|
||||||
|
major_equals || major_greater || minor_greater || patch_greater
|
||||||
|
end
|
||||||
|
|
||||||
# TODO: move this to be more like thor's template, empty_directory, etc
|
# TODO: move this to be more like thor's template, empty_directory, etc
|
||||||
def git_clone(repo, config = {})
|
def git_clone(repo, config = {})
|
||||||
config ||= {}
|
config ||= {}
|
||||||
destination = config[:destination] || File.basename(repo, '.git')
|
destination = config[:destination] || File.basename(repo, '.git')
|
||||||
|
|
||||||
destination = Pathname.new(destination) unless destination.kind_of?(Pathname)
|
destination = Pathname.new(destination) unless destination.is_a?(Pathname)
|
||||||
FileUtils.mkdir_p destination.dirname
|
FileUtils.mkdir_p destination.dirname
|
||||||
|
|
||||||
if destination.directory?
|
if destination.directory?
|
||||||
@@ -46,22 +66,22 @@ module Homesick
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def git_submodule_init(config = {})
|
def git_submodule_init
|
||||||
say_status 'git submodule', 'init', :green
|
say_status 'git submodule', 'init', :green
|
||||||
system 'git submodule --quiet init'
|
system 'git submodule --quiet init'
|
||||||
end
|
end
|
||||||
|
|
||||||
def git_submodule_update(config = {})
|
def git_submodule_update
|
||||||
say_status 'git submodule', 'update', :green
|
say_status 'git submodule', 'update', :green
|
||||||
system 'git submodule --quiet update --init --recursive >/dev/null 2>&1'
|
system 'git submodule --quiet update --init --recursive >/dev/null 2>&1'
|
||||||
end
|
end
|
||||||
|
|
||||||
def git_pull(config = {})
|
def git_pull
|
||||||
say_status 'git pull', '', :green
|
say_status 'git pull', '', :green
|
||||||
system 'git pull --quiet'
|
system 'git pull --quiet'
|
||||||
end
|
end
|
||||||
|
|
||||||
def git_push(config = {})
|
def git_push
|
||||||
say_status 'git push', '', :green
|
say_status 'git push', '', :green
|
||||||
system 'git push'
|
system 'git push'
|
||||||
end
|
end
|
||||||
@@ -69,23 +89,23 @@ module Homesick
|
|||||||
def git_commit_all(config = {})
|
def git_commit_all(config = {})
|
||||||
say_status 'git commit all', '', :green
|
say_status 'git commit all', '', :green
|
||||||
if config[:message]
|
if config[:message]
|
||||||
system "git commit -a -m '#{config[:message]}'"
|
system %(git commit -a -m "#{config[:message]}")
|
||||||
else
|
else
|
||||||
system 'git commit -v -a'
|
system 'git commit -v -a'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def git_add(file, config = {})
|
def git_add(file)
|
||||||
say_status 'git add file', '', :green
|
say_status 'git add file', '', :green
|
||||||
system "git add '#{file}'"
|
system "git add '#{file}'"
|
||||||
end
|
end
|
||||||
|
|
||||||
def git_status(config = {})
|
def git_status
|
||||||
say_status 'git status', '', :green
|
say_status 'git status', '', :green
|
||||||
system 'git status'
|
system 'git status'
|
||||||
end
|
end
|
||||||
|
|
||||||
def git_diff(config = {})
|
def git_diff
|
||||||
say_status 'git diff', '', :green
|
say_status 'git diff', '', :green
|
||||||
system 'git diff'
|
system 'git diff'
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# -*- encoding : utf-8 -*-
|
require 'fileutils'
|
||||||
require 'thor'
|
require 'thor'
|
||||||
|
|
||||||
module Homesick
|
module Homesick
|
||||||
@@ -19,29 +19,35 @@ module Homesick
|
|||||||
|
|
||||||
def initialize(args = [], options = {}, config = {})
|
def initialize(args = [], options = {}, config = {})
|
||||||
super
|
super
|
||||||
self.shell = Homesick::Shell.new
|
# Check if git is installed
|
||||||
|
unless git_version_correct?
|
||||||
|
say_status :error, "Git version >= #{Homesick::Actions::GitActions::STRING} must be installed to use Homesick", :red
|
||||||
|
exit(1)
|
||||||
|
end
|
||||||
|
configure_symlinks_diff
|
||||||
end
|
end
|
||||||
|
|
||||||
desc 'clone URI', 'Clone +uri+ as a castle for homesick'
|
desc 'clone URI CASTLE_NAME', 'Clone +uri+ as a castle with name CASTLE_NAME for homesick'
|
||||||
def clone(uri)
|
def clone(uri, destination = nil)
|
||||||
|
destination = Pathname.new(destination) unless destination.nil?
|
||||||
|
|
||||||
inside repos_dir do
|
inside repos_dir do
|
||||||
destination = nil
|
|
||||||
if File.exist?(uri)
|
if File.exist?(uri)
|
||||||
uri = Pathname.new(uri).expand_path
|
uri = Pathname.new(uri).expand_path
|
||||||
fail "Castle already cloned to #{uri}" if uri.to_s.start_with?(repos_dir.to_s)
|
raise "Castle already cloned to #{uri}" if uri.to_s.start_with?(repos_dir.to_s)
|
||||||
|
|
||||||
destination = uri.basename
|
destination = uri.basename if destination.nil?
|
||||||
|
|
||||||
ln_s uri, destination
|
ln_s uri, destination
|
||||||
elsif uri =~ GITHUB_NAME_REPO_PATTERN
|
elsif uri =~ GITHUB_NAME_REPO_PATTERN
|
||||||
destination = Pathname.new(uri).basename
|
destination = Pathname.new(uri).basename if destination.nil?
|
||||||
git_clone "https://github.com/#{Regexp.last_match[1]}.git",
|
git_clone "https://github.com/#{Regexp.last_match[1]}.git",
|
||||||
destination: destination
|
destination: destination
|
||||||
elsif uri =~ /%r([^%r]*?)(\.git)?\Z/ || uri =~ /[^:]+:([^:]+)(\.git)?\Z/
|
elsif uri =~ /%r([^%r]*?)(\.git)?\Z/ || uri =~ /[^:]+:([^:]+)(\.git)?\Z/
|
||||||
destination = Pathname.new(Regexp.last_match[1])
|
destination = Pathname.new(Regexp.last_match[1].gsub(/\.git$/, '')).basename if destination.nil?
|
||||||
git_clone uri
|
git_clone uri, destination: destination
|
||||||
else
|
else
|
||||||
fail "Unknown URI format: #{uri}"
|
raise "Unknown URI format: #{uri}"
|
||||||
end
|
end
|
||||||
|
|
||||||
setup_castle(destination)
|
setup_castle(destination)
|
||||||
@@ -49,22 +55,22 @@ module Homesick
|
|||||||
end
|
end
|
||||||
|
|
||||||
desc 'rc CASTLE', 'Run the .homesickrc for the specified castle'
|
desc 'rc CASTLE', 'Run the .homesickrc for the specified castle'
|
||||||
|
method_option :force,
|
||||||
|
type: :boolean,
|
||||||
|
default: false,
|
||||||
|
desc: 'Evaluate .homesickrc without prompting.'
|
||||||
def rc(name = DEFAULT_CASTLE_NAME)
|
def rc(name = DEFAULT_CASTLE_NAME)
|
||||||
inside repos_dir do
|
inside repos_dir do
|
||||||
destination = Pathname.new(name)
|
destination = Pathname.new(name)
|
||||||
homesickrc = destination.join('.homesickrc').expand_path
|
homesickrc = destination.join('.homesickrc').expand_path
|
||||||
if homesickrc.exist?
|
return unless homesickrc.exist?
|
||||||
proceed = shell.yes?("#{name} has a .homesickrc. Proceed with evaling it? (This could be destructive)")
|
|
||||||
if proceed
|
proceed = options[:force] || shell.yes?("#{name} has a .homesickrc. Proceed with evaling it? (This could be destructive)")
|
||||||
say_status 'eval', homesickrc
|
return say_status 'eval skip', "not evaling #{homesickrc}, #{destination} may need manual configuration", :blue unless proceed
|
||||||
inside destination do
|
|
||||||
eval homesickrc.read, binding, homesickrc.expand_path.to_s
|
say_status 'eval', homesickrc
|
||||||
end
|
inside destination do
|
||||||
else
|
eval homesickrc.read, binding, homesickrc.expand_path.to_s
|
||||||
say_status 'eval skip',
|
|
||||||
"not evaling #{homesickrc}, #{destination} may need manual configuration",
|
|
||||||
:blue
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -115,16 +121,18 @@ module Homesick
|
|||||||
|
|
||||||
desc 'link CASTLE', 'Symlinks all dotfiles from the specified castle'
|
desc 'link CASTLE', 'Symlinks all dotfiles from the specified castle'
|
||||||
method_option :force,
|
method_option :force,
|
||||||
|
type: :boolean,
|
||||||
default: false,
|
default: false,
|
||||||
desc: 'Overwrite existing conflicting symlinks without prompting.'
|
desc: 'Overwrite existing conflicting symlinks without prompting.'
|
||||||
def link(name = DEFAULT_CASTLE_NAME)
|
def link(name = DEFAULT_CASTLE_NAME)
|
||||||
check_castle_existance(name, 'symlink')
|
check_castle_existance(name, 'symlink')
|
||||||
|
|
||||||
inside castle_dir(name) do
|
castle_path = castle_dir(name)
|
||||||
|
inside castle_path do
|
||||||
subdirs = subdirs(name)
|
subdirs = subdirs(name)
|
||||||
|
|
||||||
# link files
|
# link files
|
||||||
symlink_each(name, castle_dir(name), subdirs)
|
symlink_each(name, castle_path, subdirs)
|
||||||
|
|
||||||
# link files in subdirs
|
# link files in subdirs
|
||||||
subdirs.each do |subdir|
|
subdirs.each do |subdir|
|
||||||
@@ -232,11 +240,10 @@ module Homesick
|
|||||||
desc 'destroy CASTLE', 'Delete all symlinks and remove the cloned repository'
|
desc 'destroy CASTLE', 'Delete all symlinks and remove the cloned repository'
|
||||||
def destroy(name)
|
def destroy(name)
|
||||||
check_castle_existance name, 'destroy'
|
check_castle_existance name, 'destroy'
|
||||||
|
return unless shell.yes?('This will destroy your castle irreversible! Are you sure?')
|
||||||
|
|
||||||
if shell.yes?('This will destroy your castle irreversible! Are you sure?')
|
unlink(name)
|
||||||
unlink(name)
|
rm_rf repos_dir.join(name)
|
||||||
rm_rf repos_dir.join(name)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
desc 'cd CASTLE', 'Open a new shell in the root of the given castle'
|
desc 'cd CASTLE', 'Open a new shell in the root of the given castle'
|
||||||
@@ -263,11 +270,11 @@ module Homesick
|
|||||||
end
|
end
|
||||||
check_castle_existance castle, 'open'
|
check_castle_existance castle, 'open'
|
||||||
castle_dir = repos_dir.join(castle)
|
castle_dir = repos_dir.join(castle)
|
||||||
say_status "#{ENV['EDITOR']} #{castle_dir.realpath}",
|
say_status "#{castle_dir.realpath}: #{ENV['EDITOR']} .",
|
||||||
"Opening the root directory of castle '#{castle}' in editor '#{ENV['EDITOR']}'.",
|
"Opening the root directory of castle '#{castle}' in editor '#{ENV['EDITOR']}'.",
|
||||||
:green
|
:green
|
||||||
inside castle_dir do
|
inside castle_dir do
|
||||||
system(ENV['EDITOR'])
|
system("#{ENV['EDITOR']} .")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
require 'thor'
|
|
||||||
|
|
||||||
module Homesick
|
|
||||||
# Hack in support for diffing symlinks
|
|
||||||
class Shell < Thor::Shell::Color
|
|
||||||
def show_diff(destination, content)
|
|
||||||
destination = Pathname.new(destination)
|
|
||||||
|
|
||||||
if destination.symlink?
|
|
||||||
say "- #{destination.readlink}", :red, true
|
|
||||||
say "+ #{content.expand_path}", :green, true
|
|
||||||
else
|
|
||||||
super
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,27 +1,12 @@
|
|||||||
# -*- encoding : utf-8 -*-
|
require 'pathname'
|
||||||
|
|
||||||
module Homesick
|
module Homesick
|
||||||
# Various utility methods that are used by Homesick
|
# Various utility methods that are used by Homesick
|
||||||
module Utils
|
module Utils
|
||||||
QUIETABLE = ['say_status']
|
|
||||||
|
|
||||||
PRETENDABLE = ['system']
|
|
||||||
|
|
||||||
QUIETABLE.each do |method_name|
|
|
||||||
define_method(method_name) do |*args|
|
|
||||||
super(*args) unless options[:quiet]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
PRETENDABLE.each do |method_name|
|
|
||||||
define_method(method_name) do |*args|
|
|
||||||
super(*args) unless options[:pretend]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def home_dir
|
def home_dir
|
||||||
@home_dir ||= Pathname.new(ENV['HOME'] || '~').expand_path
|
@home_dir ||= Pathname.new(ENV['HOME'] || '~').realpath
|
||||||
end
|
end
|
||||||
|
|
||||||
def repos_dir
|
def repos_dir
|
||||||
@@ -33,12 +18,12 @@ module Homesick
|
|||||||
end
|
end
|
||||||
|
|
||||||
def check_castle_existance(name, action)
|
def check_castle_existance(name, action)
|
||||||
unless castle_dir(name).exist?
|
return if castle_dir(name).exist?
|
||||||
say_status :error,
|
|
||||||
"Could not #{action} #{name}, expected #{castle_dir(name)} exist and contain dotfiles",
|
say_status :error,
|
||||||
:red
|
"Could not #{action} #{name}, expected #{castle_dir(name)} to exist and contain dotfiles",
|
||||||
exit(1)
|
:red
|
||||||
end
|
exit(1)
|
||||||
end
|
end
|
||||||
|
|
||||||
def all_castles
|
def all_castles
|
||||||
@@ -51,7 +36,7 @@ module Homesick
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def inside_each_castle(&block)
|
def inside_each_castle
|
||||||
all_castles.each do |git_dir|
|
all_castles.each do |git_dir|
|
||||||
castle = git_dir.dirname
|
castle = git_dir.dirname
|
||||||
Dir.chdir castle do # so we can call git config from the right contxt
|
Dir.chdir castle do # so we can call git config from the right contxt
|
||||||
@@ -128,7 +113,6 @@ module Homesick
|
|||||||
def move_dir_contents(target, dir_path)
|
def move_dir_contents(target, dir_path)
|
||||||
child_files = dir_path.children
|
child_files = dir_path.children
|
||||||
child_files.each do |child|
|
child_files.each do |child|
|
||||||
|
|
||||||
target_path = target.join(child.basename)
|
target_path = target.join(child.basename)
|
||||||
if target_path.exist?
|
if target_path.exist?
|
||||||
if more_recent?(child, target_path) && target.file?
|
if more_recent?(child, target_path) && target.file?
|
||||||
@@ -148,50 +132,14 @@ module Homesick
|
|||||||
first_p.mtime > second_p.mtime && !first_p.symlink?
|
first_p.mtime > second_p.mtime && !first_p.symlink?
|
||||||
end
|
end
|
||||||
|
|
||||||
def collision_accepted?(destination)
|
def collision_accepted?(destination, source)
|
||||||
fail "Argument must be an instance of Pathname, #{destination.class.name} given" unless destination.instance_of?(Pathname)
|
raise "Arguments must be instances of Pathname, #{destination.class.name} and #{source.class.name} given" unless destination.instance_of?(Pathname) && source.instance_of?(Pathname)
|
||||||
|
|
||||||
options[:force] || shell.file_collision(destination) { source }
|
options[:force] || shell.file_collision(destination) { source }
|
||||||
end
|
end
|
||||||
|
|
||||||
def each_file(castle, basedir, subdirs)
|
|
||||||
absolute_basedir = Pathname.new(basedir).expand_path
|
|
||||||
inside basedir do
|
|
||||||
files = Pathname.glob('{.*,*}').reject do |a|
|
|
||||||
['.', '..'].include?(a.to_s)
|
|
||||||
end
|
|
||||||
files.each do |path|
|
|
||||||
absolute_path = path.expand_path
|
|
||||||
castle_home = castle_dir(castle)
|
|
||||||
|
|
||||||
# make ignore dirs
|
|
||||||
ignore_dirs = []
|
|
||||||
subdirs.each do |subdir|
|
|
||||||
# ignore all parent of each line in subdir file
|
|
||||||
Pathname.new(subdir).ascend do |p|
|
|
||||||
ignore_dirs.push(p)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# ignore dirs written in subdir file
|
|
||||||
matched = false
|
|
||||||
ignore_dirs.uniq.each do |ignore_dir|
|
|
||||||
if absolute_path == castle_home.join(ignore_dir)
|
|
||||||
matched = true
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
next if matched
|
|
||||||
|
|
||||||
relative_dir = absolute_basedir.relative_path_from(castle_home)
|
|
||||||
home_path = home_dir.join(relative_dir).join(path)
|
|
||||||
|
|
||||||
yield(absolute_path, home_path)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def unsymlink_each(castle, basedir, subdirs)
|
def unsymlink_each(castle, basedir, subdirs)
|
||||||
each_file(castle, basedir, subdirs) do |absolute_path, home_path|
|
each_file(castle, basedir, subdirs) do |_absolute_path, home_path|
|
||||||
rm_link home_path
|
rm_link home_path
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -212,5 +160,56 @@ module Homesick
|
|||||||
|
|
||||||
rc(path)
|
rc(path)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def each_file(castle, basedir, subdirs)
|
||||||
|
absolute_basedir = Pathname.new(basedir).expand_path
|
||||||
|
castle_home = castle_dir(castle)
|
||||||
|
inside basedir do |destination_root|
|
||||||
|
FileUtils.cd(destination_root) unless destination_root == FileUtils.pwd
|
||||||
|
files = Pathname.glob('*', File::FNM_DOTMATCH)
|
||||||
|
.reject { |a| ['.', '..'].include?(a.to_s) }
|
||||||
|
.reject { |path| matches_ignored_dir? castle_home, path.expand_path, subdirs }
|
||||||
|
files.each do |path|
|
||||||
|
absolute_path = path.expand_path
|
||||||
|
|
||||||
|
relative_dir = absolute_basedir.relative_path_from(castle_home)
|
||||||
|
home_path = home_dir.join(relative_dir).join(path)
|
||||||
|
|
||||||
|
yield(absolute_path, home_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def matches_ignored_dir?(castle_home, absolute_path, subdirs)
|
||||||
|
# make ignore dirs
|
||||||
|
ignore_dirs = []
|
||||||
|
subdirs.each do |subdir|
|
||||||
|
# ignore all parent of each line in subdir file
|
||||||
|
Pathname.new(subdir).ascend do |p|
|
||||||
|
ignore_dirs.push(p)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# ignore dirs written in subdir file
|
||||||
|
ignore_dirs.uniq.each do |ignore_dir|
|
||||||
|
return true if absolute_path == castle_home.join(ignore_dir)
|
||||||
|
end
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def configure_symlinks_diff
|
||||||
|
# Hack in support for diffing symlinks
|
||||||
|
# Also adds support for checking if destination or content is a directory
|
||||||
|
shell_metaclass = class << shell; self; end
|
||||||
|
shell_metaclass.send(:define_method, :show_diff) do |destination, source|
|
||||||
|
destination = Pathname.new(destination)
|
||||||
|
source = Pathname.new(source)
|
||||||
|
return 'Unable to create diff: destination or content is a directory' if destination.directory? || source.directory?
|
||||||
|
return super(destination, File.binread(source)) unless destination.symlink?
|
||||||
|
|
||||||
|
say "- #{destination.readlink}", :red, true
|
||||||
|
say "+ #{source.expand_path}", :green, true
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
# -*- encoding : utf-8 -*-
|
|
||||||
module Homesick
|
module Homesick
|
||||||
# A representation of Homesick's version number in constants, including a
|
# A representation of Homesick's version number in constants, including a
|
||||||
# String of the entire version number
|
# String of the entire version number
|
||||||
module Version
|
module Version
|
||||||
MAJOR = 1
|
MAJOR = 1
|
||||||
MINOR = 1
|
MINOR = 1
|
||||||
PATCH = 1
|
PATCH = 6
|
||||||
|
|
||||||
STRING = [MAJOR, MINOR, PATCH].compact.join('.')
|
STRING = [MAJOR, MINOR, PATCH].compact.join('.')
|
||||||
end
|
end
|
||||||
|
|||||||
57
script/run-behavior-suite-docker.sh
Executable file
57
script/run-behavior-suite-docker.sh
Executable file
@@ -0,0 +1,57 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
: "${HOMESICK_CMD:=ruby /workspace/bin/homesick}"
|
||||||
|
behavior_verbose="${BEHAVIOR_VERBOSE:-0}"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-v|--verbose)
|
||||||
|
echo "Enabling verbose output for behavior suite"
|
||||||
|
behavior_verbose=1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown argument: $1" >&2
|
||||||
|
echo "Usage: $0 [--verbose]" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
repo_root="$(cd "$script_dir/.." && pwd)"
|
||||||
|
|
||||||
|
run_docker_build() {
|
||||||
|
echo "Building Docker image for behavior suite..."
|
||||||
|
local build_log
|
||||||
|
local -a build_cmd
|
||||||
|
|
||||||
|
if docker buildx version >/dev/null 2>&1; then
|
||||||
|
build_cmd=(docker buildx build --load -f "$repo_root/docker/behavior/Dockerfile" -t homesick-behavior:latest "$repo_root")
|
||||||
|
else
|
||||||
|
build_cmd=(docker build -f "$repo_root/docker/behavior/Dockerfile" -t homesick-behavior:latest "$repo_root")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$behavior_verbose" == "1" ]]; then
|
||||||
|
"${build_cmd[@]}"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
build_log="$(mktemp)"
|
||||||
|
if ! "${build_cmd[@]}" >"$build_log" 2>&1; then
|
||||||
|
cat "$build_log" >&2
|
||||||
|
rm -f "$build_log"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$build_log"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_docker_build
|
||||||
|
|
||||||
|
echo "Running behavior suite in Docker container..."
|
||||||
|
docker run --rm \
|
||||||
|
-e HOMESICK_CMD="$HOMESICK_CMD" \
|
||||||
|
-e BEHAVIOR_VERBOSE="$behavior_verbose" \
|
||||||
|
homesick-behavior:latest
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- encoding : utf-8 -*-
|
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
require 'capture-output'
|
require 'capture-output'
|
||||||
|
require 'pathname'
|
||||||
|
|
||||||
describe Homesick::CLI do
|
describe Homesick::CLI do
|
||||||
let(:home) { create_construct }
|
let(:home) { create_construct }
|
||||||
@@ -12,6 +12,48 @@ describe Homesick::CLI do
|
|||||||
|
|
||||||
before { allow(homesick).to receive(:repos_dir).and_return(castles) }
|
before { allow(homesick).to receive(:repos_dir).and_return(castles) }
|
||||||
|
|
||||||
|
describe 'smoke tests' do
|
||||||
|
context 'when running bin/homesick' do
|
||||||
|
before do
|
||||||
|
bin_path = Pathname.new(__FILE__).parent.parent
|
||||||
|
@output = `#{bin_path.expand_path}/bin/homesick`
|
||||||
|
end
|
||||||
|
it 'should output some text when bin/homesick is called' do
|
||||||
|
expect(@output.length).to be > 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when a git version that doesn\'t meet the minimum required is installed' do
|
||||||
|
before do
|
||||||
|
expect_any_instance_of(Homesick::Actions::GitActions).to receive(:`).and_return('git version 1.7.6')
|
||||||
|
end
|
||||||
|
it 'should raise an exception' do
|
||||||
|
output = Capture.stdout { expect { Homesick::CLI.new }.to raise_error SystemExit }
|
||||||
|
expect(output.chomp).to include(Homesick::Actions::GitActions::STRING)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when a git version that is the same as the minimum required is installed' do
|
||||||
|
before do
|
||||||
|
expect_any_instance_of(Homesick::Actions::GitActions).to receive(:`).at_least(:once).and_return("git version #{Homesick::Actions::GitActions::STRING}")
|
||||||
|
end
|
||||||
|
it 'should not raise an exception' do
|
||||||
|
output = Capture.stdout { expect { Homesick::CLI.new }.not_to raise_error }
|
||||||
|
expect(output.chomp).not_to include(Homesick::Actions::GitActions::STRING)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when a git version that is greater than the minimum required is installed' do
|
||||||
|
before do
|
||||||
|
expect_any_instance_of(Homesick::Actions::GitActions).to receive(:`).at_least(:once).and_return('git version 3.9.8')
|
||||||
|
end
|
||||||
|
it 'should not raise an exception' do
|
||||||
|
output = Capture.stdout { expect { Homesick::CLI.new }.not_to raise_error }
|
||||||
|
expect(output.chomp).not_to include(Homesick::Actions::GitActions::STRING)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'clone' do
|
describe 'clone' do
|
||||||
context 'has a .homesickrc' do
|
context 'has a .homesickrc' do
|
||||||
it 'runs the .homesickrc' do
|
it 'runs the .homesickrc' do
|
||||||
@@ -63,55 +105,63 @@ describe Homesick::CLI do
|
|||||||
homesick.clone "file://#{bare_repo}"
|
homesick.clone "file://#{bare_repo}"
|
||||||
end
|
end
|
||||||
expect(File.directory?(File.join(home.to_s, '.homesick/repos/dotfiles')))
|
expect(File.directory?(File.join(home.to_s, '.homesick/repos/dotfiles')))
|
||||||
.to be_true
|
.to be_truthy
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'clones git repo like git://host/path/to.git' do
|
it 'clones git repo like git://host/path/to.git' do
|
||||||
expect(homesick).to receive(:git_clone)
|
expect(homesick).to receive(:git_clone)
|
||||||
.with('git://github.com/technicalpickles/pickled-vim.git')
|
.with('git://github.com/technicalpickles/pickled-vim.git', destination: Pathname.new('pickled-vim'))
|
||||||
|
|
||||||
homesick.clone 'git://github.com/technicalpickles/pickled-vim.git'
|
homesick.clone 'git://github.com/technicalpickles/pickled-vim.git'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'clones git repo like git@host:path/to.git' do
|
it 'clones git repo like git@host:path/to.git' do
|
||||||
expect(homesick).to receive(:git_clone)
|
expect(homesick).to receive(:git_clone)
|
||||||
.with('git@github.com:technicalpickles/pickled-vim.git')
|
.with('git@github.com:technicalpickles/pickled-vim.git', destination: Pathname.new('pickled-vim'))
|
||||||
|
|
||||||
homesick.clone 'git@github.com:technicalpickles/pickled-vim.git'
|
homesick.clone 'git@github.com:technicalpickles/pickled-vim.git'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'clones git repo like http://host/path/to.git' do
|
it 'clones git repo like http://host/path/to.git' do
|
||||||
expect(homesick).to receive(:git_clone)
|
expect(homesick).to receive(:git_clone)
|
||||||
.with('http://github.com/technicalpickles/pickled-vim.git')
|
.with('http://github.com/technicalpickles/pickled-vim.git', destination: Pathname.new('pickled-vim'))
|
||||||
|
|
||||||
homesick.clone 'http://github.com/technicalpickles/pickled-vim.git'
|
homesick.clone 'http://github.com/technicalpickles/pickled-vim.git'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'clones git repo like http://host/path/to' do
|
it 'clones git repo like http://host/path/to' do
|
||||||
expect(homesick).to receive(:git_clone)
|
expect(homesick).to receive(:git_clone)
|
||||||
.with('http://github.com/technicalpickles/pickled-vim')
|
.with('http://github.com/technicalpickles/pickled-vim', destination: Pathname.new('pickled-vim'))
|
||||||
|
|
||||||
homesick.clone 'http://github.com/technicalpickles/pickled-vim'
|
homesick.clone 'http://github.com/technicalpickles/pickled-vim'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'clones git repo like host-alias:repos.git' do
|
it 'clones git repo like host-alias:repos.git' do
|
||||||
expect(homesick).to receive(:git_clone).with('gitolite:pickled-vim.git')
|
expect(homesick).to receive(:git_clone).with('gitolite:pickled-vim.git',
|
||||||
|
destination: Pathname.new('pickled-vim'))
|
||||||
|
|
||||||
homesick.clone 'gitolite:pickled-vim.git'
|
homesick.clone 'gitolite:pickled-vim.git'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'throws an exception when trying to clone a malformed uri like malformed' do
|
it 'throws an exception when trying to clone a malformed uri like malformed' do
|
||||||
expect(homesick).not_to receive(:git_clone)
|
expect(homesick).not_to receive(:git_clone)
|
||||||
expect { homesick.clone 'malformed' }.to raise_error
|
expect { homesick.clone 'malformed' }.to raise_error(RuntimeError)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'clones a github repo' do
|
it 'clones a github repo' do
|
||||||
expect(homesick).to receive(:git_clone)
|
expect(homesick).to receive(:git_clone)
|
||||||
.with('https://github.com/wfarr/dotfiles.git',
|
.with('https://github.com/wfarr/dotfiles.git', destination: Pathname.new('dotfiles'))
|
||||||
destination: Pathname.new('dotfiles'))
|
|
||||||
|
|
||||||
homesick.clone 'wfarr/dotfiles'
|
homesick.clone 'wfarr/dotfiles'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'accepts a destination', :focus do
|
||||||
|
expect(homesick).to receive(:git_clone)
|
||||||
|
.with('https://github.com/wfarr/dotfiles.git',
|
||||||
|
destination: Pathname.new('other-name'))
|
||||||
|
|
||||||
|
homesick.clone 'wfarr/dotfiles', 'other-name'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'rc' do
|
describe 'rc' do
|
||||||
@@ -136,6 +186,26 @@ describe Homesick::CLI do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when options[:force] == true' do
|
||||||
|
let(:homesick) { Homesick::CLI.new [], force: true }
|
||||||
|
before do
|
||||||
|
expect_any_instance_of(Thor::Shell::Basic).to_not receive(:yes?)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'executes the .homesickrc' do
|
||||||
|
castle.file('.homesickrc') do |file|
|
||||||
|
file << "File.open(Dir.pwd + '/testing', 'w') do |f|
|
||||||
|
f.print 'testing'
|
||||||
|
end"
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(homesick).to receive(:say_status).with('eval', kind_of(Pathname))
|
||||||
|
homesick.rc castle
|
||||||
|
|
||||||
|
expect(castle.join('testing')).to exist
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when told not to do so' do
|
context 'when told not to do so' do
|
||||||
before do
|
before do
|
||||||
expect_any_instance_of(Thor::Shell::Basic).to receive(:yes?).with(be_a(String)).and_return(false)
|
expect_any_instance_of(Thor::Shell::Basic).to receive(:yes?).with(be_a(String)).and_return(false)
|
||||||
@@ -156,7 +226,7 @@ describe Homesick::CLI do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'link' do
|
describe 'link_castle' do
|
||||||
let(:castle) { given_castle('glencairn') }
|
let(:castle) { given_castle('glencairn') }
|
||||||
|
|
||||||
it 'links dotfiles from a castle to the home folder' do
|
it 'links dotfiles from a castle to the home folder' do
|
||||||
@@ -244,11 +314,9 @@ describe Homesick::CLI do
|
|||||||
home_config_dir = home.join('.config')
|
home_config_dir = home.join('.config')
|
||||||
home_someapp_dir = home_config_dir.join('someapp')
|
home_someapp_dir = home_config_dir.join('someapp')
|
||||||
expect(home_config_dir.symlink?).to eq(false)
|
expect(home_config_dir.symlink?).to eq(false)
|
||||||
expect(home_config_dir.join('.some_dotfile').readlink)
|
expect(home_config_dir.join('.some_dotfile').readlink).to eq(config_dotfile)
|
||||||
.to eq(config_dotfile)
|
|
||||||
expect(home_someapp_dir.symlink?).to eq(false)
|
expect(home_someapp_dir.symlink?).to eq(false)
|
||||||
expect(home_someapp_dir.join('.some_appfile').readlink)
|
expect(home_someapp_dir.join('.some_appfile').readlink).to eq(someapp_dotfile)
|
||||||
.to eq(someapp_dotfile)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -262,6 +330,40 @@ describe Homesick::CLI do
|
|||||||
expect(home.join('.some_dotfile').readlink).to eq(dotfile)
|
expect(home.join('.some_dotfile').readlink).to eq(dotfile)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when call and some files conflict' do
|
||||||
|
it 'shows differences for conflicting text files' do
|
||||||
|
contents = { castle: 'castle has new content', home: 'home already has content' }
|
||||||
|
|
||||||
|
dotfile = castle.file('text')
|
||||||
|
File.open(dotfile.to_s, 'w') do |f|
|
||||||
|
f.write contents[:castle]
|
||||||
|
end
|
||||||
|
File.open(home.join('text').to_s, 'w') do |f|
|
||||||
|
f.write contents[:home]
|
||||||
|
end
|
||||||
|
message = Capture.stdout { homesick.shell.show_diff(home.join('text'), dotfile) }
|
||||||
|
expect(message.b).to match(/- ?#{contents[:home]}\n.*\+ ?#{contents[:castle]}$/m)
|
||||||
|
end
|
||||||
|
it 'shows message or differences for conflicting binary files' do
|
||||||
|
# content which contains NULL character, without any parentheses, braces, ...
|
||||||
|
contents = { castle: (0..255).step(30).map(&:chr).join, home: (0..255).step(30).reverse_each.map(&:chr).join }
|
||||||
|
|
||||||
|
dotfile = castle.file('binary')
|
||||||
|
File.open(dotfile.to_s, 'w') do |f|
|
||||||
|
f.write contents[:castle]
|
||||||
|
end
|
||||||
|
File.open(home.join('binary').to_s, 'w') do |f|
|
||||||
|
f.write contents[:home]
|
||||||
|
end
|
||||||
|
message = Capture.stdout { homesick.shell.show_diff(home.join('binary'), dotfile) }
|
||||||
|
if homesick.shell.is_a?(Thor::Shell::Color)
|
||||||
|
expect(message.b).to match(/- ?#{contents[:home]}\n.*\+ ?#{contents[:castle]}$/m)
|
||||||
|
elsif homesick.shell.is_a?(Thor::Shell::Basic)
|
||||||
|
expect(message.b).to match(/^Binary files .+ differ$/)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'unlink' do
|
describe 'unlink' do
|
||||||
@@ -357,13 +459,9 @@ describe Homesick::CLI do
|
|||||||
given_castle('wtf/zomg')
|
given_castle('wtf/zomg')
|
||||||
|
|
||||||
expect(homesick).to receive(:say_status)
|
expect(homesick).to receive(:say_status)
|
||||||
.with('zomg',
|
.with('zomg', 'git://github.com/technicalpickles/zomg.git', :cyan)
|
||||||
'git://github.com/technicalpickles/zomg.git',
|
|
||||||
:cyan)
|
|
||||||
expect(homesick).to receive(:say_status)
|
expect(homesick).to receive(:say_status)
|
||||||
.with('wtf/zomg',
|
.with('wtf/zomg', 'git://github.com/technicalpickles/zomg.git', :cyan)
|
||||||
'git://github.com/technicalpickles/zomg.git',
|
|
||||||
:cyan)
|
|
||||||
|
|
||||||
homesick.list
|
homesick.list
|
||||||
end
|
end
|
||||||
@@ -373,7 +471,7 @@ describe Homesick::CLI do
|
|||||||
it 'says "nothing to commit" when there are no changes' do
|
it 'says "nothing to commit" when there are no changes' do
|
||||||
given_castle('castle_repo')
|
given_castle('castle_repo')
|
||||||
text = Capture.stdout { homesick.status('castle_repo') }
|
text = Capture.stdout { homesick.status('castle_repo') }
|
||||||
expect(text).to match(/nothing to commit \(create\/copy files and use "git add" to track\)$/)
|
expect(text).to match(%r{nothing to commit \(create/copy files and use "git add" to track\)$})
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'says "Changes to be committed" when there are changes' do
|
it 'says "Changes to be committed" when there are changes' do
|
||||||
@@ -381,9 +479,7 @@ describe Homesick::CLI do
|
|||||||
some_rc_file = home.file '.some_rc_file'
|
some_rc_file = home.file '.some_rc_file'
|
||||||
homesick.track(some_rc_file.to_s, 'castle_repo')
|
homesick.track(some_rc_file.to_s, 'castle_repo')
|
||||||
text = Capture.stdout { homesick.status('castle_repo') }
|
text = Capture.stdout { homesick.status('castle_repo') }
|
||||||
expect(text).to match(
|
expect(text).to match(%r{Changes to be committed:.*new file:\s*home\/.some_rc_file}m)
|
||||||
/Changes to be committed:.*new file:\s*home\/.some_rc_file/m
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -435,9 +531,9 @@ describe Homesick::CLI do
|
|||||||
|
|
||||||
it 'prints an error message when trying to pull a non-existant castle' do
|
it 'prints an error message when trying to pull a non-existant castle' do
|
||||||
expect(homesick).to receive('say_status').once
|
expect(homesick).to receive('say_status').once
|
||||||
.with(:error,
|
.with(:error,
|
||||||
/Could not pull castle_repo, expected .* exist and contain dotfiles/,
|
/Could not pull castle_repo, expected .* to exist and contain dotfiles/,
|
||||||
:red)
|
:red)
|
||||||
expect { homesick.pull 'castle_repo' }.to raise_error(SystemExit)
|
expect { homesick.pull 'castle_repo' }.to raise_error(SystemExit)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -447,15 +543,14 @@ describe Homesick::CLI do
|
|||||||
given_castle('glencairn')
|
given_castle('glencairn')
|
||||||
allow(homesick).to receive(:system).exactly(2).times.with('git pull --quiet')
|
allow(homesick).to receive(:system).exactly(2).times.with('git pull --quiet')
|
||||||
allow(homesick).to receive(:system).exactly(2).times
|
allow(homesick).to receive(:system).exactly(2).times
|
||||||
.with('git submodule --quiet init')
|
.with('git submodule --quiet init')
|
||||||
allow(homesick).to receive(:system).exactly(2).times
|
allow(homesick).to receive(:system).exactly(2).times
|
||||||
.with('git submodule --quiet update --init --recursive >/dev/null 2>&1')
|
.with('git submodule --quiet update --init --recursive >/dev/null 2>&1')
|
||||||
Capture.stdout do
|
Capture.stdout do
|
||||||
Capture.stderr { homesick.invoke 'pull', [], all: true }
|
Capture.stderr { homesick.invoke 'pull', [], all: true }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'push' do
|
describe 'push' do
|
||||||
@@ -467,9 +562,7 @@ describe Homesick::CLI do
|
|||||||
|
|
||||||
it 'prints an error message when trying to push a non-existant castle' do
|
it 'prints an error message when trying to push a non-existant castle' do
|
||||||
expect(homesick).to receive('say_status').once
|
expect(homesick).to receive('say_status').once
|
||||||
.with(:error,
|
.with(:error, /Could not push castle_repo, expected .* to exist and contain dotfiles/, :red)
|
||||||
/Could not push castle_repo, expected .* exist and contain dotfiles/,
|
|
||||||
:red)
|
|
||||||
expect { homesick.push 'castle_repo' }.to raise_error(SystemExit)
|
expect { homesick.push 'castle_repo' }.to raise_error(SystemExit)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -553,7 +646,6 @@ describe Homesick::CLI do
|
|||||||
# Note that this is a test for the subdir_file related feature of track,
|
# Note that this is a test for the subdir_file related feature of track,
|
||||||
# not for the subdir_file method itself.
|
# not for the subdir_file method itself.
|
||||||
describe 'subdir_file' do
|
describe 'subdir_file' do
|
||||||
|
|
||||||
it 'adds the nested files parent to the subdir_file' do
|
it 'adds the nested files parent to the subdir_file' do
|
||||||
castle = given_castle('castle_repo')
|
castle = given_castle('castle_repo')
|
||||||
|
|
||||||
@@ -628,9 +720,7 @@ describe Homesick::CLI do
|
|||||||
|
|
||||||
it 'returns an error message when the given castle does not exist' do
|
it 'returns an error message when the given castle does not exist' do
|
||||||
expect(homesick).to receive('say_status').once
|
expect(homesick).to receive('say_status').once
|
||||||
.with(:error,
|
.with(:error, /Could not cd castle_repo, expected .* to exist and contain dotfiles/, :red)
|
||||||
/Could not cd castle_repo, expected .* exist and contain dotfiles/,
|
|
||||||
:red)
|
|
||||||
expect { homesick.cd 'castle_repo' }.to raise_error(SystemExit)
|
expect { homesick.cd 'castle_repo' }.to raise_error(SystemExit)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -643,27 +733,27 @@ describe Homesick::CLI do
|
|||||||
allow(ENV).to receive(:[]).with('EDITOR').and_return('vim')
|
allow(ENV).to receive(:[]).with('EDITOR').and_return('vim')
|
||||||
given_castle 'castle_repo'
|
given_castle 'castle_repo'
|
||||||
expect(homesick).to receive('inside').once.with(kind_of(Pathname)).and_yield
|
expect(homesick).to receive('inside').once.with(kind_of(Pathname)).and_yield
|
||||||
expect(homesick).to receive('system').once.with('vim')
|
expect(homesick).to receive('system').once.with('vim .')
|
||||||
Capture.stdout { homesick.open 'castle_repo' }
|
Capture.stdout { homesick.open 'castle_repo' }
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns an error message when the $EDITOR environment variable is not set' do
|
it 'returns an error message when the $EDITOR environment variable is not set' do
|
||||||
|
# Return empty ENV, the test does not call it anyway
|
||||||
|
allow(ENV).to receive(:[]).and_return(nil)
|
||||||
# Set the default editor to make sure it fails.
|
# Set the default editor to make sure it fails.
|
||||||
allow(ENV).to receive(:[]).with('EDITOR').and_return(nil)
|
allow(ENV).to receive(:[]).with('EDITOR').and_return(nil)
|
||||||
expect(homesick).to receive('say_status').once
|
expect(homesick).to receive('say_status').once
|
||||||
.with(:error,
|
.with(:error, 'The $EDITOR environment variable must be set to use this command', :red)
|
||||||
'The $EDITOR environment variable must be set to use this command',
|
|
||||||
:red)
|
|
||||||
expect { homesick.open 'castle_repo' }.to raise_error(SystemExit)
|
expect { homesick.open 'castle_repo' }.to raise_error(SystemExit)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns an error message when the given castle does not exist' do
|
it 'returns an error message when the given castle does not exist' do
|
||||||
|
# Return empty ENV, the test does not call it anyway
|
||||||
|
allow(ENV).to receive(:[]).and_return(nil)
|
||||||
# Set a default just in case none is set
|
# Set a default just in case none is set
|
||||||
allow(ENV).to receive(:[]).with('EDITOR').and_return('vim')
|
allow(ENV).to receive(:[]).with('EDITOR').and_return('vim')
|
||||||
allow(homesick).to receive('say_status').once
|
allow(homesick).to receive('say_status').once
|
||||||
.with(:error,
|
.with(:error, /Could not open castle_repo, expected .* to exist and contain dotfiles/, :red)
|
||||||
/Could not open castle_repo, expected .* exist and contain dotfiles/,
|
|
||||||
:red)
|
|
||||||
expect { homesick.open 'castle_repo' }.to raise_error(SystemExit)
|
expect { homesick.open 'castle_repo' }.to raise_error(SystemExit)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -671,7 +761,7 @@ describe Homesick::CLI do
|
|||||||
describe 'version' do
|
describe 'version' do
|
||||||
it 'prints the current version of homesick' do
|
it 'prints the current version of homesick' do
|
||||||
text = Capture.stdout { homesick.version }
|
text = Capture.stdout { homesick.version }
|
||||||
expect(text.chomp).to match(/\d+\.\d+\.\d+/)
|
expect(text.chomp).to match(/#{Regexp.escape(Homesick::Version::STRING)}/)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -682,9 +772,7 @@ describe Homesick::CLI do
|
|||||||
it 'executes a single command with no arguments inside a given castle' do
|
it 'executes a single command with no arguments inside a given castle' do
|
||||||
allow(homesick).to receive('inside').once.with(kind_of(Pathname)).and_yield
|
allow(homesick).to receive('inside').once.with(kind_of(Pathname)).and_yield
|
||||||
allow(homesick).to receive('say_status').once
|
allow(homesick).to receive('say_status').once
|
||||||
.with(be_a(String),
|
.with(be_a(String), be_a(String), :green)
|
||||||
be_a(String),
|
|
||||||
:green)
|
|
||||||
allow(homesick).to receive('system').once.with('ls')
|
allow(homesick).to receive('system').once.with('ls')
|
||||||
Capture.stdout { homesick.exec 'castle_repo', 'ls' }
|
Capture.stdout { homesick.exec 'castle_repo', 'ls' }
|
||||||
end
|
end
|
||||||
@@ -692,18 +780,14 @@ describe Homesick::CLI do
|
|||||||
it 'executes a single command with arguments inside a given castle' do
|
it 'executes a single command with arguments inside a given castle' do
|
||||||
allow(homesick).to receive('inside').once.with(kind_of(Pathname)).and_yield
|
allow(homesick).to receive('inside').once.with(kind_of(Pathname)).and_yield
|
||||||
allow(homesick).to receive('say_status').once
|
allow(homesick).to receive('say_status').once
|
||||||
.with(be_a(String),
|
.with(be_a(String), be_a(String), :green)
|
||||||
be_a(String),
|
|
||||||
:green)
|
|
||||||
allow(homesick).to receive('system').once.with('ls -la')
|
allow(homesick).to receive('system').once.with('ls -la')
|
||||||
Capture.stdout { homesick.exec 'castle_repo', 'ls', '-la' }
|
Capture.stdout { homesick.exec 'castle_repo', 'ls', '-la' }
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'raises an error when the method is called without a command' do
|
it 'raises an error when the method is called without a command' do
|
||||||
allow(homesick).to receive('say_status').once
|
allow(homesick).to receive('say_status').once
|
||||||
.with(:error,
|
.with(:error, be_a(String), :red)
|
||||||
be_a(String),
|
|
||||||
:red)
|
|
||||||
allow(homesick).to receive('exit').once.with(1)
|
allow(homesick).to receive('exit').once.with(1)
|
||||||
Capture.stdout { homesick.exec 'castle_repo' }
|
Capture.stdout { homesick.exec 'castle_repo' }
|
||||||
end
|
end
|
||||||
@@ -711,11 +795,9 @@ describe Homesick::CLI do
|
|||||||
context 'pretend' do
|
context 'pretend' do
|
||||||
it 'does not execute a command when the pretend option is passed' do
|
it 'does not execute a command when the pretend option is passed' do
|
||||||
allow(homesick).to receive('say_status').once
|
allow(homesick).to receive('say_status').once
|
||||||
.with(be_a(String),
|
.with(be_a(String), match(/.*Would execute.*/), :green)
|
||||||
match(/.*Would execute.*/),
|
|
||||||
:green)
|
|
||||||
expect(homesick).to receive('system').never
|
expect(homesick).to receive('system').never
|
||||||
Capture.stdout { homesick.invoke 'exec', %w(castle_repo ls -la), pretend: true }
|
Capture.stdout { homesick.invoke 'exec', %w[castle_repo ls -la], pretend: true }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -723,8 +805,8 @@ describe Homesick::CLI do
|
|||||||
it 'does not print status information when quiet is passed' do
|
it 'does not print status information when quiet is passed' do
|
||||||
expect(homesick).to receive('say_status').never
|
expect(homesick).to receive('say_status').never
|
||||||
allow(homesick).to receive('system').once
|
allow(homesick).to receive('system').once
|
||||||
.with('ls -la')
|
.with('ls -la')
|
||||||
Capture.stdout { homesick.invoke 'exec', %w(castle_repo ls -la), quiet: true }
|
Capture.stdout { homesick.invoke 'exec', %w[castle_repo ls -la], quiet: true }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -738,9 +820,7 @@ describe Homesick::CLI do
|
|||||||
it 'executes a command without arguments inside the root of each cloned castle' do
|
it 'executes a command without arguments inside the root of each cloned castle' do
|
||||||
allow(homesick).to receive('inside_each_castle').exactly(:twice).and_yield('castle_repo').and_yield('another_castle_repo')
|
allow(homesick).to receive('inside_each_castle').exactly(:twice).and_yield('castle_repo').and_yield('another_castle_repo')
|
||||||
allow(homesick).to receive('say_status').at_least(:once)
|
allow(homesick).to receive('say_status').at_least(:once)
|
||||||
.with(be_a(String),
|
.with(be_a(String), be_a(String), :green)
|
||||||
be_a(String),
|
|
||||||
:green)
|
|
||||||
allow(homesick).to receive('system').at_least(:once).with('ls')
|
allow(homesick).to receive('system').at_least(:once).with('ls')
|
||||||
Capture.stdout { homesick.exec_all 'ls' }
|
Capture.stdout { homesick.exec_all 'ls' }
|
||||||
end
|
end
|
||||||
@@ -748,18 +828,14 @@ describe Homesick::CLI do
|
|||||||
it 'executes a command with arguments inside the root of each cloned castle' do
|
it 'executes a command with arguments inside the root of each cloned castle' do
|
||||||
allow(homesick).to receive('inside_each_castle').exactly(:twice).and_yield('castle_repo').and_yield('another_castle_repo')
|
allow(homesick).to receive('inside_each_castle').exactly(:twice).and_yield('castle_repo').and_yield('another_castle_repo')
|
||||||
allow(homesick).to receive('say_status').at_least(:once)
|
allow(homesick).to receive('say_status').at_least(:once)
|
||||||
.with(be_a(String),
|
.with(be_a(String), be_a(String), :green)
|
||||||
be_a(String),
|
|
||||||
:green)
|
|
||||||
allow(homesick).to receive('system').at_least(:once).with('ls -la')
|
allow(homesick).to receive('system').at_least(:once).with('ls -la')
|
||||||
Capture.stdout { homesick.exec_all 'ls', '-la' }
|
Capture.stdout { homesick.exec_all 'ls', '-la' }
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'raises an error when the method is called without a command' do
|
it 'raises an error when the method is called without a command' do
|
||||||
allow(homesick).to receive('say_status').once
|
allow(homesick).to receive('say_status').once
|
||||||
.with(:error,
|
.with(:error, be_a(String), :red)
|
||||||
be_a(String),
|
|
||||||
:red)
|
|
||||||
allow(homesick).to receive('exit').once.with(1)
|
allow(homesick).to receive('exit').once.with(1)
|
||||||
Capture.stdout { homesick.exec_all }
|
Capture.stdout { homesick.exec_all }
|
||||||
end
|
end
|
||||||
@@ -767,11 +843,9 @@ describe Homesick::CLI do
|
|||||||
context 'pretend' do
|
context 'pretend' do
|
||||||
it 'does not execute a command when the pretend option is passed' do
|
it 'does not execute a command when the pretend option is passed' do
|
||||||
allow(homesick).to receive('say_status').at_least(:once)
|
allow(homesick).to receive('say_status').at_least(:once)
|
||||||
.with(be_a(String),
|
.with(be_a(String), match(/.*Would execute.*/), :green)
|
||||||
match(/.*Would execute.*/),
|
|
||||||
:green)
|
|
||||||
expect(homesick).to receive('system').never
|
expect(homesick).to receive('system').never
|
||||||
Capture.stdout { homesick.invoke 'exec_all', %w(ls -la), pretend: true }
|
Capture.stdout { homesick.invoke 'exec_all', %w[ls -la], pretend: true }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -779,8 +853,8 @@ describe Homesick::CLI do
|
|||||||
it 'does not print status information when quiet is passed' do
|
it 'does not print status information when quiet is passed' do
|
||||||
expect(homesick).to receive('say_status').never
|
expect(homesick).to receive('say_status').never
|
||||||
allow(homesick).to receive('system').at_least(:once)
|
allow(homesick).to receive('system').at_least(:once)
|
||||||
.with('ls -la')
|
.with('ls -la')
|
||||||
Capture.stdout { homesick.invoke 'exec_all', %w(ls -la), quiet: true }
|
Capture.stdout { homesick.invoke 'exec_all', %w[ls -la], quiet: true }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ $LOAD_PATH.unshift(File.dirname(__FILE__))
|
|||||||
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
||||||
require 'homesick'
|
require 'homesick'
|
||||||
require 'rspec'
|
require 'rspec'
|
||||||
require 'rspec/autorun'
|
|
||||||
require 'test_construct'
|
require 'test_construct'
|
||||||
require 'tempfile'
|
require 'tempfile'
|
||||||
|
|
||||||
|
|||||||
193
test/behavior/behavior_suite.sh
Executable file
193
test/behavior/behavior_suite.sh
Executable file
@@ -0,0 +1,193 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
: "${HOMESICK_CMD:=ruby /workspace/bin/homesick}"
|
||||||
|
: "${BEHAVIOR_VERBOSE:=0}"
|
||||||
|
|
||||||
|
RUN_OUTPUT=""
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
echo "FAIL: $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
pass() {
|
||||||
|
if [[ -t 1 ]]; then
|
||||||
|
printf ' \033[32mPassed\033[0m\n'
|
||||||
|
else
|
||||||
|
echo " Passed"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_args() {
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-v|--verbose)
|
||||||
|
BEHAVIOR_VERBOSE=1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
fail "unknown argument: $1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
run_git() {
|
||||||
|
if [[ "$BEHAVIOR_VERBOSE" == "1" ]]; then
|
||||||
|
git "$@"
|
||||||
|
else
|
||||||
|
git "$@" >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_path_exists() {
|
||||||
|
local path="$1"
|
||||||
|
[[ -e "$path" ]] || fail "expected path to exist: $path"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_path_missing() {
|
||||||
|
local path="$1"
|
||||||
|
[[ ! -e "$path" ]] || fail "expected path to be missing: $path"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_symlink_target() {
|
||||||
|
local link_path="$1"
|
||||||
|
local expected_target="$2"
|
||||||
|
[[ -L "$link_path" ]] || fail "expected symlink: $link_path"
|
||||||
|
local actual_target
|
||||||
|
actual_target="$(readlink "$link_path")"
|
||||||
|
[[ "$actual_target" == "$expected_target" ]] || fail "expected symlink target '$expected_target' but got '$actual_target'"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_homesick() {
|
||||||
|
local out_file
|
||||||
|
local output
|
||||||
|
out_file="$(mktemp)"
|
||||||
|
if ! bash -lc "$HOMESICK_CMD $*" >"$out_file" 2>&1; then
|
||||||
|
cat "$out_file" >&2
|
||||||
|
rm -f "$out_file"
|
||||||
|
fail "homesick command failed: $*"
|
||||||
|
fi
|
||||||
|
|
||||||
|
output="$(cat "$out_file")"
|
||||||
|
RUN_OUTPUT="$output"
|
||||||
|
|
||||||
|
if [[ "$BEHAVIOR_VERBOSE" == "1" && -n "$output" ]]; then
|
||||||
|
printf '%s\n' "$output"
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$out_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_remote_castle() {
|
||||||
|
local remote_dir="$1"
|
||||||
|
local work_dir="$2"
|
||||||
|
|
||||||
|
mkdir -p "$remote_dir"
|
||||||
|
run_git init --bare "$remote_dir/base.git"
|
||||||
|
|
||||||
|
mkdir -p "$work_dir/base"
|
||||||
|
pushd "$work_dir/base" >/dev/null
|
||||||
|
run_git init
|
||||||
|
run_git config user.email "behavior@test.local"
|
||||||
|
run_git config user.name "Behavior Test"
|
||||||
|
|
||||||
|
mkdir -p home/.config/myapp
|
||||||
|
echo "set number" > home/.vimrc
|
||||||
|
echo "export PATH=\"$PATH:$HOME/bin\"" > home/.zshrc
|
||||||
|
echo "option=true" > home/.config/myapp/config.toml
|
||||||
|
printf '.config\n' > .homesick_subdir
|
||||||
|
|
||||||
|
run_git add .
|
||||||
|
run_git commit -m "initial castle"
|
||||||
|
run_git remote add origin "$remote_dir/base.git"
|
||||||
|
run_git push -u origin master
|
||||||
|
popd >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_local_test_file() {
|
||||||
|
mkdir -p "$HOME/.local/bin"
|
||||||
|
echo "#!/usr/bin/env bash" > "$HOME/.local/bin/tool"
|
||||||
|
chmod +x "$HOME/.local/bin/tool"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_suite() {
|
||||||
|
local tmp_root
|
||||||
|
tmp_root="$(mktemp -d)"
|
||||||
|
trap "rm -rf '$tmp_root'" EXIT
|
||||||
|
|
||||||
|
export HOME="$tmp_root/home"
|
||||||
|
mkdir -p "$HOME"
|
||||||
|
|
||||||
|
local remote_root="$tmp_root/remote"
|
||||||
|
local work_root="$tmp_root/work"
|
||||||
|
|
||||||
|
setup_remote_castle "$remote_root" "$work_root"
|
||||||
|
|
||||||
|
echo "[1/7] clone"
|
||||||
|
run_homesick "clone file://$remote_root/base.git parity-castle"
|
||||||
|
assert_path_exists "$HOME/.homesick/repos/parity-castle/.git"
|
||||||
|
assert_path_exists "$HOME/.homesick/repos/parity-castle/home/.vimrc"
|
||||||
|
pass
|
||||||
|
|
||||||
|
echo "[2/7] link"
|
||||||
|
run_homesick "link parity-castle"
|
||||||
|
assert_symlink_target "$HOME/.vimrc" "$HOME/.homesick/repos/parity-castle/home/.vimrc"
|
||||||
|
assert_symlink_target "$HOME/.zshrc" "$HOME/.homesick/repos/parity-castle/home/.zshrc"
|
||||||
|
assert_path_exists "$HOME/.config/myapp"
|
||||||
|
assert_symlink_target "$HOME/.config/myapp" "$HOME/.homesick/repos/parity-castle/home/.config/myapp"
|
||||||
|
pass
|
||||||
|
|
||||||
|
echo "[3/7] unlink"
|
||||||
|
run_homesick "unlink parity-castle"
|
||||||
|
assert_path_missing "$HOME/.vimrc"
|
||||||
|
assert_path_missing "$HOME/.zshrc"
|
||||||
|
assert_path_exists "$HOME/.config"
|
||||||
|
assert_path_missing "$HOME/.config/myapp"
|
||||||
|
pass
|
||||||
|
|
||||||
|
echo "[4/7] relink + track"
|
||||||
|
run_homesick "link parity-castle"
|
||||||
|
setup_local_test_file
|
||||||
|
run_homesick "track $HOME/.local/bin/tool parity-castle"
|
||||||
|
assert_symlink_target "$HOME/.local/bin/tool" "$HOME/.homesick/repos/parity-castle/home/.local/bin/tool"
|
||||||
|
assert_path_exists "$HOME/.homesick/repos/parity-castle/.homesick_subdir"
|
||||||
|
grep -q "^.local/bin$" "$HOME/.homesick/repos/parity-castle/.homesick_subdir" || fail "expected .homesick_subdir to contain .local/bin"
|
||||||
|
pass
|
||||||
|
|
||||||
|
echo "[5/7] list and show_path"
|
||||||
|
local list_output
|
||||||
|
run_homesick "list"
|
||||||
|
list_output="$RUN_OUTPUT"
|
||||||
|
[[ "$list_output" == *"parity-castle"* ]] || fail "expected list output to include parity-castle"
|
||||||
|
local show_path_output
|
||||||
|
run_homesick "show_path parity-castle"
|
||||||
|
show_path_output="$RUN_OUTPUT"
|
||||||
|
[[ "$show_path_output" == *"$HOME/.homesick/repos/parity-castle"* ]] || fail "expected show_path output to include parity-castle path"
|
||||||
|
pass
|
||||||
|
|
||||||
|
echo "[6/7] status and diff"
|
||||||
|
echo "change" >> "$HOME/.vimrc"
|
||||||
|
local status_output
|
||||||
|
run_homesick "status parity-castle"
|
||||||
|
status_output="$RUN_OUTPUT"
|
||||||
|
[[ "$status_output" == *"modified:"* ]] || fail "expected status output to include modified file"
|
||||||
|
local diff_output
|
||||||
|
run_homesick "diff parity-castle"
|
||||||
|
diff_output="$RUN_OUTPUT"
|
||||||
|
[[ "$diff_output" == *"diff --git"* ]] || fail "expected diff output to include git diff"
|
||||||
|
pass
|
||||||
|
|
||||||
|
echo "[7/7] version"
|
||||||
|
local version_output
|
||||||
|
run_homesick "version"
|
||||||
|
version_output="$RUN_OUTPUT"
|
||||||
|
[[ "$version_output" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || fail "expected semantic version output, got: $version_output"
|
||||||
|
pass
|
||||||
|
|
||||||
|
echo "PASS: behavior suite completed for command: $HOMESICK_CMD"
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_args "$@"
|
||||||
|
run_suite
|
||||||
Reference in New Issue
Block a user