249 Commits

Author SHA1 Message Date
Micheal Wilkinson
8fc831dfdf chore(ci): re-enable Go module caching and add coverage badge to README
All checks were successful
Push Validation / validate (push) Successful in 1m52s
2026-03-20 13:55:09 +00:00
Micheal Wilkinson
7e32cd83c5 chore(ci): install aws cli via setup action
All checks were successful
Push Validation / validate (push) Successful in 1m46s
2026-03-20 13:42:23 +00:00
Micheal Wilkinson
3d71433630 chore(ci): pin Go toolchain to 1.26.1 in workflows
Some checks failed
Push Validation / validate (push) Failing after 1m30s
2026-03-20 13:30:19 +00:00
Micheal Wilkinson
c6c382afce chore(ci): add bash as default shell for workflows
Some checks failed
Push Validation / validate (push) Failing after 1m17s
2026-03-20 13:20:24 +00:00
Micheal Wilkinson
665401f2bd chore(ci): use catthehacker/ubuntu container for better tool availability
Some checks failed
Push Validation / validate (push) Failing after 1m38s
2026-03-20 13:16:11 +00:00
Micheal Wilkinson
d084abd636 chore(ci): remove Go module caching to eliminate artifact cache timeouts 2026-03-20 13:13:53 +00:00
Micheal Wilkinson
a6034ce470 chore(bash): remove redundant bash script 2026-03-20 13:06:46 +00:00
Micheal Wilkinson
484db0781b ci(gitea): use pipx for awscli installation
Some checks failed
Push Validation / validate (push) Failing after 6m34s
2026-03-20 13:04:30 +00:00
Micheal Wilkinson
4a8ef7e1f6 ci(gitea): use pip for awscli installation
Some checks failed
Push Validation / validate (push) Failing after 5m59s
2026-03-20 12:53:09 +00:00
Micheal Wilkinson
b3f66e9e2e docs(changelog): note go cache in gitea pipelines 2026-03-20 12:07:18 +00:00
Micheal Wilkinson
9d6dacb0f8 ci: cache go modules and build outputs in workflows 2026-03-20 12:07:10 +00:00
Micheal Wilkinson
195b936de6 docs(changelog): note coverage artefact publishing
Some checks failed
Push Validation / validate (push) Failing after 6m32s
2026-03-20 11:46:12 +00:00
Micheal Wilkinson
f6b5186f31 ci(gitea): publish coverage reports to artefact storage 2026-03-20 11:46:05 +00:00
Micheal Wilkinson
ea16ba8430 chore(go): Removing unused function 2026-03-20 09:57:40 +00:00
Micheal Wilkinson
96ce572792 docs(changelog): note CLI help messaging improvements 2026-03-20 09:56:16 +00:00
Micheal Wilkinson
d638f201fe fix(cli): improve help name and description 2026-03-20 09:54:42 +00:00
Micheal Wilkinson
e09bdd78c2 docs(changelog): note unified push validation workflow 2026-03-20 09:50:12 +00:00
Micheal Wilkinson
0034a6f4e2 ci(gitea): unify push and merged-pr validation 2026-03-20 09:50:00 +00:00
Micheal Wilkinson
aa66695665 docs(readme): add workflow status badges 2026-03-20 09:41:28 +00:00
Micheal Wilkinson
a7e4c501e4 ci(gitea): add validation and release workflows 2026-03-20 09:37:09 +00:00
Micheal Wilkinson
0dfacc31d4 chore(build): rename binary to gosick 2026-03-19 16:33:45 +00:00
Micheal Wilkinson
1d26594010 docs(changelog): add unreleased migration notes 2026-03-19 16:29:39 +00:00
Micheal Wilkinson
c10ff251d5 docs(changelog): update formatting 2026-03-19 16:29:34 +00:00
Micheal Wilkinson
8d34674415 chore: remove Ruby implementation and tooling 2026-03-19 16:17:54 +00:00
Micheal Wilkinson
8174c6a983 refactor(cli): use kong for command parsing 2026-03-19 14:51:47 +00:00
Micheal Wilkinson
1d4c088edc test(cli): add parser coverage for kong refactor 2026-03-19 14:40:08 +00:00
Micheal Wilkinson
040bf31b56 fix(core): route status and diff output through app writers 2026-03-19 14:29:52 +00:00
Micheal Wilkinson
4355e7fd9d test(core): add status diff and version suites 2026-03-19 14:29:03 +00:00
Micheal Wilkinson
b7c353553a test(core): add dedicated list and show_path suites 2026-03-19 14:25:37 +00:00
Micheal Wilkinson
2f45d28acb feat(core,cli): implement track command with go-git staging 2026-03-19 14:21:15 +00:00
Micheal Wilkinson
904c1be192 chore(go): Adding fun comment 2026-03-19 14:20:15 +00:00
Micheal Wilkinson
f443e96f9e test(core): add failing track behavior suite 2026-03-19 14:19:29 +00:00
Micheal Wilkinson
0076588e1f chore(git): updating ignore to split irrelevant files out 2026-03-19 14:16:15 +00:00
Micheal Wilkinson
919f033c8b feat(go): implement unlink 2026-03-19 14:11:49 +00:00
Micheal Wilkinson
dbc77a1b34 feat(core): reimplement clone with go-git 2026-03-19 14:05:50 +00:00
Micheal Wilkinson
d02d118b28 test(core): add failing clone suite for go-git migration 2026-03-19 13:58:25 +00:00
Micheal Wilkinson
a952c4f6bf chore(just): build linux binary for behavior-go 2026-03-19 13:48:26 +00:00
Micheal Wilkinson
e733dff818 feat(go): implement link with subdir and force handling 2026-03-19 13:46:48 +00:00
Micheal Wilkinson
41584dec6a chore(go): scaffold module and add failing link tests 2026-03-19 13:44:02 +00:00
Micheal Wilkinson
005209703e Adding a set of behavioural tests 2026-03-19 10:57:25 +00:00
Jeremy Cook
ee4388b0f4 Moved code to a more logical home. 2019-01-19 23:21:27 -05:00
Jeremy Cook
a44a514007 Reduced visibility of methods. 2019-01-19 18:59:46 -05:00
Jeremy Cook
9431cb78af Moved code to utils to reduce method complexity. 2019-01-19 18:55:02 -05:00
Jeremy Cook
46c52769a6 Fixed issue where using pretend option would not evaluate files
correctly.
2019-01-19 15:03:28 -05:00
Jeremy Cook
fdb57cd846 Apply fixes suggested by Rubocop. 2019-01-19 11:50:20 -05:00
Jeremy Cook
ff387280d5 Merge branch 'master' of github.com:technicalpickles/homesick 2019-01-18 23:33:41 -05:00
Jeremy Cook
f09c62d922 Minor fixes suggested by rubocop. 2019-01-18 23:32:31 -05:00
Balint Reczey
dd7d52a25d Run Travis tests on Ruby 2.5.0, too 2019-01-18 17:02:57 -05:00
Balint Reczey
f1630ece79 Fix tests on Ruby 2.5 2019-01-18 17:02:57 -05:00
Denny Schäfer
11ee8cdc0d Fix markdown typo 2019-01-18 17:02:57 -05:00
Jeremy Cook
ceb08cbe22 Update Gemfile to remove reference to outdated version of rack. 2019-01-18 17:02:52 -05:00
Jeremy Cook
057e1cfc59 Regenerate gemspec for version 1.1.6 2019-01-18 17:02:52 -05:00
Jeremy Cook
89f3000d8b Prepare for release of new version 2019-01-18 17:02:51 -05:00
Diego Rabatone Oliveira
36e3cb6bbf Require fileutils correctly
Fix #165
2019-01-18 17:02:51 -05:00
mail6543210
9ebae75e7d Add testcase 2019-01-18 17:02:51 -05:00
mail6543210
35e1909790 Real fix for #148 2019-01-18 17:02:51 -05:00
mail6543210
3b633ed326 Rename content to source
It is a instance of Pathname, not binary content
2019-01-18 17:02:51 -05:00
mail6543210
fdf2da84dd Revert "Use source content instead of source path (fixes: #148)"
This reverts commit ed397bdaf8.
2019-01-18 17:02:51 -05:00
Jeremy Cook
e561566b46 Merge pull request #172 from rbalint/master
Fix tests on Ruby 2.5
2018-03-08 06:37:30 -05:00
Balint Reczey
dcef34c17d Run Travis tests on Ruby 2.5.0, too 2018-03-08 08:46:36 +01:00
Balint Reczey
72d11c4a47 Fix tests on Ruby 2.5 2018-03-07 18:06:20 +01:00
Jeremy Cook
c2457bae9f Merge pull request #171 from tuxinaut/master
Fix markdown typo
2018-01-11 07:41:01 -05:00
Denny Schäfer
001bd32bb3 Fix markdown typo 2018-01-11 00:20:23 +01:00
Jeremy Cook
7080321081 Regenerate gemspec for version 1.1.6 2017-12-20 16:03:17 -05:00
Jeremy Cook
9d9cf66de6 Prepare for release of new version 2017-12-20 16:01:20 -05:00
Jeremy Cook
9e9a940825 Merge pull request #170 from diraol/fix-fileutils
Require fileutils correctly
2017-12-18 10:48:14 -05:00
Diego Rabatone Oliveira
257e974c38 Require fileutils correctly
Fix #165
2017-12-18 13:40:44 -02:00
Jeremy Cook
615e31428c Merge pull request #167 from mail6543210/master
Fix "diff" action on binary files
2017-10-13 17:59:36 -04:00
mail6543210
8c2a1d0f84 Add testcase 2017-09-23 00:41:33 +08:00
mail6543210
62c934774b Real fix for #148 2017-09-19 20:30:15 +08:00
mail6543210
d3d6974b7b Rename content to source
It is a instance of Pathname, not binary content
2017-09-19 20:29:06 +08:00
mail6543210
474d69da0b Revert "Use source content instead of source path (fixes: #148)"
This reverts commit ed397bdaf8.
2017-09-19 20:26:01 +08:00
Jeremy Cook
db6a513d1d Merge pull request #166 from MainShayne233/master
Enhancement/Add 'to' to error message.
2017-09-02 19:59:40 -04:00
Shayne Tremblay
ae343c4cab Add 'to' to error message. 2017-09-02 13:06:46 -07:00
Jeremy Cook
a2b365fb6f Merge pull request #164 from fuzzbomb/update-README-symlink-command
Rename symlink command to link in README.
2017-08-28 12:55:50 -04:00
Andrew Macpherson
4cb2006f41 Rename symlink command to link in README.
symlink command was previously renamed to link, but references in the README
were not updated.
2017-06-12 17:37:45 +01:00
Jeremy Cook
66347d307f Merge pull request #161 from philoserf/patch-1
Update ChangeLog.markdown
2017-03-23 17:33:31 -04:00
Mark Ayers
8f92a1b4f0 Update ChangeLog.markdown
Add the space that markdown requires between the header marker `#` and the header text.
2017-03-23 13:44:33 -07:00
Jeremy Cook
7a2df591c0 Regenerate gemspec for version 1.1.5 2017-03-23 08:37:44 -04:00
Jeremy Cook
4923265dea Updated homesick version and changelog due to problem with version
number in release.
2017-03-23 08:34:47 -04:00
Jeremy Cook
79421580e9 Merge branch 'master' of github.com:technicalpickles/homesick 2017-03-22 17:24:28 -04:00
Jeremy Cook
cabde9e5f1 Updated change log for new version. 2017-03-22 17:23:49 -04:00
Jeremy Cook
0d60ae9d1a Regenerate gemspec for version 1.1.4 2017-03-22 17:14:09 -04:00
Jeremy Cook
d5317b8e17 Bumped version in preparation for release. 2017-03-22 17:10:23 -04:00
Jeremy Cook
3b8a5b4be4 Merge pull request #159 from mruwek/refactor-conflict-actions
Wrap symlink and regular conflicts into one case
2017-03-20 16:07:19 -04:00
Jacek Sowiński
6590a1eeff Wrap symlink and regular conflicts into one case
This way we're not duplicating collision-related code.
2017-03-20 20:26:14 +01:00
Jeremy Cook
693ae5f05e Merge pull request #157 from mruwek/verbose-symlink-conflicts
Don't overwrite silently on symlink conflicts
2017-03-19 22:02:41 -04:00
Jeremy Cook
da3002f199 Merge pull request #154 from singular0/symlink_realpath
Use real paths of symlinks when linking castle into home
2017-03-19 22:02:01 -04:00
Jeremy Cook
feaaab2fa4 Merge pull request #158 from JCook21/master
Minor updates
2017-03-19 22:00:34 -04:00
Jeremy Cook
59f75711a4 Changed strings to use symbols. 2017-03-19 15:57:44 -04:00
Jeremy Cook
f24030b51f Updates to Gemfile to clean it up. 2017-03-19 15:57:22 -04:00
Jeremy Cook
71bb120a12 Update to dependency. 2017-03-19 14:44:44 -04:00
Jacek Sowiński
85f46e01b1 Don't overwrite silently on symlink conflicts
Symlink conflicts are now handled in similar fashion as normal
file-conflicts.
2017-03-17 19:31:23 +01:00
Jeremy Cook
c5b24b9b38 Merge pull request #155 from danielbayerlein/ruby-2.4.0
Add support for Ruby 2.4.0
2016-12-26 07:08:38 -05:00
Daniel Bayerlein
68460af45e Add support for Ruby 2.4.0 2016-12-26 10:31:34 +01:00
Denis Yantarev
5614b6b8b3 Use real paths of symlinks when linking castle into home 2016-12-25 18:34:33 +03:00
Jeremy Cook
570b063632 Merge pull request #152 from singular0/master
Thanks for taking care of this.
2016-12-24 12:13:55 -05:00
Jeremy Cook
1d398587d0 Remove config for removed ruby versions.
Deleted config for unused ruby versions.
2016-12-24 12:11:01 -05:00
Jeremy Cook
085853faaa Merge branch 'master' into master 2016-12-24 12:05:54 -05:00
Jeremy Cook
21b4e344a9 Merge pull request #153 from danielbayerlein/ruby-version
Looks good to me, thanks!
2016-12-24 12:03:51 -05:00
Daniel Bayerlein
a6194dfe8b Update RSpec 2016-12-23 11:59:44 +01:00
Daniel Bayerlein
5692194fa2 Add support for Ruby 2.2.6 and 2.3.3 2016-12-23 11:37:47 +01:00
Daniel Bayerlein
11745098c2 Support Ruby 2.1.0, 2.2.0, 2.3.0 2016-12-23 11:23:11 +01:00
Denis Yantarev
b1bb0c996c Add Ruby 2.2 & 2.3 to Travis config and fix GEM dependencies 2016-12-05 03:34:57 +03:00
Denis Yantarev
a62039da50 Ignore rbenv configuration files 2016-12-05 03:34:14 +03:00
Denis Yantarev
4bfd1c60c2 Fix default option value type warning 2016-12-03 15:56:22 +03:00
Jeremy Cook
f0e11abb5b Merge pull request #149 from mail6543210/master
Use source content instead of source path (fixes: #148). Thanks for fixing this!
2016-01-21 20:05:33 -05:00
mail6543210
ed397bdaf8 Use source content instead of source path (fixes: #148) 2016-01-21 18:36:52 +08:00
Jeremy Cook
2f5e20d963 Fixed formatting in Changelog file. 2015-10-31 09:54:41 -04:00
Jeremy Cook
cc83a4e1fa Preparing for 1.1.3 release. 2015-10-31 09:48:10 -04:00
Jeremy Cook
dcc5cb0bc1 Merge pull request #146 from JCook21/issue134
Fix for issue134
2015-10-25 15:21:26 -04:00
Jeremy Cook
978416d1e4 Fixing diff problems by providing source block and checking for
directories in diff
2015-10-20 21:47:43 -04:00
Jeremy Cook
1c12c73e4b Merge branch 'rweng-named_castles' 2015-10-14 22:31:20 -04:00
Jeremy Cook
1016002638 Merge branch 'named_castles' of https://github.com/rweng/homesick into rweng-named_castles 2015-10-14 22:30:46 -04:00
Jeremy Cook
6431a864ad Merge pull request #145 from JCook21/master
Maintenance fixes
2015-10-13 22:43:07 -04:00
Jeremy Cook
42f661cfbf Update to use new travis container infrastructure (see
http://docs.travis-ci.com/user/migrating-from-legacy/?utm_source=legacy-notice&utm_medium=banner&utm_campaign=legacy-upgrade)
2015-10-12 19:30:29 -04:00
Jeremy Cook
7632591681 Coding standards fixes based off of Rubocop and minor edits to make
logic flow easier to understand.
2015-10-12 19:30:29 -04:00
Jeremy Cook
a9a5b81dc5 Adding system notifications to development gems. 2015-10-12 19:30:29 -04:00
Jeremy Cook
721c10cffd Merge pull request #143 from rweng/fix-open
Fix homesick open
2015-10-12 11:06:10 -04:00
Robin Wenglewski
332aad8ad0 change 'homesick open' to run '$EDITOR .' instead of '$EDITOR' in castle_dir #142 2015-10-12 16:39:52 +02:00
Robin Wenglewski
171b4c1fb8 add option to pass in destination to homesick clone 2015-10-11 18:21:22 +02:00
Jeremy Cook
60d4458bbc Merge pull request #124 from shioyama/clone_destination
Pass destination when cloning url.
2015-03-22 14:12:41 -04:00
Chris Salzberg
9ad171ab78 Pass destination when cloning url. 2015-03-05 22:26:41 +09:00
Jeremy Cook
5918746059 Merge pull request #137 from gerasiov/master
Thanks!
2015-02-23 23:00:22 -05:00
Alexander GQ Gerasiov
4641843ffd Add "requite 'pathname'" to lib/homesick/utils.rb
Since Pathname is used in lib/homesick/utils.rb, it should require this module
itself.

Signed-off-by: Alexander GQ Gerasiov <gq@cs.msu.su>
2015-02-22 23:09:18 +03:00
Jeremy Cook
1a181b907c Merge pull request #136 from williamboman/patch-1
Thanks for fixing this!
2015-02-20 09:19:15 -05:00
William Boman
fb7595d254 Escape message correctly on git_commit_all. 2015-02-19 16:51:53 +01:00
Jeremy Cook
c8f0999035 Preparing for new release. 2015-01-02 00:06:20 -05:00
Jeremy Cook
46faec7857 Bug fix to make sure git check works properly. 2015-01-01 21:21:27 -05:00
Jeremy Cook
e35d3fe6ba Merge pull request #128 from wireframe/force-rc 2014-12-13 08:12:04 -05:00
Jeremy Cook
ba620e0f7f Merge pull request #127 from JCook21/CheckGit
In lieu of other comments I'm going to go ahead and merge this.
2014-12-02 20:53:42 -05:00
Ryan Sonnek
5700f55dc3 Add --force option to homesick rc command
Support automatically eval-ing .homesickrc file without prompting for user input.
This is particularly useful for headless scripts that do not support
user input.
2014-12-01 13:38:43 -06:00
Jeremy Cook
2c92010093 Fix to make tests pass in Ruby 1.9.3 2014-11-25 21:18:13 -05:00
Jeremy Cook
03490531d8 Changed name of git check method to be more descriptive. 2014-11-24 08:49:17 -05:00
Jeremy Cook
7bd9759e81 Added tests for a minimumGit version of 1.8.0. 2014-11-23 22:22:44 -05:00
Jeremy Cook
a808f56caf Tightened up git checking to check for a minimum installed version of
Git.
2014-11-23 14:32:47 -05:00
Jeremy Cook
b7e2b45e69 Added simple implementation to check if git is installed before
executing commands.
2014-11-20 21:25:47 -05:00
Jeremy Cook
63c45d7c3a Removed unneeded config line. 2014-11-09 11:44:09 -05:00
Jeremy Cook
096067ac62 Merge pull request #125 from JCook21/master
Updated to use Rspec 3.1
2014-10-01 21:37:44 -04:00
Jeremy Cook
8d6bf4c0c5 Updated to use Rspec 3.1 2014-09-30 19:39:16 -04:00
Jeremy Cook
882b862780 Merge pull request #123 from JCook21/SymlinkedHomes
I'm merging this as it seems to be harmless and hasn't elicited any responses.
2014-09-27 17:37:21 -04:00
Jeremy Cook
e06a5d6300 Merge pull request #122 from JCook21/SmokeTest
Added basic smoke test
2014-09-19 14:38:44 -04:00
Jeremy Cook
7451e8c739 Bug fix to cover cases where homes are symlinked. 2014-09-19 14:36:32 -04:00
Jeremy Cook
f034f773c5 Added a smoke test to ensure that calling bin/homesick outputs some text 2014-09-19 14:19:09 -04:00
Jeremy Cook
681fd98dc3 Merge pull request #121 from PeterDaveHello/patch-1
Looks good to me, thanks.
2014-09-19 12:10:34 -04:00
Jeremy Cook
e57b139e32 Merge pull request #117 from JCook21/Shell
Merging as there doesn't seem to be any comments or issues with this.
2014-09-19 12:08:54 -04:00
Peter Dave Hello
b64bfe2bb6 Use svg instead of png to get better image quality 2014-09-08 15:42:54 +08:00
Jeremy Cook
ee04b5788a Removed the homesick shell module and folded its code in elsewhere. 2014-06-14 15:03:57 -04:00
Jeremy Cook
2e8d431ab5 Regenerate gemspec for version 1.1.1 2014-05-21 20:56:20 -04:00
Jeremy Cook
3465c37c0e Updated version to 1.1.1 2014-05-21 20:56:07 -04:00
Jeremy Cook
bf6894e313 Regenerate gemspec for version 1.1.0 2014-05-21 20:55:35 -04:00
Jeremy Cook
77e3f7f479 Merge pull request #116 from JCook21/master
Minimum Ruby Version
2014-05-21 20:54:15 -04:00
Jeremy Cook
753f5027b0 Added minimum ruby version to the gemspec. 2014-05-08 22:07:37 -04:00
Jeremy Cook
23c012a527 Regenerate gemspec for version 1.1.0 2014-04-28 20:48:43 -04:00
Jeremy Cook
895543641b Added details of 1.1.0 release. 2014-04-28 20:45:52 -04:00
Jeremy Cook
72bfc5a2fd Regenerate gemspec for version 1.1.0 2014-04-28 19:20:20 -04:00
Jeremy Cook
f5054cf41d Bumped version number. 2014-04-28 19:20:02 -04:00
Jeremy Cook
b60703d496 Bumped version number in preparation for a release. 2014-04-26 18:06:54 -04:00
Jeremy Cook
9a8788fb80 Removed uneeded config lines since the pretend and quiet options are set
in global config.
2014-04-26 18:04:34 -04:00
Jeremy Cook
1a44edcde1 Merge pull request #111 from JCook21/Pretend
Edits to keep code DRY
2014-04-26 17:45:08 -04:00
Nicolas McCurdy
383f2a9f32 Merge pull request #113 from JCook21/BugFix
Bug fix
2014-04-23 01:17:08 -04:00
Jeremy Cook
f55828f1d4 Fixing a bug that breaks the handling of collisions. 2014-04-22 20:54:05 -04:00
Jeremy Cook
d4f9633a0c Added ability for methods to be overrode, through the pretend and quiet
options, skipping their default behaviour if so.
2014-04-22 19:43:40 -04:00
Jeremy Cook
b5138bcdd1 Merge pull request #110 from nicolasmccurdy/refactor
Refactor
2014-04-22 12:46:49 -04:00
Nicolas McCurdy
d9ee74bf14 Move GitActions and FileActions into a new Actions module 2014-04-16 20:54:26 -04:00
Nicolas McCurdy
2148697864 Separate Actions into two new modules: FileActions and GitActions 2014-04-16 18:05:02 -04:00
Nicolas McCurdy
1c3403064e Don't refer to Homesick as a class 2014-04-16 17:57:00 -04:00
Nicolas McCurdy
03d87807e0 Use require instead of autoload (fix #108) 2014-04-16 17:48:36 -04:00
Nicolas McCurdy
f6c4e5e42e Require Thor in Homesick::Shell 2014-04-16 17:29:17 -04:00
Nicolas McCurdy
705a416d74 Extract the CLI into a new Homesick::CLI class, while making Homesick a module 2014-04-16 17:20:05 -04:00
Nicolas McCurdy
41f3f9d374 Merge pull request #107 from JCook21/Utils
Small refactoring to keep main homesick class smaller.
2014-04-16 13:38:56 -04:00
Jeremy Cook
74cfd29272 Small refactoring to keep main homesick class smaller. 2014-04-15 22:20:12 -04:00
Jeremy Cook
9a3268b7c3 Merge pull request #100 from nicolasmccurdy/use-coveralls
Use coveralls
2014-04-15 21:15:46 -04:00
Nicolas McCurdy
70c1666606 Add a Gitter badge to the readme 2014-04-13 17:20:28 -04:00
Jeremy Cook
53ac09a5e9 Merge pull request #101 from nicolasmccurdy/use-libraries
Use stdlib methods to replace most non-git shell calls. Looks good.
2014-04-13 17:17:30 -04:00
Jeremy Cook
c790e34b39 Added documentation for the exec and exec_all commands. 2014-04-13 14:58:54 -04:00
Nicolas McCurdy
8428ad1c9c Merge pull request #105 from JCook21/ExecCommands
Further work on exec command
2014-04-13 14:29:59 -04:00
Jeremy Cook
efea18327b Added options for exec command and a new exec_all command. 2014-04-13 10:59:40 -04:00
Nicolas McCurdy
84fb1d1462 Replace a system call in the spec helper 2014-04-06 02:01:15 -04:00
Nicolas McCurdy
5dc7b5068d Replace any system calls in lib that don't use git with calls to FileUtils 2014-04-06 02:01:15 -04:00
Nicolas McCurdy
ab603240e4 Merge pull request #97 from JCook21/exec
Added Exec command
2014-04-06 00:48:20 -04:00
Nicolas McCurdy
2e5c2ec018 Add coverage status to the readme 2014-04-05 23:44:27 -04:00
Nicolas McCurdy
349e75584f Switch from simplecov to coveralls for code coverage (in the cloud!!!) 2014-04-05 23:27:29 -04:00
Jeremy Cook
bea3a0b680 Added exec command to homesick. 2014-04-05 09:21:16 -04:00
Jeremy Cook
8b6bf92e9a Applied fixes that allow tests to pass again. 2014-04-05 09:18:56 -04:00
Nicolas McCurdy
133c3613e9 Merge pull request #96 from nicolasmccurdy/fix-simplecov
Fix simplecov
2014-04-05 00:47:31 -04:00
Nicolas McCurdy
ff2e5ee064 Merge pull request #95 from nicolasmccurdy/use-expect-syntax
Use expect syntax
2014-04-04 23:23:42 -04:00
Nicolas McCurdy
22aed48d4e Set up simplecov (it was in the Gemfile before, but it wasn't actually used) 2014-04-04 15:11:31 -04:00
Nicolas McCurdy
4c7e45a1d5 Use simplecov instead of rcov for all Rubies, since we have dropped 1.8 anyway 2014-04-04 15:08:36 -04:00
Nicolas McCurdy
ca41ae7f85 Only allow RSpec's new "expect" syntax 2014-04-04 02:10:19 -04:00
Nicolas McCurdy
fa61e7b10e Avoid using "should" in example descriptions 2014-04-04 01:53:23 -04:00
Nicolas McCurdy
8397dcacc5 Convert specs to RSpec 2.14.8 syntax with Transpec
This conversion is done by Transpec 1.10.4 with the following command:
    transpec

* 39 conversions
    from: obj.should
      to: expect(obj).to

* 22 conversions
    from: == expected
      to: eq(expected)

* 18 conversions
    from: obj.should_receive(:message)
      to: expect(obj).to receive(:message)

* 13 conversions
    from: obj.stub(:message)
      to: allow(obj).to receive(:message)

* 11 conversions
    from: obj.should_not
      to: expect(obj).not_to

* 5 conversions
    from: =~ /pattern/
      to: match(/pattern/)

* 2 conversions
    from: obj.should_not_receive(:message)
      to: expect(obj).not_to receive(:message)
2014-04-04 01:37:44 -04:00
Jeremy Cook
3f2d343161 Merge pull request #93 from JCook21/master
Made tests less reliant on file paths
2014-03-16 10:44:57 -04:00
Jeremy Cook
d91628f811 Made tests more generic since file paths may be different on various
systems.
2014-03-08 17:26:07 -05:00
Jeremy Cook
94bff3aa9d Small change to make symlink text a symbol in the map command. 2014-01-25 23:16:15 -05:00
Christian Bundy
7253bdd634 Merge pull request #82 from thenickperson/rubocop-fixes
Reduce Rubocop errors
2014-01-23 12:19:57 -08:00
Jeremy Cook
8c6a17404f Merge pull request #83 from thenickperson/remove-duplicate-methods
Remove duplicate methods rm and rm_link
2014-01-22 04:14:27 -08:00
Nicolas McCurdy
98edb54ca4 Merge remote-tracking branch 'upstream/master' into rubocop-fixes
Conflicts:
	lib/homesick.rb
2014-01-21 22:37:48 -05:00
Christian Bundy
9b780ffac6 Merge pull request #87 from JCook21/Consistent
Rename "symlink" command to "link" (with backward compatibility)
2014-01-21 16:17:12 -08:00
Jeremy Cook
59f6239ea0 Renamed symlink command to link. 2014-01-21 17:21:07 -05:00
Nicolas McCurdy
c2cb6081e1 Fix some more code style issues with block params being in the wrong place 2014-01-20 23:26:29 -05:00
Nicolas McCurdy
95943deb82 Revert changes that use "\" and disable the line length cop 2014-01-20 21:03:21 -05:00
Nicolas McCurdy
08a71f657f Fix a minor code style issue where "do" wasn't on the same line as its params 2014-01-20 16:03:18 -05:00
Nicolas McCurdy
2b48544e32 Merge pull request #81 from thenickperson/ruby-2.1
Test with Ruby 2.1.0 on Travis and add it to the supported Rubies
2014-01-20 12:56:20 -08:00
Nicolas McCurdy
f1191d4b3c Remove duplicate methods rm and rm_link 2014-01-16 02:44:04 -05:00
Nicolas McCurdy
8173429131 Ignore the remaining cyclomatic complexity issues 2014-01-16 00:31:00 -05:00
Nicolas McCurdy
82be04ad8a Fix rubocop issues for some recently merged code 2014-01-16 00:22:13 -05:00
Nicolas McCurdy
bb735763c6 Separate the action handling of ln_s into a new method to lower complexity 2014-01-15 23:48:56 -05:00
Nicolas McCurdy
0bbb82f9ba Wrap all lines of Ruby code to 79 characters (maximum) 2014-01-15 23:44:39 -05:00
Nicolas McCurdy
e42eff4e10 Refactor the ln_s method to decrease its cyclomatic complexity 2014-01-15 23:34:12 -05:00
Nicolas McCurdy
7e659f11fe Fix some broken spec expectations and move/rename a method 2014-01-15 23:29:40 -05:00
Nicolas McCurdy
c667cefd4c Refactor the clone method to decrease its cyclomatic complexity 2014-01-15 23:29:40 -05:00
Nicolas McCurdy
571c5799e9 Swap some if/else statements so the positive case is always first 2014-01-15 23:23:30 -05:00
Nicolas McCurdy
604a3b2a20 Add todo comments to the rubocop config 2014-01-15 23:23:30 -05:00
Nicolas McCurdy
7d36460851 Rename the todo config to .rubocop.yml, now that more rubocop issues are fixed 2014-01-15 23:23:30 -05:00
Nicolas McCurdy
8f634b9d07 Add brief documentation comments for Homesick and Homesick::Actions 2014-01-15 23:23:30 -05:00
Nicolas McCurdy
12244abb56 Fix a few more syntax-related rubocop issues manually 2014-01-15 23:23:29 -05:00
Nicolas McCurdy
fc2bbb1d6e Fix several rubocop issues with "rubocop -a" 2014-01-15 23:23:29 -05:00
Nicolas McCurdy
f03e7670cf Fix an issue reported by rubocop that wasn't ignored in the todo config
Issue text: Favor modifier if/unless usage when you have a single-line body.
2014-01-15 23:22:03 -05:00
Nicolas McCurdy
e202b7eae7 Add a rubocop todo-style config created by "--auto-gen-config" and inherit it 2014-01-15 23:22:03 -05:00
Nicolas McCurdy
3fcbd21104 Test with Ruby 2.1.0 on Travis and add it to the supported Rubies 2014-01-15 21:55:28 -05:00
Jeremy Cook
db0f604faf Regenerate gemspec for version 1.0.0 2014-01-15 20:24:36 -05:00
Jeremy Cook
e5a6e43333 Preparing for 1.0.0 release. 2014-01-15 20:23:25 -05:00
Jeremy Cook
faa5f0b9ed Merge pull request #79 from JCook21/Tests
Added tests for untested methods
2014-01-15 16:43:35 -08:00
Jeremy Cook
7c13727978 Merge branch 'master' into Tests 2014-01-15 19:41:06 -05:00
Jeremy Cook
2dba8b6496 Merge branch 'master' of github.com:technicalpickles/homesick 2014-01-15 19:38:29 -05:00
Jeremy Cook
2dadd4e064 Removed libnotify which seems to be causing build errors. 2014-01-15 19:38:02 -05:00
Jeremy Cook
a60ca62eba Merge pull request #71 from thenickperson/master
Merging this one in following discussions on #80.
2014-01-15 16:35:17 -08:00
Nicolas McCurdy
674ffb6bb2 Merge remote-tracking branch 'upstream/master' 2014-01-15 16:57:41 -05:00
Jeremy Cook
5fa0fc037c Updated README for .homesickrc. 2014-01-10 09:36:09 -05:00
Nicolas McCurdy
8a537b8204 Add gem version, dependency status, and code quality badges to the readme 2014-01-10 09:36:09 -05:00
Jeremy Cook
6e25f13e06 Added libnotify Gem on *nix systems in case people want to use it for
Guard notifications.
2014-01-10 09:36:09 -05:00
Jeremy Cook
df8f6b1cb0 Added command to display current version of homesick. 2014-01-10 09:36:09 -05:00
Jeremy Cook
6050a9a7ac Updated README for .homesickrc. 2014-01-09 22:38:47 -05:00
Jeremy Cook
0abd9436ad Merge branch 'master' of github.com:technicalpickles/homesick 2014-01-09 22:34:36 -05:00
Jeremy Cook
af159f5b97 Added libnotify Gem on *nix systems in case people want to use it for
Guard notifications.
2014-01-09 17:45:05 -05:00
Josh Nichols
a657c5622e Merge pull request #77 from thenickperson/readme-badges
Add gem version, dependency status, and code quality badges to the readme
2014-01-09 08:11:56 -08:00
Nicolas McCurdy
6f216cd916 Add gem version, dependency status, and code quality badges to the readme 2014-01-09 02:59:26 -05:00
Nicolas McCurdy
8bf1864335 Switch from the test-construct gem (deprecated) to test_construct 2014-01-09 02:49:52 -05:00
Nicolas McCurdy
8931739e97 Travis: Don't test on Ruby 1.8 (it's deprecated, and it breaks the build) 2014-01-09 02:49:52 -05:00
Jeremy Cook
ab46cf7b2f Merge branch 'master' of github.com:technicalpickles/homesick into Tests 2014-01-06 21:47:17 -05:00
Jeremy Cook
d115714a9f Merge branch 'master' of github.com:technicalpickles/homesick 2014-01-06 21:45:04 -05:00
Jeremy Cook
e787abd3f3 Merge pull request #78 from JCook21/Docs
Added docs for the rc command.
2014-01-06 18:43:06 -08:00
Jeremy Cook
1c0fe66944 Added docs for the rc command. 2014-01-06 21:42:24 -05:00
Jeremy Cook
148d18565f Added tests for untested methods. 2014-01-06 21:24:24 -05:00
Jeremy Cook
264d586863 Updated tests to remove shell output from test results. 2014-01-06 21:24:24 -05:00
Jeremy Cook
76bee65475 Updated gems to use test_construct, removing warning message. 2014-01-06 21:24:24 -05:00
Jeremy Cook
8dac49548c Added command to display current version of homesick. 2014-01-06 19:12:13 -05:00
Jeremy Cook
5c5d204d15 Removed notification settings from project so that user can set them in
~/.guard.rb instead.
2014-01-05 10:08:29 -05:00
43 changed files with 2864 additions and 1699 deletions

View File

@@ -1,5 +0,0 @@
README.rdoc
lib/**/*.rb
bin/*
features/**/*.feature
LICENSE

View File

@@ -0,0 +1,166 @@
name: Pull Request Validation
on:
pull_request:
types:
- opened
- synchronize
- reopened
jobs:
validate:
runs-on: ubuntu-latest
container: docker.io/catthehacker/ubuntu:act-latest
defaults:
run:
shell: bash
env:
ARTEFACT_BUCKET_NAME: ${{ vars.ARTEFACT_BUCKET_NAME }}
ARTEFACT_BUCKET_ENDPONT: ${{ vars.ARTEFACT_BUCKET_ENDPONT }}
ARTEFACT_BUCKET_REGION: ${{ vars.ARTEFACT_BUCKET_REGION }}
AWS_ACCESS_KEY_ID: ${{ secrets.ARTEFACT_BUCKET_WRITE_ACCESS_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.ARTEFACT_BUCKET_WRITE_ACCESS_SECRET }}
AWS_DEFAULT_REGION: ${{ vars.ARTEFACT_BUCKET_REGION }}
AWS_EC2_METADATA_DISABLED: true
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.26.1'
check-latest: true
cache: true
cache-dependency-path: go.sum
- name: Install AWS CLI v2
uses: ankurk91/install-aws-cli-action@v1
- name: Ensure tooling is available
run: |
set -euo pipefail
aws --version
if ! command -v jq >/dev/null 2>&1; then
apt-get update
apt-get install -y jq
fi
- name: Run full unit test suite with coverage
id: coverage
run: |
set -euo pipefail
go test -covermode=atomic -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
total="$(go tool cover -func=coverage.out | awk '/^total:/ {sub(/%/, "", $3); print $3}')"
printf '{\n "total": "%s"\n}\n' "$total" > coverage-summary.json
printf 'total=%s\n' "$total" >> "$GITHUB_OUTPUT"
- name: Generate coverage badge
env:
COVERAGE_TOTAL: ${{ steps.coverage.outputs.total }}
run: |
set -euo pipefail
color="$(awk -v total="$COVERAGE_TOTAL" 'BEGIN {
if (total >= 80) print "brightgreen";
else if (total >= 70) print "green";
else if (total >= 60) print "yellowgreen";
else if (total >= 50) print "yellow";
else print "red";
}')"
cat > coverage-badge.svg <<EOF
<svg xmlns="http://www.w3.org/2000/svg" width="126" height="20" role="img" aria-label="coverage: ${COVERAGE_TOTAL}%">
<linearGradient id="smooth" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="1" stop-opacity=".1"/>
</linearGradient>
<clipPath id="round">
<rect width="126" height="20" rx="3" fill="#fff"/>
</clipPath>
<g clip-path="url(#round)">
<rect width="63" height="20" fill="#555"/>
<rect x="63" width="63" height="20" fill="${color}"/>
<rect width="126" height="20" fill="url(#smooth)"/>
</g>
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">
<text x="32.5" y="15" fill="#010101" fill-opacity=".3">coverage</text>
<text x="32.5" y="14">coverage</text>
<text x="93.5" y="15" fill="#010101" fill-opacity=".3">${COVERAGE_TOTAL}%</text>
<text x="93.5" y="14">${COVERAGE_TOTAL}%</text>
</g>
</svg>
EOF
- name: Upload PR coverage artefacts
id: upload
run: |
set -euo pipefail
aws configure set default.s3.addressing_style path
repo_name="${GITHUB_REPOSITORY##*/}"
prefix="${repo_name}/pull-requests/${{ github.event.pull_request.number }}"
report_url="${ARTEFACT_BUCKET_ENDPONT%/}/${ARTEFACT_BUCKET_NAME}/${prefix}/coverage.html"
badge_url="${ARTEFACT_BUCKET_ENDPONT%/}/${ARTEFACT_BUCKET_NAME}/${prefix}/coverage-badge.svg"
aws --endpoint-url "${ARTEFACT_BUCKET_ENDPONT}" s3 cp coverage.html "s3://${ARTEFACT_BUCKET_NAME}/${prefix}/coverage.html" --content-type text/html
aws --endpoint-url "${ARTEFACT_BUCKET_ENDPONT}" s3 cp coverage-badge.svg "s3://${ARTEFACT_BUCKET_NAME}/${prefix}/coverage-badge.svg" --content-type image/svg+xml
aws --endpoint-url "${ARTEFACT_BUCKET_ENDPONT}" s3 cp coverage-summary.json "s3://${ARTEFACT_BUCKET_NAME}/${prefix}/coverage-summary.json" --content-type application/json
printf 'report_url=%s\n' "$report_url" >> "$GITHUB_OUTPUT"
printf 'badge_url=%s\n' "$badge_url" >> "$GITHUB_OUTPUT"
- name: Comment coverage report on pull request
env:
COVERAGE_BADGE_URL: ${{ steps.upload.outputs.badge_url }}
COVERAGE_REPORT_URL: ${{ steps.upload.outputs.report_url }}
COVERAGE_TOTAL: ${{ steps.coverage.outputs.total }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
marker='<!-- gosick-coverage-report -->'
api_base="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
payload="$(jq -n \
--arg marker "$marker" \
--arg total "$COVERAGE_TOTAL" \
--arg report "$COVERAGE_REPORT_URL" \
--arg badge "$COVERAGE_BADGE_URL" \
'{body: ($marker + "\n## Coverage Report\n\nCoverage total: **" + $total + "%**\n\n[HTML report](" + $report + ")\n\n![Coverage badge](" + $badge + ")")}')"
comments="$(curl -sS -H "Authorization: token ${GITHUB_TOKEN}" "${api_base}/issues/${{ github.event.pull_request.number }}/comments")"
comment_id="$(printf '%s' "$comments" | jq -r '.[] | select(.body | contains("<!-- gosick-coverage-report -->")) | .id' | tail -n 1)"
if [[ -n "$comment_id" ]]; then
curl -sS -X PATCH \
-H "Authorization: token ${GITHUB_TOKEN}" \
-H 'Content-Type: application/json' \
-d "$payload" \
"${api_base}/issues/comments/${comment_id}" >/dev/null
else
curl -sS -X POST \
-H "Authorization: token ${GITHUB_TOKEN}" \
-H 'Content-Type: application/json' \
-d "$payload" \
"${api_base}/issues/${{ github.event.pull_request.number }}/comments" >/dev/null
fi
- name: Add coverage summary
run: |
{
echo '## Coverage'
echo
echo '- Total: `${{ steps.coverage.outputs.total }}%`'
echo '- Report: ${{ steps.upload.outputs.report_url }}'
echo '- Badge: ${{ steps.upload.outputs.badge_url }}'
} >> "$GITHUB_STEP_SUMMARY"
- name: Run behavior suite
run: ./script/run-behavior-suite-docker.sh

View File

@@ -0,0 +1,123 @@
name: Push Validation
on:
push:
branches:
- "**"
tags-ignore:
- "*"
jobs:
validate:
runs-on: ubuntu-latest
container: docker.io/catthehacker/ubuntu:act-latest
defaults:
run:
shell: bash
env:
ARTEFACT_BUCKET_NAME: ${{ vars.ARTEFACT_BUCKET_NAME }}
ARTEFACT_BUCKET_ENDPONT: ${{ vars.ARTEFACT_BUCKET_ENDPONT }}
ARTEFACT_BUCKET_REGION: ${{ vars.ARTEFACT_BUCKET_REGION }}
AWS_ACCESS_KEY_ID: ${{ secrets.ARTEFACT_BUCKET_WRITE_ACCESS_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.ARTEFACT_BUCKET_WRITE_ACCESS_SECRET }}
AWS_DEFAULT_REGION: ${{ vars.ARTEFACT_BUCKET_REGION }}
AWS_EC2_METADATA_DISABLED: true
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.26.1'
check-latest: true
cache: true
cache-dependency-path: go.sum
- name: Install AWS CLI v2
uses: ankurk91/install-aws-cli-action@v1
- name: Verify AWS CLI
run: aws --version
- name: Run full unit test suite with coverage
id: coverage
run: |
set -euo pipefail
go test -covermode=atomic -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
total="$(go tool cover -func=coverage.out | awk '/^total:/ {sub(/%/, "", $3); print $3}')"
printf '{\n "total": "%s"\n}\n' "$total" > coverage-summary.json
printf 'total=%s\n' "$total" >> "$GITHUB_OUTPUT"
- name: Generate coverage badge
env:
COVERAGE_TOTAL: ${{ steps.coverage.outputs.total }}
run: |
set -euo pipefail
color="$(awk -v total="$COVERAGE_TOTAL" 'BEGIN {
if (total >= 80) print "brightgreen";
else if (total >= 70) print "green";
else if (total >= 60) print "yellowgreen";
else if (total >= 50) print "yellow";
else print "red";
}')"
cat > coverage-badge.svg <<EOF
<svg xmlns="http://www.w3.org/2000/svg" width="126" height="20" role="img" aria-label="coverage: ${COVERAGE_TOTAL}%">
<linearGradient id="smooth" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="1" stop-opacity=".1"/>
</linearGradient>
<clipPath id="round">
<rect width="126" height="20" rx="3" fill="#fff"/>
</clipPath>
<g clip-path="url(#round)">
<rect width="63" height="20" fill="#555"/>
<rect x="63" width="63" height="20" fill="${color}"/>
<rect width="126" height="20" fill="url(#smooth)"/>
</g>
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">
<text x="32.5" y="15" fill="#010101" fill-opacity=".3">coverage</text>
<text x="32.5" y="14">coverage</text>
<text x="93.5" y="15" fill="#010101" fill-opacity=".3">${COVERAGE_TOTAL}%</text>
<text x="93.5" y="14">${COVERAGE_TOTAL}%</text>
</g>
</svg>
EOF
- name: Upload branch coverage artefacts
id: upload
run: |
set -euo pipefail
aws configure set default.s3.addressing_style path
repo_name="${GITHUB_REPOSITORY##*/}"
prefix="${repo_name}/branch/${GITHUB_REF_NAME}"
report_url="${ARTEFACT_BUCKET_ENDPONT%/}/${ARTEFACT_BUCKET_NAME}/${prefix}/coverage.html"
badge_url="${ARTEFACT_BUCKET_ENDPONT%/}/${ARTEFACT_BUCKET_NAME}/${prefix}/coverage-badge.svg"
aws --endpoint-url "${ARTEFACT_BUCKET_ENDPONT}" s3 cp coverage.html "s3://${ARTEFACT_BUCKET_NAME}/${prefix}/coverage.html" --content-type text/html
aws --endpoint-url "${ARTEFACT_BUCKET_ENDPONT}" s3 cp coverage-badge.svg "s3://${ARTEFACT_BUCKET_NAME}/${prefix}/coverage-badge.svg" --content-type image/svg+xml
aws --endpoint-url "${ARTEFACT_BUCKET_ENDPONT}" s3 cp coverage-summary.json "s3://${ARTEFACT_BUCKET_NAME}/${prefix}/coverage-summary.json" --content-type application/json
printf 'report_url=%s\n' "$report_url" >> "$GITHUB_OUTPUT"
printf 'badge_url=%s\n' "$badge_url" >> "$GITHUB_OUTPUT"
- name: Add coverage summary
run: |
{
echo '## Coverage'
echo
echo '- Total: `${{ steps.coverage.outputs.total }}%`'
echo '- Report: ${{ steps.upload.outputs.report_url }}'
echo '- Badge: ${{ steps.upload.outputs.badge_url }}'
} >> "$GITHUB_STEP_SUMMARY"
- name: Run behavior suite on main pushes
if: ${{ github.ref == 'refs/heads/main' }}
run: ./script/run-behavior-suite-docker.sh

View File

@@ -0,0 +1,116 @@
name: Tag Build Artifacts
on:
push:
tags:
- "v*"
jobs:
build:
runs-on: ubuntu-latest
container: docker.io/catthehacker/ubuntu:act-latest
defaults:
run:
shell: bash
env:
RUNNER_TOOL_CACHE: /cache/tools
strategy:
fail-fast: false
matrix:
include:
- goos: linux
goarch: amd64
- goos: linux
goarch: arm64
- goos: darwin
goarch: amd64
- goos: darwin
goarch: arm64
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.26.1'
check-latest: true
cache: true
cache-dependency-path: go.sum
- name: Build binary
run: |
mkdir -p dist
GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} CGO_ENABLED=0 \
go build -o dist/gosick_${{ matrix.goos }}_${{ matrix.goarch }} ./cmd/homesick
- name: Package artifact
run: |
cd dist
tar -czf gosick_${{ matrix.goos }}_${{ matrix.goarch }}.tar.gz gosick_${{ matrix.goos }}_${{ matrix.goarch }}
- name: Publish workflow artifact
uses: actions/upload-artifact@v4
with:
name: gosick_${{ matrix.goos }}_${{ matrix.goarch }}
path: dist/gosick_${{ matrix.goos }}_${{ matrix.goarch }}.tar.gz
release:
runs-on: ubuntu-latest
needs: build
env:
RELEASE_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
path: dist
- name: Ensure jq is installed
run: |
if ! command -v jq >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get install -y jq
fi
- name: Create release if needed and upload assets
run: |
set -euo pipefail
if [[ -z "${RELEASE_TOKEN:-}" ]]; then
echo "RELEASE_TOKEN is empty. Expected secrets.GITHUB_TOKEN to be available." >&2
exit 1
fi
tag="${GITHUB_REF_NAME}"
api_base="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
release_json="$(curl -sS -H "Authorization: token ${RELEASE_TOKEN}" "${api_base}/releases/tags/${tag}" || true)"
release_id="$(printf '%s' "${release_json}" | jq -r '.id // empty')"
if [[ -z "${release_id}" ]]; then
create_payload="$(jq -n --arg tag "${tag}" --arg name "${tag}" '{tag_name:$tag, name:$name, draft:false, prerelease:false}')"
release_json="$(curl -sS -X POST \
-H "Authorization: token ${RELEASE_TOKEN}" \
-H "Content-Type: application/json" \
-d "${create_payload}" \
"${api_base}/releases")"
release_id="$(printf '%s' "${release_json}" | jq -r '.id // empty')"
fi
if [[ -z "${release_id}" ]]; then
echo "Unable to determine or create release id for tag ${tag}" >&2
printf '%s\n' "${release_json}" >&2
exit 1
fi
find dist -type f -name '*.tar.gz' -print0 | while IFS= read -r -d '' file; do
asset_name="$(basename "${file}")"
curl -sS -X POST \
-H "Authorization: token ${RELEASE_TOKEN}" \
-H "Content-Type: application/octet-stream" \
--data-binary @"${file}" \
"${api_base}/releases/${release_id}/assets?name=${asset_name}"
echo "Uploaded ${asset_name}"
done

25
.gitignore vendored
View File

@@ -1,19 +1,5 @@
# rcov generated
coverage
# rdoc generated # Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore:
rdoc
# yard generated
doc
.yardoc
# jeweler generated
pkg
.bundle
# Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore:
# #
# * Create a file at ~/.gitignore # * Create a file at ~/.gitignore
# * Include files you want ignored # * Include files you want ignored
@@ -44,5 +30,10 @@ pkg
.idea/ .idea/
*.iml *.iml
Gemfile.lock
vendor/ # Go scaffolding artifacts
dist/
*.test
*.out
.github/*

1
.rspec
View File

@@ -1 +0,0 @@
--color

View File

@@ -1,5 +0,0 @@
language: ruby
rvm:
- 2.0.0
- 1.9.3
- 1.8.7

View File

@@ -1,92 +0,0 @@
# 0.9.8
* Introduce new commands
* `homesick cd`
* `homesick open`
# 0.9.4
* Use https protocol instead of git protocol
* Introduce new commands
* `homesick unlink`
* `homesick rc`
# 0.9.3
* Add recursive option to `homesick clone`
# 0.9.2
* Set "dotfiles" as default castle name
* Introduce new commands
* `homesick show_path`
* `homesick status`
* `homesick diff`
# 0.9.1
* Fixed small bugs: #35, #40
# 0.9.0
* Introduce .homesick_subdir #39
# 0.8.1
*Fixed `homesick list` bug on ruby 2.0 #37
# 0.8.0
* Introduce commit & push command
* commit changes in castle and push to remote
* Enable recursive submodule update
* Git add when track
# 0.7.0
* Fixed double-cloning #14
* New option for pull command: --all
* pulls each castle, instead of just one
# 0.6.1
* Add a license
# 0.6.0
* Introduce .homesickrc
* Castles can now have a .homesickrc inside them
* On clone, this is eval'd inside the destination directory
* Introduce track command
* Allows easily moving an existing file into a castle, and symlinking it back
# 0.5.0
* Fixed listing of castles cloned using `homesick clone <github-user>/<github-repo>` (issue 3)
* Added `homesick pull <CASTLE>` for updating castles (thanks Jorge Dias!)
* Added a very basic `homesick generate <CASTLE>`
# 0.4.1
* Improved error message when a castle's home dir doesn't exist
# 0.4.0
* `homesick clone` can now take a path to a directory on the filesystem, which will be symlinked into place
* `homesick clone` now tries to `git submodule init` and `git submodule update` if git submodules are defined for a cloned repo
* Fixed missing dependency on thor and others
* Use HOME environment variable for where to store files, instead of assuming ~
# 0.3.0
* Renamed 'link' to 'symlink'
* Fixed conflict resolution when symlink destination exists and is a normal file
# 0.2.0
* Better support for recognizing git urls (thanks jacobat!)
* if it looks like a github user/repo, do that
* otherwise hand off to git clone
* Listing now displays in color, and show git remote
* Support pretend, force, and quiet modes
# 0.1.1
* Fixed trying to link against castles that don't exist
* Fixed linking, which tries to exclude . and .. from the list of files to
link (thanks Martinos!)
# 0.1.0
* Initial release

22
Gemfile
View File

@@ -1,22 +0,0 @@
source 'https://rubygems.org'
# Add dependencies required to use your gem here.
gem "thor", ">= 0.14.0"
# Add dependencies to develop your gem here.
# Include everything needed to run rake, tests, features, etc.
group :development do
gem "rake", ">= 0.8.7"
gem "rspec", "~> 2.10"
gem "guard"
gem "guard-rspec"
gem "rb-readline", "~> 0.5.0"
gem "jeweler", ">= 1.6.2"
gem "rcov", :platforms => :mri_18
gem "simplecov", :platforms => :mri_19
gem "test-construct"
gem "capture-output", "~> 1.0.0"
if RUBY_VERSION >= '1.9.2'
gem "rubocop"
end
end

View File

@@ -1,9 +0,0 @@
guard :rspec do
watch(%r{^spec/.+_spec\.rb$})
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
watch(%r{^lib/homesick/.*\.rb}) { "spec" }
watch('spec/spec_helper.rb') { "spec" }
notification :tmux, display_message: true
end

View File

@@ -1,155 +0,0 @@
# homesick
[![Build Status](https://travis-ci.org/technicalpickles/homesick.png?branch=master)](https://travis-ci.org/technicalpickles/homesick)
Your home directory is your castle. Don't leave your dotfiles behind.
Homesick is sorta like [rip](http://github.com/defunkt/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.
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
* 'home' contains any number of files and directories that begin with '.'
To get started, install homesick first:
gem install homesick
Next, you use the homesick command to clone a castle:
homesick clone git://github.com/technicalpickles/pickled-vim.git
Alternatively, if it's on github, there's a slightly shorter way:
homesick clone technicalpickles/pickled-vim
With the castle cloned, you can now link its contents into your home dir:
homesick symlink pickled-vim
You can remove symlinks anytime when you don't need them anymore
homesick unlink pickled-vim
If you're not sure what castles you have around, you can easily list them:
homesick list
To pull your castle (or all castles):
homesick pull --all|CASTLE
To commit your castle's changes:
homesick commit CASTLE
To push your castle:
homesick push CASTLE
To open a terminal in the root of a castle:
homesick cd CASTLE
To open your default editor in the root of a castle (the $EDITOR environment variable must be set):
homesick open CASTLE
Not sure what else homesick has up its sleeve? There's always the built in help:
homesick help
## .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.
For example, when you have castle like this:
castle/home
`-- .config
`-- fooapp
|-- config1
|-- config2
`-- config3
and have home like this:
$ tree -a
~
|-- .config
| `-- barapp
| |-- config1
| |-- config2
| `-- config3
`-- .emacs.d
|-- elisp
`-- inits
You may want to symlink only to `castle/home/.config/fooapp` instead of `castle/home/.config` because you already have `~/.config/barapp`. In this case, you can use .homesick_subdir. Please write "directories you want to look up sub directories (instead of just first depth)" in this file.
castle/.homesick_subdir
.config
and run `homesick symlink CASTLE`. The result is:
~
|-- .config
| |-- barapp
| | |-- config1
| | |-- config2
| | `-- config3
| `-- fooapp -> castle/home/.config/fooapp
`-- .emacs.d
|-- elisp
`-- inits
Or `homesick track NESTED_FILE CASTLE` adds a line automatically. For example:
homesick track .emacs.d/elisp castle
castle/.homesick_subdir
.config
.emacs.d
home directory
~
|-- .config
| |-- barapp
| | |-- config1
| | |-- config2
| | `-- config3
| `-- fooapp -> castle/home/.config/fooapp
`-- .emacs.d
|-- elisp -> castle/home/.emacs.d/elisp
`-- inits
and castle
castle/home
|-- .config
| `-- fooapp
| |-- config1
| |-- config2
| `-- config3
`-- .emacs.d
`-- elisp
## Note on Patches/Pull Requests
* Fork the project.
* 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.
* 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.
## Need homesick without the ruby dependency?
Check out [homeshick](https://github.com/andsens/homeshick).
## Copyright
Copyright (c) 2010 Joshua Nichols. See LICENSE for details.

79
README.md Normal file
View File

@@ -0,0 +1,79 @@
# homesick
[![Main Validation](https://git.hrafn.xyz/aether/gosick/actions/workflows/push-validation.yml?branch=main&event=push)](https://git.hrafn.xyz/aether/gosick/actions/workflows/push-validation.yml)
[![PR Validation](https://git.hrafn.xyz/aether/gosick/actions/workflows/pr-validation.yml?branch=main&event=pull_request)](https://git.hrafn.xyz/aether/gosick/actions/workflows/pr-validation.yml)
[![Tag Build Artifacts](https://git.hrafn.xyz/aether/gosick/actions/workflows/tag-build-artifacts.yml?event=push)](https://git.hrafn.xyz/aether/gosick/actions/workflows/tag-build-artifacts.yml)
[![Coverage](https://s3.hrafn.xyz/aether-workflow-report-artefacts/gosick/branch/main/coverage-badge.svg)](https://s3.hrafn.xyz/aether-workflow-report-artefacts/gosick/branch/main/coverage.html)
Your home directory is your castle. Don't leave your dotfiles behind.
This repository now contains a Go implementation of Homesick. A dotfiles repository is called a castle and should contain a `home/` directory with files to link into your `$HOME`.
## Build
Build with just:
```bash
just go-build
```
Or directly with Go:
```bash
go build -o dist/gosick ./cmd/homesick
```
## Commands
Implemented commands:
- `clone URI [CASTLE_NAME]`
- `list`
- `show_path [CASTLE]`
- `status [CASTLE]`
- `diff [CASTLE]`
- `link [CASTLE]`
- `unlink [CASTLE]`
- `track FILE [CASTLE]`
- `version`
Not yet implemented:
- `pull`
- `push`
- `commit`
- `destroy`
- `cd`
- `open`
- `exec`
- `exec_all`
- `rc`
- `generate`
## Behavior Suite
The repository includes a Docker-based behavior suite that validates filesystem and git outcomes for the implemented commands.
Run behavior suite:
```bash
just behavior
```
Verbose behavior suite output:
```bash
just behavior-verbose
```
## Testing
Run all Go tests:
```bash
just go-test
```
## License
See `LICENSE`.

View File

@@ -1,67 +0,0 @@
require 'rubygems'
require 'bundler'
begin
Bundler.setup(:default, :development)
rescue Bundler::BundlerError => e
$stderr.puts e.message
$stderr.puts "Run `bundle install` to install missing gems"
exit e.status_code
end
require 'rake'
require 'jeweler'
Jeweler::Tasks.new do |gem|
gem.name = "homesick"
gem.summary = %Q{Your home directory is your castle. Don't leave your dotfiles behind.}
gem.description = %Q{
Your home directory is your castle. Don't leave your dotfiles behind.
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.
}
gem.email = ["josh@technicalpickles.com", "info@muratayusuke.com"]
gem.homepage = "http://github.com/technicalpickles/homesick"
gem.authors = ["Joshua Nichols", "Yusuke Murata"]
gem.version = "0.9.7"
gem.license = "MIT"
# Have dependencies? Add them to Gemfile
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
end
Jeweler::GemcutterTasks.new
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec) do |spec|
spec.pattern = FileList['spec/**/*_spec.rb']
end
RSpec::Core::RakeTask.new(:rcov) do |spec|
spec.pattern = 'spec/**/*_spec.rb'
spec.rcov = true
end
task :rubocop do
if RUBY_VERSION >= '1.9.2'
system('rubocop')
end
end
task :test do
Rake::Task['spec'].execute
Rake::Task['rubocop'].execute
end
task :default => :test
require 'rdoc/task'
Rake::RDocTask.new do |rdoc|
version = File.exist?('VERSION') ? File.read('VERSION') : ""
rdoc.rdoc_dir = 'rdoc'
rdoc.title = "homesick #{version}"
rdoc.rdoc_files.include('README*')
rdoc.rdoc_files.include('lib/**/*.rb')
end

View File

@@ -1,9 +0,0 @@
#!/usr/bin/env ruby
require 'pathname'
lib = Pathname.new(__FILE__).dirname.join('..', 'lib').expand_path
$LOAD_PATH.unshift lib.to_s
require 'homesick'
Homesick.start

269
changelog.md Normal file
View File

@@ -0,0 +1,269 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- Native Go implementations for `clone`, `link`, `unlink`, and `track`.
- Containerized behavior test suite for command parity validation.
- Dedicated test suites for `list`, `show_path`, `status`, `diff`, and `version`.
- Just workflow support for building and running the Linux behavior binary.
- Coverage reports and badges published to shared object storage for branches and pull requests.
- Pull requests now receive coverage report links in CI comments.
### Changed
- CLI argument parsing migrated to Kong.
- Git operations for clone and track migrated to `go-git`.
- Build and behavior workflows now produce and run the `gosick` binary name.
- CI validation is unified into push events, running behavior tests only on `main` pushes.
- Gitea CI workflows now cache Go modules and build artifacts using a shared runner tool cache.
- Gitea workflow and README badge updated from `push-unit-tests` to `push-validation`.
- CLI help now uses the invoked binary name (defaulting to `gosick`) in usage output.
- CLI help description now reflects Homesick's purpose for managing precious dotfiles.
- Release notes standardized to Keep a Changelog format.
### Fixed
- `status` and `diff` now consistently write through configured app output writers.
### Removed
- Legacy Ruby implementation and Ruby toolchain.
## [1.1.6] - 2017-12-20
### Fixed
- Ensure `FileUtils` is imported correctly to avoid a potential error.
- Fix an issue where comparing a diff did not use the content of the new file.
### Changed
- Small documentation fixes.
## [1.1.5] - 2017-03-23
### Fixed
- Problem with version number being incorrect.
## [1.1.4] - 2017-03-22
### Fixed
- Ensure symlink conflicts are explicitly communicated to users and symlinks are not silently overwritten.
- Fix a problem in diff when asking a user to resolve a conflict.
### Changed
- Use real paths of symlinks when linking a castle into home.
- Code refactoring and fixes.
## [1.1.3] - 2015-10-31
### Added
- Allow a destination to be passed when cloning a castle.
### Fixed
- Make sure `homesick edit` opens the default editor in the root of the given castle.
- Bug when diffing edited files.
- Crashing bug when attempting to diff directories.
- Ensure that messages are escaped correctly on `git commit all`.
## [1.1.2] - 2015-01-02
### Added
- `--force` option to the rc command to bypass confirmation checks when running a `.homesickrc` file.
- Check to ensure that at least Git 1.8.0 is installed.
### Fixed
- Stop Homesick failing silently when Git is not installed.
### Changed
- Code refactoring and fixes.
## [1.1.0] - 2014-04-28
### Added
- `exec` and `exec_all` commands to run commands inside one or all cloned castles.
### Changed
- Code refactoring.
## [1.0.0] - 2014-01-15
### Added
- `version` command.
### Removed
- Support for Ruby 1.8.7.
## [0.9.8] - 2014-01-02
### Added
- `homesick cd` command.
- `homesick open` command.
## [0.9.4] - 2013-07-31
### Added
- `homesick unlink` command.
- `homesick rc` command.
### Changed
- Use HTTPS protocol instead of git protocol.
## [0.9.3] - 2013-07-07
### Added
- Recursive option to `homesick clone`.
## [0.9.2] - 2013-06-27
### Added
- `homesick show_path` command.
- `homesick status` command.
- `homesick diff` command.
### Changed
- Set `dotfiles` as default castle name.
## [0.9.1] - 2013-06-17
### Fixed
- Small bugs: #35, #40.
## [0.9.0] - 2013-06-06
### Added
- `.homesick_subdir` (#39).
## [0.8.1] - 2013-05-19
### Fixed
- `homesick list` bug on Ruby 2.0 (#37).
## [0.8.0] - 2013-04-06
### Added
- `commit` and `push` command.
- Commit changes in a castle and push to remote.
- Enable recursive submodule update.
- Git add when using track.
## [0.7.0] - 2012-05-28
### Added
- New option for pull command: `--all`.
- Pull each castle instead of just one.
### Fixed
- Double-cloning (#14).
## [0.6.1] - 2010-11-13
### Added
- License.
## [0.6.0] - 2010-10-27
### Added
- `.homesickrc` support.
- Castles can now have a `.homesickrc` inside them.
- On clone, this is eval'd inside the destination directory.
- `track` command.
- Allows easily moving an existing file into a castle and symlinking it back.
## [0.5.0] - 2010-05-18
### Added
- `homesick pull <CASTLE>` for updating castles (thanks Jorge Dias).
- A very basic `homesick generate <CASTLE>`.
### Fixed
- Listing of castles cloned using `homesick clone <github-user>/<github-repo>` (issue 3).
## [0.4.1] - 2010-04-02
### Fixed
- Improve error message when a castle's home dir does not exist.
## [0.4.0] - 2010-04-01
### Added
- `homesick clone` can take a path to a directory on the filesystem, which is symlinked into place.
- `homesick clone` tries to run `git submodule init` and `git submodule update` if git submodules are defined for a cloned repo.
### Changed
- Use `HOME` environment variable for where to store files, instead of assuming `~`.
### Fixed
- Missing dependency on thor and others.
## [0.3.0] - 2010-04-01
### Changed
- Rename `link` to `symlink`.
### Fixed
- Conflict resolution when symlink destination exists and is a normal file.
## [0.2.0] - 2010-03-19
### Added
- Better support for recognizing git URLs (thanks jacobat).
- If it looks like a GitHub user/repo, use that.
- Otherwise hand off to git clone.
- Listing now displays in color and shows git remote.
- Support pretend, force, and quiet modes.
## [0.1.1] - 2010-03-17
### Fixed
- Trying to link against castles that do not exist.
- Linking now excludes `.` and `..` from the list of files to link (thanks Martinos).
## [0.1.0] - 2010-03-10
### Added
- Initial release.

12
cmd/homesick/main.go Normal file
View 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)
}

View File

@@ -0,0 +1,21 @@
FROM golang:1.26-alpine AS builder
WORKDIR /workspace
COPY go.mod go.sum /workspace/
RUN go mod download
COPY . /workspace
RUN mkdir -p /workspace/dist && \
go build -o /workspace/dist/gosick ./cmd/homesick
FROM alpine:3.21
RUN apk add --no-cache \
bash \
ca-certificates \
git
WORKDIR /workspace
COPY . /workspace
COPY --from=builder /workspace/dist/gosick /workspace/dist/gosick
ENTRYPOINT ["/workspace/test/behavior/behavior_suite.sh"]

35
go.mod Normal file
View File

@@ -0,0 +1,35 @@
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/alecthomas/kong v1.12.1 // 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
)

104
go.sum Normal file
View File

@@ -0,0 +1,104 @@
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/alecthomas/kong v1.12.1 h1:iq6aMJDcFYP9uFrLdsiZQ2ZMmcshduyGv4Pek0MQPW0=
github.com/alecthomas/kong v1.12.1/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU=
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=

View File

@@ -1,78 +0,0 @@
# Generated by jeweler
# DO NOT EDIT THIS FILE DIRECTLY
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
# -*- encoding: utf-8 -*-
Gem::Specification.new do |s|
s.name = "homesick"
s.version = "0.9.8"
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
s.authors = ["Joshua Nichols", "Yusuke Murata"]
s.date = "2013-12-31"
s.description = "\n A man's home (directory) is his castle, so don't leave home with out it.\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.email = ["josh@technicalpickles.com", "info@muratayusuke.com"]
s.executables = ["homesick"]
s.extra_rdoc_files = [
"ChangeLog.markdown",
"LICENSE",
"README.markdown"
]
s.files = [
".document",
".rspec",
".travis.yml",
"ChangeLog.markdown",
"Gemfile",
"LICENSE",
"README.markdown",
"Rakefile",
"bin/homesick",
"homesick.gemspec",
"lib/homesick.rb",
"lib/homesick/actions.rb",
"lib/homesick/shell.rb",
"spec/homesick_spec.rb",
"spec/spec.opts",
"spec/spec_helper.rb"
]
s.homepage = "http://github.com/technicalpickles/homesick"
s.licenses = ["MIT"]
s.require_paths = ["lib"]
s.rubygems_version = "1.8.23"
s.summary = "A man's home is his castle. Never leave your dotfiles behind."
if s.respond_to? :specification_version then
s.specification_version = 3
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
s.add_runtime_dependency(%q<thor>, [">= 0.14.0"])
s.add_development_dependency(%q<rake>, [">= 0.8.7"])
s.add_development_dependency(%q<rspec>, ["~> 2.10"])
s.add_development_dependency(%q<jeweler>, [">= 1.6.2"])
s.add_development_dependency(%q<rcov>, [">= 0"])
s.add_development_dependency(%q<simplecov>, [">= 0"])
s.add_development_dependency(%q<test-construct>, [">= 0"])
s.add_development_dependency(%q<rubocop>, [">= 0"])
else
s.add_dependency(%q<thor>, [">= 0.14.0"])
s.add_dependency(%q<rake>, [">= 0.8.7"])
s.add_dependency(%q<rspec>, ["~> 2.10"])
s.add_dependency(%q<jeweler>, [">= 1.6.2"])
s.add_dependency(%q<rcov>, [">= 0"])
s.add_dependency(%q<simplecov>, [">= 0"])
s.add_dependency(%q<test-construct>, [">= 0"])
s.add_dependency(%q<rubocop>, [">= 0"])
end
else
s.add_dependency(%q<thor>, [">= 0.14.0"])
s.add_dependency(%q<rake>, [">= 0.8.7"])
s.add_dependency(%q<rspec>, ["~> 2.10"])
s.add_dependency(%q<jeweler>, [">= 1.6.2"])
s.add_dependency(%q<rcov>, [">= 0"])
s.add_dependency(%q<simplecov>, [">= 0"])
s.add_dependency(%q<test-construct>, [">= 0"])
s.add_dependency(%q<rubocop>, [">= 0"])
end
end

View File

@@ -0,0 +1,250 @@
package cli
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"git.hrafn.xyz/aether/gosick/internal/homesick/core"
"git.hrafn.xyz/aether/gosick/internal/homesick/version"
"github.com/alecthomas/kong"
)
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
}
parser, err := kong.New(
&cliModel{},
kong.Name(programName()),
kong.Description("Your home is your castle. Don't leave your precious dotfiles behind."),
kong.Writers(stdout, stderr),
kong.Exit(func(int) {}),
kong.ConfigureHelp(kong.HelpOptions{Compact: true}),
)
if err != nil {
_, _ = fmt.Fprintf(stderr, "error: %v\n", err)
return 1
}
normalizedArgs := normalizeArgs(args)
ctx, err := parser.Parse(normalizedArgs)
if err != nil {
var parseErr *kong.ParseError
if errors.As(err, &parseErr) {
if parseErr.ExitCode() == 0 || isHelpRequest(normalizedArgs) {
return 0
}
_, _ = fmt.Fprintf(stderr, "error: %v\n", err)
if parseErr.Context != nil {
_ = parseErr.Context.PrintUsage(false)
}
return parseErr.ExitCode()
}
_, _ = fmt.Fprintf(stderr, "error: %v\n", err)
return 1
}
if err := ctx.Run(app); err != nil {
var exitErr *cliExitError
if errors.As(err, &exitErr) {
_, _ = fmt.Fprintf(stderr, "error: %v\n", exitErr.err)
return exitErr.code
}
_, _ = fmt.Fprintf(stderr, "error: %v\n", err)
return 1
}
return 0
}
type cliModel struct {
Clone cloneCmd `cmd:"" help:"Clone a castle."`
List listCmd `cmd:"" help:"List castles."`
ShowPath showPathCmd `cmd:"" name:"show_path" help:"Show the path of a castle."`
Status statusCmd `cmd:"" help:"Show git status for a castle."`
Diff diffCmd `cmd:"" help:"Show git diff for a castle."`
Link linkCmd `cmd:"" help:"Symlink all dotfiles from a castle."`
Unlink unlinkCmd `cmd:"" help:"Unsymlink all dotfiles from a castle."`
Track trackCmd `cmd:"" help:"Track a file in a castle."`
Version versionCmd `cmd:"" help:"Display the current version."`
Pull pullCmd `cmd:"" help:"Pull the specified castle."`
Push pushCmd `cmd:"" help:"Push the specified castle."`
Commit commitCmd `cmd:"" help:"Commit the specified castle's changes."`
Destroy destroyCmd `cmd:"" help:"Destroy a castle."`
Cd cdCmd `cmd:"" help:"Print the path to a castle."`
Open openCmd `cmd:"" help:"Open a castle."`
Exec execCmd `cmd:"" help:"Execute a command in a castle."`
ExecAll execAllCmd `cmd:"" name:"exec_all" help:"Execute a command in every castle."`
Rc rcCmd `cmd:"" help:"Run rc hooks for a castle."`
Generate generateCmd `cmd:"" help:"Generate a castle."`
}
type cloneCmd struct {
URI string `arg:"" name:"URI" help:"Castle URI to clone."`
Destination string `arg:"" optional:"" name:"CASTLE_NAME" help:"Optional local castle name."`
}
func (c *cloneCmd) Run(app *core.App) error {
return app.Clone(c.URI, c.Destination)
}
type listCmd struct{}
func (c *listCmd) Run(app *core.App) error {
return app.List()
}
type showPathCmd struct {
Castle string `arg:"" optional:"" name:"CASTLE" help:"Castle name."`
}
func (c *showPathCmd) Run(app *core.App) error {
return app.ShowPath(defaultCastle(c.Castle))
}
type statusCmd struct {
Castle string `arg:"" optional:"" name:"CASTLE" help:"Castle name."`
}
func (c *statusCmd) Run(app *core.App) error {
return app.Status(defaultCastle(c.Castle))
}
type diffCmd struct {
Castle string `arg:"" optional:"" name:"CASTLE" help:"Castle name."`
}
func (c *diffCmd) Run(app *core.App) error {
return app.Diff(defaultCastle(c.Castle))
}
type linkCmd struct {
Castle string `arg:"" optional:"" name:"CASTLE" help:"Castle name."`
}
func (c *linkCmd) Run(app *core.App) error {
return app.LinkCastle(defaultCastle(c.Castle))
}
type unlinkCmd struct {
Castle string `arg:"" optional:"" name:"CASTLE" help:"Castle name."`
}
func (c *unlinkCmd) Run(app *core.App) error {
return app.Unlink(defaultCastle(c.Castle))
}
type trackCmd struct {
File string `arg:"" name:"FILE" help:"File to track."`
Castle string `arg:"" optional:"" name:"CASTLE" help:"Castle name."`
}
func (c *trackCmd) Run(app *core.App) error {
return app.Track(c.File, defaultCastle(c.Castle))
}
type versionCmd struct{}
func (c *versionCmd) Run(app *core.App) error {
return app.Version(version.String)
}
type pullCmd struct{}
type pushCmd struct{}
type commitCmd struct{}
type destroyCmd struct{}
type cdCmd struct{}
type openCmd struct{}
type execCmd struct{}
type execAllCmd struct{}
type rcCmd struct{}
type generateCmd struct{}
func (c *pullCmd) Run() error { return notImplemented("pull") }
func (c *pushCmd) Run() error { return notImplemented("push") }
func (c *commitCmd) Run() error { return notImplemented("commit") }
func (c *destroyCmd) Run() error { return notImplemented("destroy") }
func (c *cdCmd) Run() error { return notImplemented("cd") }
func (c *openCmd) Run() error { return notImplemented("open") }
func (c *execCmd) Run() error { return notImplemented("exec") }
func (c *execAllCmd) Run() error { return notImplemented("exec_all") }
func (c *rcCmd) Run() error { return notImplemented("rc") }
func (c *generateCmd) Run() error { return notImplemented("generate") }
func defaultCastle(castle string) string {
if strings.TrimSpace(castle) == "" {
return "dotfiles"
}
return castle
}
func programName() string {
if len(os.Args) > 0 {
if name := strings.TrimSpace(filepath.Base(os.Args[0])); name != "" {
return name
}
}
return "gosick"
}
func normalizeArgs(args []string) []string {
if len(args) == 0 {
return []string{"--help"}
}
switch args[0] {
case "-h", "--help":
return []string{"--help"}
case "help":
if len(args) == 1 {
return []string{"--help"}
}
return append(args[1:], "--help")
case "-v", "--version":
return []string{"version"}
default:
return args
}
}
func isHelpRequest(args []string) bool {
for _, arg := range args {
if arg == "-h" || arg == "--help" {
return true
}
}
return false
}
type cliExitError struct {
code int
err error
}
func (e *cliExitError) Error() string {
return e.err.Error()
}
func notImplemented(command string) error {
return &cliExitError{code: 2, err: fmt.Errorf("%s is not implemented in Go yet", command)}
}
func init() {
_ = os.Setenv("GIT_TERMINAL_PROMPT", "0")
}

