Compare commits
79 Commits
v1.1.3
...
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 |
10
.gitignore
vendored
10
.gitignore
vendored
@@ -48,3 +48,13 @@ Gemfile.lock
|
|||||||
vendor/
|
vendor/
|
||||||
|
|
||||||
homesick*.gem
|
homesick*.gem
|
||||||
|
|
||||||
|
# Go scaffolding artifacts
|
||||||
|
dist/
|
||||||
|
*.test
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# rbenv configuration
|
||||||
|
.ruby-version
|
||||||
|
|
||||||
|
.github/*
|
||||||
@@ -1,6 +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
|
sudo: false
|
||||||
|
|||||||
@@ -1,19 +1,34 @@
|
|||||||
#1.1.3
|
# 1.1.6
|
||||||
* Allow a destinaton to be passed when cloning a castle
|
* 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
|
* Make sure `homesick edit` opens default editor in the root of the given castle
|
||||||
* Fixed bug when diffing edited files
|
* Fixed bug when diffing edited files
|
||||||
* Fixed crashing bug when attempting to diff directories
|
* Fixed crashing bug when attempting to diff directories
|
||||||
* Ensure that messages are escaped correctly on `git commit all`
|
* Ensure that messages are escaped correctly on `git commit all`
|
||||||
#1.1.2
|
|
||||||
|
# 1.1.2
|
||||||
* Added '--force' option to the rc command to bypass confirmation checks when running a .homesickrc file
|
* 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.
|
* 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.
|
* Code refactoring and fixes.
|
||||||
|
|
||||||
#1.1.0
|
# 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
|
||||||
|
|
||||||
@@ -45,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,28 +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", "~> 3.1.0"
|
|
||||||
gem "guard"
|
|
||||||
gem "guard-rspec"
|
|
||||||
gem "rb-readline", "~> 0.5.0"
|
|
||||||
gem "jeweler", ">= 1.6.2"
|
|
||||||
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 RbConfig::CONFIG['host_os'] =~ /darwin|mac os/
|
|
||||||
gem 'terminal-notifier-guard', '~> 1.6.1'
|
install_if -> { RUBY_PLATFORM =~ /darwin/ } do
|
||||||
|
gem 'terminal-notifier-guard', '~> 1.7.0'
|
||||||
end
|
end
|
||||||
if RUBY_VERSION >= '1.9.2'
|
|
||||||
gem "rubocop"
|
install_if -> { this_ruby < ruby_230 } do
|
||||||
|
gem 'listen', '< 3'
|
||||||
|
gem 'rack', '~> 2.0.6'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -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=
|
||||||
106
homesick.gemspec
106
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.3 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.3"
|
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 = "2015-10-31"
|
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",
|
||||||
@@ -43,51 +43,63 @@ Gem::Specification.new do |s|
|
|||||||
"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>, ["~> 3.1.0"])
|
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>, ["~> 3.1.0"])
|
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>, ["~> 3.1.0"])
|
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,14 +1,29 @@
|
|||||||
# -*- encoding : utf-8 -*-
|
|
||||||
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 = %r{\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,18 +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
|
||||||
|
protected
|
||||||
|
|
||||||
def mv(source, destination)
|
def mv(source, destination)
|
||||||
source = Pathname.new(source)
|
source = Pathname.new(source)
|
||||||
destination = Pathname.new(destination + source.basename)
|
destination = Pathname.new(destination + source.basename)
|
||||||
case
|
say_status :conflict, "#{destination} exists", :red if destination.exist? && (options[:force] || shell.file_collision(destination) { source })
|
||||||
when destination.exist? && (options[:force] || shell.file_collision(destination) { source })
|
FileUtils.mv source, destination unless options[:pretend]
|
||||||
say_status :conflict, "#{destination} exists", :red
|
|
||||||
FileUtils.mv source, destination unless options[:pretend]
|
|
||||||
else
|
|
||||||
FileUtils.mv source, destination unless options[:pretend]
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def rm_rf(dir)
|
def rm_rf(dir)
|
||||||
@@ -24,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
|
||||||
@@ -42,47 +38,40 @@ module Homesick
|
|||||||
end
|
end
|
||||||
|
|
||||||
def ln_s(source, destination)
|
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
|
|
||||||
FileUtils.ln_s source, destination, force: true unless options[:pretend]
|
|
||||||
when :conflict
|
|
||||||
say_status :conflict, "#{destination} exists", :red
|
|
||||||
|
|
||||||
if collision_accepted?(destination, source)
|
if collision_accepted?(destination, source)
|
||||||
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,4 +1,3 @@
|
|||||||
# -*- encoding : utf-8 -*-
|
|
||||||
module Homesick
|
module Homesick
|
||||||
module Actions
|
module Actions
|
||||||
# Git-related helper methods for Homesick
|
# Git-related helper methods for Homesick
|
||||||
@@ -8,18 +7,20 @@ module Homesick
|
|||||||
major: 1,
|
major: 1,
|
||||||
minor: 8,
|
minor: 8,
|
||||||
patch: 0
|
patch: 0
|
||||||
}
|
}.freeze
|
||||||
STRING = MIN_VERSION.values.join('.')
|
STRING = MIN_VERSION.values.join('.')
|
||||||
|
|
||||||
def git_version_correct?
|
def git_version_correct?
|
||||||
info = `git --version`.scan(/(\d+)\.(\d+)\.(\d+)/).flatten.map(&:to_i)
|
info = `git --version`.scan(/(\d+)\.(\d+)\.(\d+)/).flatten.map(&:to_i)
|
||||||
return false unless info.count == 3
|
return false unless info.count == 3
|
||||||
current_version = Hash[[:major, :minor, :patch].zip(info)]
|
|
||||||
return true if current_version.eql?(MIN_VERSION)
|
current_version = Hash[%i[major minor patch].zip(info)]
|
||||||
return true if current_version[:major] > MIN_VERSION[:major]
|
major_equals = current_version.eql?(MIN_VERSION)
|
||||||
return true if current_version[:major] == MIN_VERSION[:major] && current_version[:minor] > MIN_VERSION[:minor]
|
major_greater = current_version[:major] > MIN_VERSION[:major]
|
||||||
return true if current_version[:major] == MIN_VERSION[:major] && current_version[:minor] == MIN_VERSION[:minor] && current_version[:patch] >= MIN_VERSION[:patch]
|
minor_greater = current_version[:major] == MIN_VERSION[:major] && current_version[:minor] > MIN_VERSION[:minor]
|
||||||
false
|
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
|
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
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# -*- encoding : utf-8 -*-
|
require 'fileutils'
|
||||||
require 'thor'
|
require 'thor'
|
||||||
|
|
||||||
module Homesick
|
module Homesick
|
||||||
@@ -24,27 +24,17 @@ module Homesick
|
|||||||
say_status :error, "Git version >= #{Homesick::Actions::GitActions::STRING} must be installed to use Homesick", :red
|
say_status :error, "Git version >= #{Homesick::Actions::GitActions::STRING} must be installed to use Homesick", :red
|
||||||
exit(1)
|
exit(1)
|
||||||
end
|
end
|
||||||
# Hack in support for diffing symlinks
|
configure_symlinks_diff
|
||||||
# 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, content|
|
|
||||||
destination = Pathname.new(destination)
|
|
||||||
content = Pathname.new(content)
|
|
||||||
return 'Unable to create diff: destination or content is a directory' if destination.directory? || content.directory?
|
|
||||||
return super(destination, content) unless destination.symlink?
|
|
||||||
say "- #{destination.readlink}", :red, true
|
|
||||||
say "+ #{content.expand_path}", :green, true
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
desc 'clone URI CASTLE_NAME', 'Clone +uri+ as a castle with name CASTLE_NAME for homesick'
|
desc 'clone URI CASTLE_NAME', 'Clone +uri+ as a castle with name CASTLE_NAME for homesick'
|
||||||
def clone(uri, destination=nil)
|
def clone(uri, destination = nil)
|
||||||
destination = Pathname.new(destination) unless destination.nil?
|
destination = Pathname.new(destination) unless destination.nil?
|
||||||
|
|
||||||
inside repos_dir do
|
inside repos_dir do
|
||||||
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 if destination.nil?
|
destination = uri.basename if destination.nil?
|
||||||
|
|
||||||
@@ -57,7 +47,7 @@ module Homesick
|
|||||||
destination = Pathname.new(Regexp.last_match[1].gsub(/\.git$/, '')).basename if destination.nil?
|
destination = Pathname.new(Regexp.last_match[1].gsub(/\.git$/, '')).basename if destination.nil?
|
||||||
git_clone uri, destination: destination
|
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)
|
||||||
@@ -66,6 +56,7 @@ module Homesick
|
|||||||
|
|
||||||
desc 'rc CASTLE', 'Run the .homesickrc for the specified castle'
|
desc 'rc CASTLE', 'Run the .homesickrc for the specified castle'
|
||||||
method_option :force,
|
method_option :force,
|
||||||
|
type: :boolean,
|
||||||
default: false,
|
default: false,
|
||||||
desc: 'Evaluate .homesickrc without prompting.'
|
desc: 'Evaluate .homesickrc without prompting.'
|
||||||
def rc(name = DEFAULT_CASTLE_NAME)
|
def rc(name = DEFAULT_CASTLE_NAME)
|
||||||
@@ -73,8 +64,10 @@ module Homesick
|
|||||||
destination = Pathname.new(name)
|
destination = Pathname.new(name)
|
||||||
homesickrc = destination.join('.homesickrc').expand_path
|
homesickrc = destination.join('.homesickrc').expand_path
|
||||||
return unless homesickrc.exist?
|
return unless homesickrc.exist?
|
||||||
|
|
||||||
proceed = options[:force] || shell.yes?("#{name} has a .homesickrc. Proceed with evaling it? (This could be destructive)")
|
proceed = options[:force] || shell.yes?("#{name} has a .homesickrc. Proceed with evaling it? (This could be destructive)")
|
||||||
return say_status 'eval skip', "not evaling #{homesickrc}, #{destination} may need manual configuration", :blue unless proceed
|
return say_status 'eval skip', "not evaling #{homesickrc}, #{destination} may need manual configuration", :blue unless proceed
|
||||||
|
|
||||||
say_status 'eval', homesickrc
|
say_status 'eval', homesickrc
|
||||||
inside destination do
|
inside destination do
|
||||||
eval homesickrc.read, binding, homesickrc.expand_path.to_s
|
eval homesickrc.read, binding, homesickrc.expand_path.to_s
|
||||||
@@ -128,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|
|
||||||
|
|||||||
@@ -1,25 +1,8 @@
|
|||||||
# -*- encoding : utf-8 -*-
|
|
||||||
require 'pathname'
|
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
|
||||||
@@ -36,8 +19,9 @@ module Homesick
|
|||||||
|
|
||||||
def check_castle_existance(name, action)
|
def check_castle_existance(name, action)
|
||||||
return if castle_dir(name).exist?
|
return if castle_dir(name).exist?
|
||||||
|
|
||||||
say_status :error,
|
say_status :error,
|
||||||
"Could not #{action} #{name}, expected #{castle_dir(name)} exist and contain dotfiles",
|
"Could not #{action} #{name}, expected #{castle_dir(name)} to exist and contain dotfiles",
|
||||||
:red
|
:red
|
||||||
exit(1)
|
exit(1)
|
||||||
end
|
end
|
||||||
@@ -149,47 +133,11 @@ module Homesick
|
|||||||
end
|
end
|
||||||
|
|
||||||
def collision_accepted?(destination, source)
|
def collision_accepted?(destination, source)
|
||||||
fail "Arguments must be instances of Pathname, #{destination.class.name} and #{source.class.name} given" unless destination.instance_of?(Pathname) && source.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
|
||||||
@@ -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 = 3
|
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,4 +1,3 @@
|
|||||||
# -*- encoding : utf-8 -*-
|
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
require 'capture-output'
|
require 'capture-output'
|
||||||
require 'pathname'
|
require 'pathname'
|
||||||
@@ -146,7 +145,7 @@ describe Homesick::CLI do
|
|||||||
|
|
||||||
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
|
||||||
@@ -158,8 +157,8 @@ describe Homesick::CLI do
|
|||||||
|
|
||||||
it 'accepts a destination', :focus do
|
it 'accepts a destination', :focus 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('other-name'))
|
destination: Pathname.new('other-name'))
|
||||||
|
|
||||||
homesick.clone 'wfarr/dotfiles', 'other-name'
|
homesick.clone 'wfarr/dotfiles', 'other-name'
|
||||||
end
|
end
|
||||||
@@ -331,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
|
||||||
@@ -498,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
|
||||||
|
|
||||||
@@ -510,9 +543,9 @@ 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
|
||||||
@@ -529,7 +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, /Could not push castle_repo, expected .* exist and contain dotfiles/, :red)
|
.with(:error, /Could not push castle_repo, expected .* to 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
|
||||||
@@ -687,7 +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, /Could not cd castle_repo, expected .* exist and contain dotfiles/, :red)
|
.with(:error, /Could not cd castle_repo, expected .* to 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
|
||||||
@@ -705,18 +738,22 @@ describe Homesick::CLI do
|
|||||||
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, 'The $EDITOR environment variable must be set to use this command', :red)
|
.with(:error, '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, /Could not open castle_repo, expected .* exist and contain dotfiles/, :red)
|
.with(:error, /Could not open castle_repo, expected .* to 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
|
||||||
@@ -735,7 +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), be_a(String), :green)
|
.with(be_a(String), 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
|
||||||
@@ -743,14 +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), be_a(String), :green)
|
.with(be_a(String), 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, be_a(String), :red)
|
.with(:error, 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
|
||||||
@@ -758,9 +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), match(/.*Would execute.*/), :green)
|
.with(be_a(String), 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
|
||||||
|
|
||||||
@@ -768,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
|
||||||
@@ -783,7 +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), be_a(String), :green)
|
.with(be_a(String), 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
|
||||||
@@ -791,14 +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), be_a(String), :green)
|
.with(be_a(String), 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, be_a(String), :red)
|
.with(:error, 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
|
||||||
@@ -806,9 +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), match(/.*Would execute.*/), :green)
|
.with(be_a(String), 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
|
||||||
|
|
||||||
@@ -816,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
|
||||||
|
|||||||
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