View File

@@ -0,0 +1,76 @@
package cli_test
import (
"bytes"
"os"
"path/filepath"
"testing"
"git.hrafn.xyz/aether/gosick/internal/homesick/cli"
"git.hrafn.xyz/aether/gosick/internal/homesick/version"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
type CLISuite struct {
suite.Suite
homeDir string
stdout *bytes.Buffer
stderr *bytes.Buffer
}
func TestCLISuite(t *testing.T) {
suite.Run(t, new(CLISuite))
}
func (s *CLISuite) SetupTest() {
s.homeDir = filepath.Join(s.T().TempDir(), "home")
require.NoError(s.T(), os.MkdirAll(s.homeDir, 0o755))
require.NoError(s.T(), os.Setenv("HOME", s.homeDir))
s.stdout = &bytes.Buffer{}
s.stderr = &bytes.Buffer{}
}
func (s *CLISuite) TestRun_VersionAliases() {
for _, args := range [][]string{{"-v"}, {"--version"}, {"version"}} {
s.stdout.Reset()
s.stderr.Reset()
exitCode := cli.Run(args, s.stdout, s.stderr)
require.Equal(s.T(), 0, exitCode)
require.Equal(s.T(), version.String+"\n", s.stdout.String())
require.Empty(s.T(), s.stderr.String())
}
}
func (s *CLISuite) TestRun_ShowPath_DefaultCastle() {
exitCode := cli.Run([]string{"show_path"}, s.stdout, s.stderr)
require.Equal(s.T(), 0, exitCode)
require.Equal(s.T(), filepath.Join(s.homeDir, ".homesick", "repos", "dotfiles")+"\n", s.stdout.String())
require.Empty(s.T(), s.stderr.String())
}
func (s *CLISuite) TestRun_CloneSubcommandHelp() {
exitCode := cli.Run([]string{"clone", "--help"}, s.stdout, s.stderr)
require.Equal(s.T(), 0, exitCode)
require.Contains(s.T(), s.stdout.String(), "clone")
require.Contains(s.T(), s.stdout.String(), "URI")
require.Empty(s.T(), s.stderr.String())
}
func (s *CLISuite) TestRun_Help_UsesProgramNameAndDescription() {
originalArgs := os.Args
s.T().Cleanup(func() { os.Args = originalArgs })
os.Args = []string{"gosick"}
exitCode := cli.Run([]string{"--help"}, s.stdout, s.stderr)
require.Equal(s.T(), 0, exitCode)
require.Contains(s.T(), s.stdout.String(), "Usage: gosick")
require.NotContains(s.T(), s.stdout.String(), "Usage: homesick")
require.Contains(s.T(), s.stdout.String(), "precious dotfiles")
require.Empty(s.T(), s.stderr.String())
}

View 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)
}

View File

@@ -0,0 +1,555 @@
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 runGitWithIO(filepath.Join(a.ReposDir, castle), a.Stdout, a.Stderr, "status")
}
func (a *App) Diff(castle string) error {
return runGitWithIO(filepath.Join(a.ReposDir, castle), a.Stdout, a.Stderr, "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(filePath string, castle string) error {
return a.TrackPath(filePath, castle)
}
func (a *App) TrackPath(filePath string, castle string) error {
if strings.TrimSpace(castle) == "" {
castle = "dotfiles"
}
trimmedFile := strings.TrimSpace(filePath)
if trimmedFile == "" {
return errors.New("track requires FILE")
}
castleRoot := filepath.Join(a.ReposDir, castle)
castleHome := filepath.Join(castleRoot, "home")
info, err := os.Stat(castleHome)
if err != nil || !info.IsDir() {
return fmt.Errorf("could not track %s, expected %s to exist and contain dotfiles", castle, castleHome)
}
absolutePath, err := filepath.Abs(strings.TrimRight(trimmedFile, string(filepath.Separator)))
if err != nil {
return err
}
if _, err := os.Lstat(absolutePath); err != nil {
return err
}
relativeDir, err := filepath.Rel(a.HomeDir, filepath.Dir(absolutePath))
if err != nil {
return err
}
if relativeDir == ".." || strings.HasPrefix(relativeDir, ".."+string(filepath.Separator)) {
return fmt.Errorf("track requires file under %s", a.HomeDir)
}
castleTargetDir := filepath.Join(castleHome, relativeDir)
if relativeDir == "." {
castleTargetDir = castleHome
}
if err := os.MkdirAll(castleTargetDir, 0o755); err != nil {
return err
}
trackedPath := filepath.Join(castleTargetDir, filepath.Base(absolutePath))
if _, err := os.Lstat(trackedPath); err == nil {
return fmt.Errorf("%s already exists", trackedPath)
} else if !errors.Is(err, os.ErrNotExist) {
return err
}
if err := os.Rename(absolutePath, trackedPath); err != nil {
return err
}
subdirChanged := false
if relativeDir != "." {
subdirPath := filepath.Join(castleRoot, ".homesick_subdir")
subdirChanged, err = appendUniqueSubdir(subdirPath, relativeDir)
if err != nil {
return err
}
}
if err := a.linkPath(trackedPath, absolutePath); err != nil {
return err
}
repo, err := git.PlainOpen(castleRoot)
if err != nil {
return err
}
worktree, err := repo.Worktree()
if err != nil {
return err
}
trackedRelativePath := filepath.Join("home", relativeDir, filepath.Base(absolutePath))
if relativeDir == "." {
trackedRelativePath = filepath.Join("home", filepath.Base(absolutePath))
}
if _, err := worktree.Add(filepath.ToSlash(filepath.Clean(trackedRelativePath))); err != nil {
return err
}
if subdirChanged {
if _, err := worktree.Add(".homesick_subdir"); err != nil {
return err
}
}
return nil
}
func appendUniqueSubdir(path string, subdir string) (bool, error) {
existing, err := readSubdirs(path)
if err != nil {
return false, err
}
cleanSubdir := filepath.Clean(subdir)
for _, line := range existing {
if filepath.Clean(line) == cleanSubdir {
return false, nil
}
}
file, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
if err != nil {
return false, err
}
defer file.Close()
if _, err := file.WriteString(cleanSubdir + "\n"); err != nil {
return false, err
}
return true, nil
}
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 runGitWithIO(dir string, stdout io.Writer, stderr io.Writer, args ...string) error {
cmd := exec.Command("git", args...)
cmd.Dir = dir
cmd.Stdout = stdout
cmd.Stderr = 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/")
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
}

View 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)
}
})
}
}

View File

@@ -0,0 +1,76 @@
package core_test
import (
"bytes"
"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/plumbing/object"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
type DiffSuite struct {
suite.Suite
tmpDir string
homeDir string
reposDir string
stdout *bytes.Buffer
stderr *bytes.Buffer
app *core.App
}
func TestDiffSuite(t *testing.T) {
suite.Run(t, new(DiffSuite))
}
func (s *DiffSuite) 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.stdout = &bytes.Buffer{}
s.stderr = &bytes.Buffer{}
s.app = &core.App{
HomeDir: s.homeDir,
ReposDir: s.reposDir,
Stdout: s.stdout,
Stderr: s.stderr,
}
}
func (s *DiffSuite) createCastleRepo(castle string) string {
castleRoot := filepath.Join(s.reposDir, castle)
repo, err := git.PlainInit(castleRoot, false)
require.NoError(s.T(), err)
filePath := filepath.Join(castleRoot, "home", ".vimrc")
require.NoError(s.T(), os.MkdirAll(filepath.Dir(filePath), 0o755))
require.NoError(s.T(), os.WriteFile(filePath, []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)
return castleRoot
}
func (s *DiffSuite) TestDiff_WritesGitDiffToAppStdout() {
castleRoot := s.createCastleRepo("castle_repo")
require.NoError(s.T(), os.WriteFile(filepath.Join(castleRoot, "home", ".vimrc"), []byte("changed\n"), 0o644))
require.NoError(s.T(), s.app.Diff("castle_repo"))
require.Contains(s.T(), s.stdout.String(), "diff --git")
}

View 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)
}

View File

@@ -0,0 +1,72 @@
package core_test
import (
"bytes"
"io"
"os"
"path/filepath"
"testing"
"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/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
type ListSuite struct {
suite.Suite
tmpDir string
homeDir string
reposDir string
stdout *bytes.Buffer
app *core.App
}
func TestListSuite(t *testing.T) {
suite.Run(t, new(ListSuite))
}
func (s *ListSuite) 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.stdout = &bytes.Buffer{}
s.app = &core.App{
HomeDir: s.homeDir,
ReposDir: s.reposDir,
Stdout: s.stdout,
Stderr: io.Discard,
}
}
func (s *ListSuite) createCastleRepo(castle string, remoteURL string) {
castleRoot := filepath.Join(s.reposDir, filepath.FromSlash(castle))
require.NoError(s.T(), os.MkdirAll(castleRoot, 0o755))
repo, err := git.PlainInit(castleRoot, false)
require.NoError(s.T(), err)
if remoteURL != "" {
_, err = repo.CreateRemote(&config.RemoteConfig{Name: "origin", URLs: []string{remoteURL}})
require.NoError(s.T(), err)
}
}
func (s *ListSuite) TestList_OutputsSortedCastlesWithRemoteURLs() {
s.createCastleRepo("zomg", "git://github.com/technicalpickles/zomg.git")
s.createCastleRepo("wtf/zomg", "git://github.com/technicalpickles/wtf-zomg.git")
s.createCastleRepo("alpha", "git://github.com/technicalpickles/alpha.git")
require.NoError(s.T(), s.app.List())
require.Equal(
s.T(),
"alpha git://github.com/technicalpickles/alpha.git\n"+
"wtf/zomg git://github.com/technicalpickles/wtf-zomg.git\n"+
"zomg git://github.com/technicalpickles/zomg.git\n",
s.stdout.String(),
)
}

View File

@@ -0,0 +1,51 @@
package core_test
import (
"bytes"
"io"
"os"
"path/filepath"
"testing"
"git.hrafn.xyz/aether/gosick/internal/homesick/core"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
type ShowPathSuite struct {
suite.Suite
tmpDir string
homeDir string
reposDir string
stdout *bytes.Buffer
app *core.App
}
func TestShowPathSuite(t *testing.T) {
suite.Run(t, new(ShowPathSuite))
}
func (s *ShowPathSuite) 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.stdout = &bytes.Buffer{}
s.app = &core.App{
HomeDir: s.homeDir,
ReposDir: s.reposDir,
Stdout: s.stdout,
Stderr: io.Discard,
}
}
func (s *ShowPathSuite) TestShowPath_OutputsCastlePath() {
require.NoError(s.T(), s.app.ShowPath("castle_repo"))
require.Equal(
s.T(),
filepath.Join(s.reposDir, "castle_repo")+"\n",
s.stdout.String(),
)
}

View File

@@ -0,0 +1,79 @@
package core_test
import (
"bytes"
"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/plumbing/object"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
type StatusSuite struct {
suite.Suite
tmpDir string
homeDir string
reposDir string
stdout *bytes.Buffer
stderr *bytes.Buffer
app *core.App
}
func TestStatusSuite(t *testing.T) {
suite.Run(t, new(StatusSuite))
}
func (s *StatusSuite) 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.stdout = &bytes.Buffer{}
s.stderr = &bytes.Buffer{}
s.app = &core.App{
HomeDir: s.homeDir,
ReposDir: s.reposDir,
Stdout: s.stdout,
Stderr: s.stderr,
}
}
func (s *StatusSuite) createCastleRepo(castle string) string {
castleRoot := filepath.Join(s.reposDir, castle)
repo, err := git.PlainInit(castleRoot, false)
require.NoError(s.T(), err)
filePath := filepath.Join(castleRoot, "home", ".vimrc")
require.NoError(s.T(), os.MkdirAll(filepath.Dir(filePath), 0o755))
require.NoError(s.T(), os.WriteFile(filePath, []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)
return castleRoot
}
func (s *StatusSuite) TestStatus_WritesGitStatusToAppStdout() {
castleRoot := s.createCastleRepo("castle_repo")
require.NoError(s.T(), os.WriteFile(filepath.Join(castleRoot, "home", ".vimrc"), []byte("changed\n"), 0o644))
require.NoError(s.T(), s.app.Status("castle_repo"))
require.Contains(s.T(), s.stdout.String(), "modified:")
}
var _ io.Writer

View 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)
}

View 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"))
}

View File

@@ -0,0 +1,46 @@
package core_test
import (
"bytes"
"io"
"os"
"path/filepath"
"testing"
"git.hrafn.xyz/aether/gosick/internal/homesick/core"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
type VersionSuite struct {
suite.Suite
tmpDir string
homeDir string
reposDir string
stdout *bytes.Buffer
app *core.App
}
func TestVersionSuite(t *testing.T) {
suite.Run(t, new(VersionSuite))
}
func (s *VersionSuite) 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.stdout = &bytes.Buffer{}
s.app = &core.App{
HomeDir: s.homeDir,
ReposDir: s.reposDir,
Stdout: s.stdout,
Stderr: io.Discard,
}
}
func (s *VersionSuite) TestVersion_WritesVersionToAppStdout() {
require.NoError(s.T(), s.app.Version("1.2.3"))
require.Equal(s.T(), "1.2.3\n", s.stdout.String())
}

View File

@@ -0,0 +1,3 @@
package version
const String = "1.1.6"

21
justfile Normal file
View File

@@ -0,0 +1,21 @@
set shell := ["bash", "-eu", "-o", "pipefail", "-c"]
default:
@just --list
go-build:
@mkdir -p dist
go build -o dist/gosick ./cmd/homesick
go-build-linux:
@mkdir -p dist
GOOS=linux GOARCH="$(go env GOARCH)" go build -o dist/gosick ./cmd/homesick
go-test:
go test ./...
behavior:
./script/run-behavior-suite-docker.sh
behavior-verbose:
./script/run-behavior-suite-docker.sh --verbose

View File

@@ -1,440 +0,0 @@
# -*- encoding : utf-8 -*-
require 'thor'
class Homesick < Thor
autoload :Shell, 'homesick/shell'
autoload :Actions, 'homesick/actions'
include Thor::Actions
include Homesick::Actions
add_runtime_options!
GITHUB_NAME_REPO_PATTERN = /\A([A-Za-z0-9_-]+\/[A-Za-z0-9_-]+)\Z/
SUBDIR_FILENAME = '.homesick_subdir'
DEFAULT_CASTLE_NAME = 'dotfiles'
def initialize(args = [], options = {}, config = {})
super
self.shell = Homesick::Shell.new
end
desc 'clone URI', 'Clone +uri+ as a castle for homesick'
def clone(uri)
inside repos_dir do
destination = nil
if File.exist?(uri)
uri = Pathname.new(uri).expand_path
if uri.to_s.start_with?(repos_dir.to_s)
raise "Castle already cloned to #{uri}"
end
destination = uri.basename
ln_s uri, destination
elsif uri =~ GITHUB_NAME_REPO_PATTERN
destination = Pathname.new(uri).basename
git_clone "https://github.com/#{$1}.git", :destination => destination
elsif uri =~ /%r([^%r]*?)(\.git)?\Z/
destination = Pathname.new($1)
git_clone uri
elsif uri =~ /[^:]+:([^:]+)(\.git)?\Z/
destination = Pathname.new($1)
git_clone uri
else
raise "Unknown URI format: #{uri}"
end
if destination.join('.gitmodules').exist?
inside destination do
git_submodule_init
git_submodule_update
end
end
rc(destination)
end
end
desc 'rc CASTLE', 'Run the .homesickrc for the specified castle'
def rc(name = DEFAULT_CASTLE_NAME)
inside repos_dir do
destination = Pathname.new(name)
homesickrc = destination.join('.homesickrc').expand_path
if homesickrc.exist?
proceed = shell.yes?("#{name} has a .homesickrc. Proceed with evaling it? (This could be destructive)")
if proceed
shell.say_status 'eval', homesickrc
inside destination do
eval homesickrc.read, binding, homesickrc.expand_path.to_s
end
else
shell.say_status 'eval skip', "not evaling #{homesickrc}, #{destination} may need manual configuration", :blue
end
end
end
end
desc 'pull CASTLE', 'Update the specified castle'
method_option :all, :type => :boolean, :default => false, :required => false, :desc => 'Update all cloned castles'
def pull(name = DEFAULT_CASTLE_NAME)
if options[:all]
inside_each_castle do |castle|
shell.say castle.to_s.gsub(repos_dir.to_s + '/', '') + ':'
update_castle castle
end
else
update_castle name
end
end
desc 'commit CASTLE MESSAGE', "Commit the specified castle's changes"
def commit(name = DEFAULT_CASTLE_NAME, message = nil)
commit_castle name, message
end
desc 'push CASTLE', 'Push the specified castle'
def push(name = DEFAULT_CASTLE_NAME)
push_castle name
end
desc 'unlink CASTLE', 'Unsymlinks all dotfiles from the specified castle'
def unlink(name = DEFAULT_CASTLE_NAME)
check_castle_existance(name, 'symlink')
inside castle_dir(name) do
subdirs = subdirs(name)
# unlink files
unsymlink_each(name, castle_dir(name), subdirs)
# unlink files in subdirs
subdirs.each do |subdir|
unsymlink_each(name, subdir, subdirs)
end
end
end
desc 'symlink CASTLE', 'Symlinks all dotfiles from the specified castle'
method_option :force, :default => false, :desc => 'Overwrite existing conflicting symlinks without prompting.'
def symlink(name = DEFAULT_CASTLE_NAME)
check_castle_existance(name, 'symlink')
inside castle_dir(name) do
subdirs = subdirs(name)
# link files
symlink_each(name, castle_dir(name), subdirs)
# link files in subdirs
subdirs.each do |subdir|
symlink_each(name, subdir, subdirs)
end
end
end
desc 'track FILE CASTLE', 'add a file to a castle'
def track(file, castle = DEFAULT_CASTLE_NAME)
castle = Pathname.new(castle)
file = Pathname.new(file.chomp('/'))
check_castle_existance(castle, 'track')
absolute_path = file.expand_path
relative_dir = absolute_path.relative_path_from(home_dir).dirname
castle_path = Pathname.new(castle_dir(castle)).join(relative_dir)
FileUtils.mkdir_p castle_path
# Are we already tracking this or anything inside it?
target = Pathname.new(castle_path.join(file.basename))
if target.exist?
if absolute_path.directory?
move_dir_contents(target, absolute_path)
absolute_path.rmtree
subdir_remove(castle, relative_dir + file.basename)
elsif more_recent? absolute_path, target
target.delete
mv absolute_path, castle_path
else
shell.say_status(:track, "#{target} already exists, and is more recent than #{file}. Run 'homesick SYMLINK CASTLE' to create symlinks.", :blue) unless options[:quiet]
end
else
mv absolute_path, castle_path
end
inside home_dir do
absolute_path = castle_path + file.basename
home_path = home_dir + relative_dir + file.basename
ln_s absolute_path, home_path
end
inside castle_path do
git_add absolute_path
end
# are we tracking something nested? Add the parent dir to the manifest
subdir_add(castle, relative_dir) unless relative_dir.eql?(Pathname.new('.'))
end
desc 'list', 'List cloned castles'
def list
inside_each_castle do |castle|
say_status castle.relative_path_from(repos_dir).to_s, `git config remote.origin.url`.chomp, :cyan
end
end
desc 'status CASTLE', 'Shows the git status of a castle'
def status(castle = DEFAULT_CASTLE_NAME)
check_castle_existance(castle, 'status')
inside repos_dir.join(castle) do
git_status
end
end
desc 'diff CASTLE', 'Shows the git diff of uncommitted changes in a castle'
def diff(castle = DEFAULT_CASTLE_NAME)
check_castle_existance(castle, 'diff')
inside repos_dir.join(castle) do
git_diff
end
end
desc 'show_path CASTLE', 'Prints the path of a castle'
def show_path(castle = DEFAULT_CASTLE_NAME)
check_castle_existance(castle, 'show_path')
say repos_dir.join(castle)
end
desc 'generate PATH', 'generate a homesick-ready git repo at PATH'
def generate(castle)
castle = Pathname.new(castle).expand_path
github_user = `git config github.user`.chomp
github_user = nil if github_user == ''
github_repo = castle.basename
empty_directory castle
inside castle do
git_init
if github_user
url = "git@github.com:#{github_user}/#{github_repo}.git"
git_remote_add 'origin', url
end
empty_directory 'home'
end
end
desc "destroy CASTLE", "Delete all symlinks and remove the cloned repository"
def destroy(name)
check_castle_existance name, "destroy"
if shell.yes?("This will destroy your castle irreversible! Are you sure?")
unlink(name)
rm_rf repos_dir.join(name)
end
end
desc "cd CASTLE", "Open a new shell in the root of the given castle"
def cd(castle = DEFAULT_CASTLE_NAME)
check_castle_existance castle, "cd"
castle_dir = repos_dir.join(castle)
say_status "cd #{castle_dir.realpath}", "Opening a new shell in castle '#{castle}'. To return to the original one exit from the new shell.", :green
inside castle_dir do
system(ENV['SHELL'])
end
end
desc "open CASTLE", "Open your default editor in the root of the given castle"
def open(castle = DEFAULT_CASTLE_NAME)
if ! ENV['EDITOR']
say_status :error,"The $EDITOR environment variable must be set to use this command", :red
exit(1)
end
check_castle_existance castle, "open"
castle_dir = repos_dir.join(castle)
say_status "#{ENV['EDITOR']} #{castle_dir.realpath}", "Opening the root directory of castle '#{castle}' in editor '#{ENV['EDITOR']}'.", :green
inside castle_dir do
system(ENV['EDITOR'])
end
end
protected
def home_dir
@home_dir ||= Pathname.new(ENV['HOME'] || '~').expand_path
end
def repos_dir
@repos_dir ||= home_dir.join('.homesick', 'repos').expand_path
end
def castle_dir(name)
repos_dir.join(name, 'home')
end
def check_castle_existance(name, action)
unless castle_dir(name).exist?
say_status :error, "Could not #{action} #{name}, expected #{castle_dir(name)} exist and contain dotfiles", :red
exit(1)
end
end
def all_castles
dirs = Pathname.glob("#{repos_dir}/**/.git", File::FNM_DOTMATCH)
# reject paths that lie inside another castle, like git submodules
return dirs.reject do |dir|
dirs.any? do |other|
dir != other && dir.fnmatch(other.parent.join('*').to_s)
end
end
end
def inside_each_castle(&block)
all_castles.each do |git_dir|
castle = git_dir.dirname
Dir.chdir castle do # so we can call git config from the right contxt
yield castle
end
end
end
def update_castle(castle)
check_castle_existance(castle, 'pull')
inside repos_dir.join(castle) do
git_pull
git_submodule_init
git_submodule_update
end
end
def commit_castle(castle, message)
check_castle_existance(castle, 'commit')
inside repos_dir.join(castle) do
git_commit_all :message => message
end
end
def push_castle(castle)
check_castle_existance(castle, 'push')
inside repos_dir.join(castle) do
git_push
end
end
def subdir_file(castle)
repos_dir.join(castle, SUBDIR_FILENAME)
end
def subdirs(castle)
subdir_filepath = subdir_file(castle)
subdirs = []
if subdir_filepath.exist?
subdir_filepath.readlines.each do |subdir|
subdirs.push(subdir.chomp)
end
end
subdirs
end
def subdir_add(castle, path)
subdir_filepath = subdir_file(castle)
File.open(subdir_filepath, 'a+') do |subdir|
subdir.puts path unless subdir.readlines.reduce(false) do |memo, line|
line.eql?("#{path.to_s}\n") || memo
end
end
inside castle_dir(castle) do
git_add subdir_filepath
end
end
def subdir_remove(castle, path)
subdir_filepath = subdir_file(castle)
if subdir_filepath.exist?
lines = IO.readlines(subdir_filepath).delete_if { |line| line == "#{path}\n" }
File.open(subdir_filepath, 'w') { |manfile| manfile.puts lines }
end
inside castle_dir(castle) do
git_add subdir_filepath
end
end
def move_dir_contents(target, dir_path)
child_files = dir_path.children
child_files.each do |child|
target_path = target.join(child.basename)
if target_path.exist?
if more_recent?(child, target_path) && target.file?
target_path.delete
mv child, target
end
next
end
mv child, target
end
end
def more_recent?(first, second)
first_p = Pathname.new(first)
second_p = Pathname.new(second)
first_p.mtime > second_p.mtime && !first_p.symlink?
end
def each_file(castle, basedir, subdirs)
absolute_basedir = Pathname.new(basedir).expand_path
inside basedir do
files = Pathname.glob('{.*,*}').reject{ |a| ['.', '..'].include?(a.to_s) }
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)
each_file(castle, basedir, subdirs) do |absolute_path, home_path|
rm_link home_path
end
end
def symlink_each(castle, basedir, subdirs)
each_file(castle, basedir, subdirs) do |absolute_path, home_path|
ln_s absolute_path, home_path
end
end
end

View File

@@ -1,175 +0,0 @@
# -*- encoding : utf-8 -*-
class Homesick
module Actions
# TODO move this to be more like thor's template, empty_directory, etc
def git_clone(repo, config = {})
config ||= {}
destination = config[:destination] || File.basename(repo, '.git')
destination = Pathname.new(destination) unless destination.kind_of?(Pathname)
FileUtils.mkdir_p destination.dirname
if ! destination.directory?
say_status 'git clone', "#{repo} to #{destination.expand_path}", :green unless options[:quiet]
system "git clone -q --config push.default=upstream --recursive #{repo} #{destination}" unless options[:pretend]
else
say_status :exist, destination.expand_path, :blue unless options[:quiet]
end
end
def git_init(path = '.')
path = Pathname.new(path)
inside path do
if !path.join('.git').exist?
say_status 'git init', '' unless options[:quiet]
system 'git init >/dev/null' unless options[:pretend]
else
say_status 'git init', 'already initialized', :blue unless options[:quiet]
end
end
end
def git_remote_add(name, url)
existing_remote = `git config remote.#{name}.url`.chomp
existing_remote = nil if existing_remote == ''
if !existing_remote
say_status 'git remote', "add #{name} #{url}" unless options[:quiet]
system "git remote add #{name} #{url}" unless options[:pretend]
else
say_status 'git remote', "#{name} already exists", :blue unless options[:quiet]
end
end
def git_submodule_init(config = {})
say_status 'git submodule', 'init', :green unless options[:quiet]
system 'git submodule --quiet init' unless options[:pretend]
end
def git_submodule_update(config = {})
say_status 'git submodule', 'update', :green unless options[:quiet]
system 'git submodule --quiet update --init --recursive >/dev/null 2>&1' unless options[:pretend]
end
def git_pull(config = {})
say_status 'git pull', '', :green unless options[:quiet]
system 'git pull --quiet' unless options[:pretend]
end
def git_push(config = {})
say_status 'git push', '', :green unless options[:quiet]
system 'git push' unless options[:pretend]
end
def git_commit_all(config = {})
say_status 'git commit all', '', :green unless options[:quiet]
if config[:message]
system "git commit -a -m '#{config[:message]}'" unless options[:pretend]
else
system 'git commit -v -a' unless options[:pretend]
end
end
def git_add(file, config = {})
say_status 'git add file', '', :green unless options[:quiet]
system "git add '#{file}'" unless options[:pretend]
end
def git_status(config = {})
say_status 'git status', '', :green unless options[:quiet]
system "git status" unless options[:pretend]
end
def git_diff(config = {})
say_status 'git diff', '', :green unless options[:quiet]
system "git diff" unless options[:pretend]
end
def mv(source, destination, config = {})
source = Pathname.new(source)
destination = Pathname.new(destination + source.basename)
if destination.exist?
say_status :conflict, "#{destination} exists", :red unless options[:quiet]
if options[:force] || shell.file_collision(destination) { source }
system "mv '#{source}' '#{destination}'" unless options[:pretend]
end
else
# this needs some sort of message here.
system "mv '#{source}' '#{destination}'" unless options[:pretend]
end
end
def rm_link(target)
target = Pathname.new(target)
if target.symlink?
say_status :unlink, "#{target.expand_path}", :green unless options[:quiet]
FileUtils.rm_rf target
else
say_status :conflict, "#{target} is not a symlink", :red unless options[:quiet]
end
end
def rm(file)
say_status "rm #{file}", '', :green unless options[:quiet]
system "rm #{file}" if File.exists?(file)
end
def rm_rf(dir)
say_status "rm -rf #{dir}", '', :green unless options[:quiet]
system "rm -rf #{dir}"
end
def rm_link(target)
target = Pathname.new(target)
if target.symlink?
say_status :unlink, "#{target.expand_path}", :green unless options[:quiet]
FileUtils.rm_rf target
else
say_status :conflict, "#{target} is not a symlink", :red unless options[:quiet]
end
end
def rm(file)
say_status "rm #{file}", '', :green unless options[:quiet]
system "rm #{file}"
end
def rm_r(dir)
say_status "rm -r #{dir}", '', :green unless options[:quiet]
system "rm -r #{dir}"
end
def ln_s(source, destination, config = {})
source = Pathname.new(source)
destination = Pathname.new(destination)
FileUtils.mkdir_p destination.dirname
if destination.symlink?
if destination.readlink == source
say_status :identical, destination.expand_path, :blue unless options[:quiet]
else
say_status :conflict, "#{destination} exists and points to #{destination.readlink}", :red unless options[:quiet]
if options[:force] || shell.file_collision(destination) { source }
system "ln -nsf '#{source}' '#{destination}'" unless options[:pretend]
end
end
elsif destination.exist?
say_status :conflict, "#{destination} exists", :red unless options[:quiet]
if options[:force] || shell.file_collision(destination) { source }
system "rm -rf '#{destination}'" unless options[:pretend]
system "ln -sf '#{source}' '#{destination}'" unless options[:pretend]
end
else
say_status :symlink, "#{source.expand_path} to #{destination.expand_path}", :green unless options[:quiet]
system "ln -s '#{source}' '#{destination}'" unless options[:pretend]
end
end
end
end

View File

@@ -1,17 +0,0 @@
class Homesick
# Hack in support for diffing symlinks
class Shell < Thor::Shell::Color
def show_diff(destination, content)
destination = Pathname.new(destination)
if destination.symlink?
say "- #{destination.readlink}", :red, true
say "+ #{content.expand_path}", :green, true
else
super
end
end
end
end

View File

@@ -0,0 +1,57 @@
#!/usr/bin/env bash
set -euo pipefail
: "${HOMESICK_CMD:=/workspace/dist/gosick}"
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

View File

@@ -1,568 +0,0 @@
# -*- encoding : utf-8 -*-
require 'spec_helper'
require 'capture-output'
describe 'homesick' do
let(:home) { create_construct }
after { home.destroy! }
let(:castles) { home.directory('.homesick/repos') }
let(:homesick) { Homesick.new }
before { homesick.stub(:repos_dir).and_return(castles) }
describe 'clone' do
context 'has a .homesickrc' do
it 'should run the .homesickrc' do
somewhere = create_construct
local_repo = somewhere.directory('some_repo')
local_repo.file('.homesickrc') do |file|
file << "File.open(Dir.pwd + '/testing', 'w') { |f| f.print 'testing' }"
end
expect($stdout).to receive(:print)
expect($stdin).to receive(:gets).and_return('y')
homesick.clone local_repo
castles.join('some_repo').join('testing').should exist
end
end
context 'of a file' do
it 'should symlink existing directories' do
somewhere = create_construct
local_repo = somewhere.directory('wtf')
homesick.clone local_repo
castles.join('wtf').readlink.should == local_repo
end
context 'when it exists in a repo directory' do
before do
existing_castle = given_castle('existing_castle')
@existing_dir = existing_castle.parent
end
it 'should raise an error' do
homesick.should_not_receive(:git_clone)
expect { homesick.clone @existing_dir.to_s }.to raise_error(/already cloned/i)
end
end
end
it 'should clone git repo like file:///path/to.git' do
bare_repo = File.join(create_construct.to_s, 'dotfiles.git')
system "git init --bare #{bare_repo} >/dev/null 2>&1"
homesick.clone "file://#{bare_repo}"
File.directory?(File.join(home.to_s, '.homesick/repos/dotfiles')).should be_true
end
it 'should clone git repo like git://host/path/to.git' do
homesick.should_receive(:git_clone).with('git://github.com/technicalpickles/pickled-vim.git')
homesick.clone 'git://github.com/technicalpickles/pickled-vim.git'
end
it 'should clone git repo like git@host:path/to.git' do
homesick.should_receive(:git_clone).with('git@github.com:technicalpickles/pickled-vim.git')
homesick.clone 'git@github.com:technicalpickles/pickled-vim.git'
end
it 'should clone git repo like http://host/path/to.git' do
homesick.should_receive(:git_clone).with('http://github.com/technicalpickles/pickled-vim.git')
homesick.clone 'http://github.com/technicalpickles/pickled-vim.git'
end
it 'should clone git repo like http://host/path/to' do
homesick.should_receive(:git_clone).with('http://github.com/technicalpickles/pickled-vim')
homesick.clone 'http://github.com/technicalpickles/pickled-vim'
end
it 'should clone git repo like host-alias:repos.git' do
homesick.should_receive(:git_clone).with('gitolite:pickled-vim.git')
homesick.clone 'gitolite:pickled-vim.git'
end
it 'should throw an exception when trying to clone a malformed uri like malformed' do
homesick.should_not_receive(:git_clone)
expect { homesick.clone 'malformed' }.to raise_error
end
it 'should clone a github repo' do
homesick.should_receive(:git_clone).with('https://github.com/wfarr/dotfiles.git', :destination => Pathname.new('dotfiles'))
homesick.clone 'wfarr/dotfiles'
end
end
describe 'rc' do
let(:castle) { given_castle('glencairn') }
context 'when told to do so' do
before do
expect($stdout).to receive(:print)
expect($stdin).to receive(:gets).and_return('y')
end
it 'executes the .homesickrc' do
castle.file('.homesickrc') do |file|
file << "File.open(Dir.pwd + '/testing', 'w') { |f| f.print 'testing' }"
end
homesick.rc castle
castle.join('testing').should exist
end
end
context 'when told not to do so' do
before do
expect($stdout).to receive(:print)
expect($stdin).to receive(:gets).and_return('n')
end
it 'does not execute the .homesickrc' do
castle.file('.homesickrc') do |file|
file << "File.open(Dir.pwd + '/testing', 'w') { |f| f.print 'testing' }"
end
homesick.rc castle
castle.join('testing').should_not exist
end
end
end
describe 'symlink' do
let(:castle) { given_castle('glencairn') }
it 'links dotfiles from a castle to the home folder' do
dotfile = castle.file('.some_dotfile')
homesick.symlink('glencairn')
home.join('.some_dotfile').readlink.should == dotfile
end
it 'links non-dotfiles from a castle to the home folder' do
dotfile = castle.file('bin')
homesick.symlink('glencairn')
home.join('bin').readlink.should == dotfile
end
context 'when forced' do
let(:homesick) { Homesick.new [], :force => true }
it 'can override symlinks to directories' do
somewhere_else = create_construct
existing_dotdir_link = home.join('.vim')
FileUtils.ln_s somewhere_else, existing_dotdir_link
dotdir = castle.directory('.vim')
homesick.symlink('glencairn')
existing_dotdir_link.readlink.should == dotdir
end
it 'can override existing directory' do
existing_dotdir = home.directory('.vim')
dotdir = castle.directory('.vim')
homesick.symlink('glencairn')
existing_dotdir.readlink.should == dotdir
end
end
context "with '.config' in .homesick_subdir" do
let(:castle) { given_castle('glencairn', ['.config']) }
it 'can symlink in sub directory' do
dotdir = castle.directory('.config')
dotfile = dotdir.file('.some_dotfile')
homesick.symlink('glencairn')
home_dotdir = home.join('.config')
home_dotdir.symlink?.should be == false
home_dotdir.join('.some_dotfile').readlink.should == dotfile
end
end
context "with '.config/appA' in .homesick_subdir" do
let(:castle) { given_castle('glencairn', ['.config/appA']) }
it 'can symlink in nested sub directory' do
dotdir = castle.directory('.config').directory('appA')
dotfile = dotdir.file('.some_dotfile')
homesick.symlink('glencairn')
home_dotdir = home.join('.config').join('appA')
home_dotdir.symlink?.should be == false
home_dotdir.join('.some_dotfile').readlink.should == dotfile
end
end
context "with '.config' and '.config/someapp' in .homesick_subdir" do
let(:castle) { given_castle('glencairn', ['.config', '.config/someapp']) }
it 'can symlink under both of .config and .config/someapp' do
config_dir = castle.directory('.config')
config_dotfile = config_dir.file('.some_dotfile')
someapp_dir = config_dir.directory('someapp')
someapp_dotfile = someapp_dir.file('.some_appfile')
homesick.symlink('glencairn')
home_config_dir = home.join('.config')
home_someapp_dir = home_config_dir.join('someapp')
home_config_dir.symlink?.should be == false
home_config_dir.join('.some_dotfile').readlink.should be == config_dotfile
home_someapp_dir.symlink?.should be == false
home_someapp_dir.join('.some_appfile').readlink.should == someapp_dotfile
end
end
context "when call with no castle name" do
let(:castle) { given_castle('dotfiles') }
it 'using default castle name: "dotfiles"' do
dotfile = castle.file('.some_dotfile')
homesick.symlink
home.join('.some_dotfile').readlink.should == dotfile
end
end
end
describe 'unlink' do
let(:castle) { given_castle('glencairn') }
it 'unlinks dotfiles in the home folder' do
castle.file('.some_dotfile')
homesick.symlink('glencairn')
homesick.unlink('glencairn')
home.join('.some_dotfile').should_not exist
end
it 'unlinks non-dotfiles from the home folder' do
castle.file('bin')
homesick.symlink('glencairn')
homesick.unlink('glencairn')
home.join('bin').should_not exist
end
context "with '.config' in .homesick_subdir" do
let(:castle) { given_castle('glencairn', ['.config']) }
it 'can unlink sub directories' do
castle.directory('.config').file('.some_dotfile')
homesick.symlink('glencairn')
homesick.unlink('glencairn')
home_dotdir = home.join('.config')
home_dotdir.should exist
home_dotdir.join('.some_dotfile').should_not exist
end
end
context "with '.config/appA' in .homesick_subdir" do
let(:castle) { given_castle('glencairn', ['.config/appA']) }
it 'can unsymlink in nested sub directory' do
castle.directory('.config').directory('appA').file('.some_dotfile')
homesick.symlink('glencairn')
homesick.unlink('glencairn')
home_dotdir = home.join('.config').join('appA')
home_dotdir.should exist
home_dotdir.join('.some_dotfile').should_not exist
end
end
context "with '.config' and '.config/someapp' in .homesick_subdir" do
let(:castle) { given_castle('glencairn', ['.config', '.config/someapp']) }
it 'can unsymlink under both of .config and .config/someapp' do
config_dir = castle.directory('.config')
config_dir.file('.some_dotfile')
config_dir.directory('someapp').file('.some_appfile')
homesick.symlink('glencairn')
homesick.unlink('glencairn')
home_config_dir = home.join('.config')
home_someapp_dir = home_config_dir.join('someapp')
home_config_dir.should exist
home_config_dir.join('.some_dotfile').should_not exist
home_someapp_dir.should exist
home_someapp_dir.join('.some_appfile').should_not exist
end
end
context "when call with no castle name" do
let(:castle) { given_castle('dotfiles') }
it 'using default castle name: "dotfiles"' do
castle.file('.some_dotfile')
homesick.symlink
homesick.unlink
home.join('.some_dotfile').should_not exist
end
end
end
describe 'list' do
it 'should say each castle in the castle directory' do
given_castle('zomg')
given_castle('wtf/zomg')
homesick.should_receive(:say_status).with('zomg', 'git://github.com/technicalpickles/zomg.git', :cyan)
homesick.should_receive(:say_status).with('wtf/zomg', 'git://github.com/technicalpickles/zomg.git', :cyan)
homesick.list
end
end
describe 'status' do
it 'should say "nothing to commit" when there are no changes' do
given_castle('castle_repo')
text = Capture.stdout { homesick.status('castle_repo') }
text.should =~ /nothing to commit \(create\/copy files and use "git add" to track\)$/
end
it 'should say "Changes to be committed" when there are changes' do
given_castle('castle_repo')
some_rc_file = home.file '.some_rc_file'
homesick.track(some_rc_file.to_s, 'castle_repo')
text = Capture.stdout { homesick.status('castle_repo') }
text.should =~ /Changes to be committed:.*new file:\s*home\/.some_rc_file/m
end
end
describe 'diff' do
xit 'needs testing'
end
describe 'show_path' do
it 'should say the path of a castle' do
castle = given_castle('castle_repo')
homesick.should_receive(:say).with(castle.dirname)
homesick.show_path('castle_repo')
end
end
describe 'pull' do
xit 'needs testing'
describe '--all' do
xit 'needs testing'
end
end
describe 'push' do
xit 'needs testing'
end
describe 'track' do
it 'should move the tracked file into the castle' do
castle = given_castle('castle_repo')
some_rc_file = home.file '.some_rc_file'
homesick.track(some_rc_file.to_s, 'castle_repo')
tracked_file = castle.join('.some_rc_file')
tracked_file.should exist
some_rc_file.readlink.should == tracked_file
end
it 'should handle files with parens' do
castle = given_castle('castle_repo')
some_rc_file = home.file 'Default (Linux).sublime-keymap'
homesick.track(some_rc_file.to_s, 'castle_repo')
tracked_file = castle.join('Default (Linux).sublime-keymap')
tracked_file.should exist
some_rc_file.readlink.should == tracked_file
end
it 'should track a file in nested folder structure' do
castle = given_castle('castle_repo')
some_nested_file = home.file('some/nested/file.txt')
homesick.track(some_nested_file.to_s, 'castle_repo')
tracked_file = castle.join('some/nested/file.txt')
tracked_file.should exist
some_nested_file.readlink.should == tracked_file
end
it 'should track a nested directory' do
castle = given_castle('castle_repo')
some_nested_dir = home.directory('some/nested/directory/')
homesick.track(some_nested_dir.to_s, 'castle_repo')
tracked_file = castle.join('some/nested/directory/')
tracked_file.should exist
some_nested_dir.realpath.should == tracked_file.realpath
end
context "when call with no castle name" do
it 'using default castle name: "dotfiles"' do
castle = given_castle('dotfiles')
some_rc_file = home.file '.some_rc_file'
homesick.track(some_rc_file.to_s)
tracked_file = castle.join('.some_rc_file')
tracked_file.should exist
some_rc_file.readlink.should == tracked_file
end
end
describe 'commit' do
it 'should have a commit message when the commit succeeds' do
given_castle('castle_repo')
some_rc_file = home.file '.a_random_rc_file'
homesick.track(some_rc_file.to_s, 'castle_repo')
text = Capture.stdout { homesick.commit('castle_repo', 'Test message') }
text.should =~ /^\[master \(root-commit\) \w+\] Test message/
end
end
describe 'subdir_file' do
it 'should add the nested files parent to the subdir_file' do
castle = given_castle('castle_repo')
some_nested_file = home.file('some/nested/file.txt')
homesick.track(some_nested_file.to_s, 'castle_repo')
subdir_file = castle.parent.join(Homesick::SUBDIR_FILENAME)
File.open(subdir_file, 'r') do |f|
f.readline.should == "some/nested\n"
end
end
it 'should NOT add anything if the files parent is already listed' do
castle = given_castle('castle_repo')
some_nested_file = home.file('some/nested/file.txt')
other_nested_file = home.file('some/nested/other.txt')
homesick.track(some_nested_file.to_s, 'castle_repo')
homesick.track(other_nested_file.to_s, 'castle_repo')
subdir_file = castle.parent.join(Homesick::SUBDIR_FILENAME)
File.open(subdir_file, 'r') do |f|
f.readlines.size.should == 1
end
end
it 'should remove the parent of a tracked file from the subdir_file if the parent itself is tracked' do
castle = given_castle('castle_repo')
some_nested_file = home.file('some/nested/file.txt')
nested_parent = home.directory('some/nested/')
homesick.track(some_nested_file.to_s, 'castle_repo')
homesick.track(nested_parent.to_s, 'castle_repo')
subdir_file = castle.parent.join(Homesick::SUBDIR_FILENAME)
File.open(subdir_file, 'r') do |f|
f.each_line { |line| line.should_not == "some/nested\n" }
end
end
end
end
describe "destroy" do
it "removes the symlink files" do
expect_any_instance_of(Thor::Shell::Basic).to receive(:yes?).and_return('y')
given_castle("stronghold")
some_rc_file = home.file '.some_rc_file'
homesick.track(some_rc_file.to_s, "stronghold")
homesick.destroy('stronghold')
some_rc_file.should_not be_exist
end
it "deletes the cloned repository" do
expect_any_instance_of(Thor::Shell::Basic).to receive(:yes?).and_return('y')
castle = given_castle("stronghold")
some_rc_file = home.file '.some_rc_file'
homesick.track(some_rc_file.to_s, "stronghold")
homesick.destroy('stronghold')
castle.should_not be_exist
end
end
describe "cd" do
it "cd's to the root directory of the given castle" do
given_castle('castle_repo')
homesick.should_receive("inside").once.with(kind_of(Pathname)).and_yield
homesick.should_receive("system").once.with(ENV["SHELL"])
Capture.stdout { homesick.cd 'castle_repo' }
end
it "returns an error message when the given castle does not exist" do
homesick.should_receive("say_status").once.with(:error, /Could not cd castle_repo, expected \/tmp\/construct_container.* exist and contain dotfiles/, :red)
expect { homesick.cd "castle_repo" }.to raise_error(SystemExit)
end
end
describe "open" do
it "opens the system default editor in the root of the given castle" do
ENV.stub(:[]).and_call_original # Make sure calls to ENV use default values for most things...
ENV.stub(:[]).with('EDITOR').and_return('vim') # Set a default value for 'EDITOR' just in case none is set
given_castle 'castle_repo'
homesick.should_receive("inside").once.with(kind_of(Pathname)).and_yield
homesick.should_receive("system").once.with('vim')
Capture.stdout { homesick.open 'castle_repo' }
end
it "returns an error message when the $EDITOR environment variable is not set" do
ENV.stub(:[]).with('EDITOR').and_return(nil) # Set the default editor to make sure it fails.
homesick.should_receive("say_status").once.with(:error,"The $EDITOR environment variable must be set to use this command", :red)
expect { homesick.open "castle_repo" }.to raise_error(SystemExit)
end
it "returns an error message when the given castle does not exist" do
ENV.stub(:[]).with('EDITOR').and_return('vim') # Set a default just in case none is set
homesick.should_receive("say_status").once.with(:error, /Could not open castle_repo, expected \/tmp\/construct_container.* exist and contain dotfiles/, :red)
expect { homesick.open "castle_repo" }.to raise_error(SystemExit)
end
end
end

View File

@@ -1 +0,0 @@
--color

View File

@@ -1,38 +0,0 @@
$LOAD_PATH.unshift(File.dirname(__FILE__))
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
require 'homesick'
require 'rspec'
require 'rspec/autorun'
require 'construct'
require 'tempfile'
RSpec.configure do |config|
config.include Construct::Helpers
config.before { ENV['HOME'] = home.to_s }
config.before { silence! }
def silence!
homesick.stub(:say_status)
end
def given_castle(path, subdirs = [])
name = Pathname.new(path).basename
castles.directory(path) do |castle|
Dir.chdir(castle) do
system 'git init >/dev/null 2>&1'
system 'git config user.email "test@test.com"'
system 'git config user.name "Test Name"'
system "git remote add origin git://github.com/technicalpickles/#{name}.git >/dev/null 2>&1"
if subdirs
subdir_file = castle.join(Homesick::SUBDIR_FILENAME)
subdirs.each do |subdir|
system "echo #{subdir} >> #{subdir_file}"
end
end
return castle.directory('home')
end
end
end
end

193
test/behavior/behavior_suite.sh Executable file
View File

@@ -0,0 +1,193 @@
#!/usr/bin/env bash
set -euo pipefail
: "${HOMESICK_CMD:=/workspace/dist/gosick}"
: "${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