Compare commits
483 Commits
v0.7.0
...
8fc831dfdf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fc831dfdf | ||
|
|
7e32cd83c5 | ||
|
|
3d71433630 | ||
|
|
c6c382afce | ||
|
|
665401f2bd | ||
|
|
d084abd636 | ||
|
|
a6034ce470 | ||
|
|
484db0781b | ||
|
|
4a8ef7e1f6 | ||
|
|
b3f66e9e2e | ||
|
|
9d6dacb0f8 | ||
|
|
195b936de6 | ||
|
|
f6b5186f31 | ||
|
|
ea16ba8430 | ||
|
|
96ce572792 | ||
|
|
d638f201fe | ||
|
|
e09bdd78c2 | ||
|
|
0034a6f4e2 | ||
|
|
aa66695665 | ||
|
|
a7e4c501e4 | ||
|
|
0dfacc31d4 | ||
|
|
1d26594010 | ||
|
|
c10ff251d5 | ||
|
|
8d34674415 | ||
|
|
8174c6a983 | ||
|
|
1d4c088edc | ||
|
|
040bf31b56 | ||
|
|
4355e7fd9d | ||
|
|
b7c353553a | ||
|
|
2f45d28acb | ||
|
|
904c1be192 | ||
|
|
f443e96f9e | ||
|
|
0076588e1f | ||
|
|
919f033c8b | ||
|
|
dbc77a1b34 | ||
|
|
d02d118b28 | ||
|
|
a952c4f6bf | ||
|
|
e733dff818 | ||
|
|
41584dec6a | ||
|
|
005209703e | ||
|
|
ee4388b0f4 | ||
|
|
a44a514007 | ||
|
|
9431cb78af | ||
|
|
46c52769a6 | ||
|
|
fdb57cd846 | ||
|
|
ff387280d5 | ||
|
|
f09c62d922 | ||
|
|
dd7d52a25d | ||
|
|
f1630ece79 | ||
|
|
11ee8cdc0d | ||
|
|
ceb08cbe22 | ||
|
|
057e1cfc59 | ||
|
|
89f3000d8b | ||
|
|
36e3cb6bbf | ||
|
|
9ebae75e7d | ||
|
|
35e1909790 | ||
|
|
3b633ed326 | ||
|
|
fdf2da84dd | ||
|
|
e561566b46 | ||
|
|
dcef34c17d | ||
|
|
72d11c4a47 | ||
|
|
c2457bae9f | ||
|
|
001bd32bb3 | ||
|
|
7080321081 | ||
|
|
9d9cf66de6 | ||
|
|
9e9a940825 | ||
|
|
257e974c38 | ||
|
|
615e31428c | ||
|
|
8c2a1d0f84 | ||
|
|
62c934774b | ||
|
|
d3d6974b7b | ||
|
|
474d69da0b | ||
|
|
db6a513d1d | ||
|
|
ae343c4cab | ||
|
|
a2b365fb6f | ||
|
|
4cb2006f41 | ||
|
|
66347d307f | ||
|
|
8f92a1b4f0 | ||
|
|
7a2df591c0 | ||
|
|
4923265dea | ||
|
|
79421580e9 | ||
|
|
cabde9e5f1 | ||
|
|
0d60ae9d1a | ||
|
|
d5317b8e17 | ||
|
|
3b8a5b4be4 | ||
|
|
6590a1eeff | ||
|
|
693ae5f05e | ||
|
|
da3002f199 | ||
|
|
feaaab2fa4 | ||
|
|
59f75711a4 | ||
|
|
f24030b51f | ||
|
|
71bb120a12 | ||
|
|
85f46e01b1 | ||
|
|
c5b24b9b38 | ||
|
|
68460af45e | ||
|
|
5614b6b8b3 | ||
|
|
570b063632 | ||
|
|
1d398587d0 | ||
|
|
085853faaa | ||
|
|
21b4e344a9 | ||
|
|
a6194dfe8b | ||
|
|
5692194fa2 | ||
|
|
11745098c2 | ||
|
|
b1bb0c996c | ||
|
|
a62039da50 | ||
|
|
4bfd1c60c2 | ||
|
|
f0e11abb5b | ||
|
|
ed397bdaf8 | ||
|
|
2f5e20d963 | ||
|
|
cc83a4e1fa | ||
|
|
dcc5cb0bc1 | ||
|
|
978416d1e4 | ||
|
|
1c12c73e4b | ||
|
|
1016002638 | ||
|
|
6431a864ad | ||
|
|
42f661cfbf | ||
|
|
7632591681 | ||
|
|
a9a5b81dc5 | ||
|
|
721c10cffd | ||
|
|
332aad8ad0 | ||
|
|
171b4c1fb8 | ||
|
|
60d4458bbc | ||
|
|
9ad171ab78 | ||
|
|
5918746059 | ||
|
|
4641843ffd | ||
|
|
1a181b907c | ||
|
|
fb7595d254 | ||
|
|
c8f0999035 | ||
|
|
46faec7857 | ||
|
|
e35d3fe6ba | ||
|
|
ba620e0f7f | ||
|
|
5700f55dc3 | ||
|
|
2c92010093 | ||
|
|
03490531d8 | ||
|
|
7bd9759e81 | ||
|
|
a808f56caf | ||
|
|
b7e2b45e69 | ||
|
|
63c45d7c3a | ||
|
|
096067ac62 | ||
|
|
8d6bf4c0c5 | ||
|
|
882b862780 | ||
|
|
e06a5d6300 | ||
|
|
7451e8c739 | ||
|
|
f034f773c5 | ||
|
|
681fd98dc3 | ||
|
|
e57b139e32 | ||
|
|
b64bfe2bb6 | ||
|
|
ee04b5788a | ||
|
|
2e8d431ab5 | ||
|
|
3465c37c0e | ||
|
|
bf6894e313 | ||
|
|
77e3f7f479 | ||
|
|
753f5027b0 | ||
|
|
23c012a527 | ||
|
|
895543641b | ||
|
|
72bfc5a2fd | ||
|
|
f5054cf41d | ||
|
|
b60703d496 | ||
|
|
9a8788fb80 | ||
|
|
1a44edcde1 | ||
|
|
383f2a9f32 | ||
|
|
f55828f1d4 | ||
|
|
d4f9633a0c | ||
|
|
b5138bcdd1 | ||
|
|
d9ee74bf14 | ||
|
|
2148697864 | ||
|
|
1c3403064e | ||
|
|
03d87807e0 | ||
|
|
f6c4e5e42e | ||
|
|
705a416d74 | ||
|
|
41f3f9d374 | ||
|
|
74cfd29272 | ||
|
|
9a3268b7c3 | ||
|
|
70c1666606 | ||
|
|
53ac09a5e9 | ||
|
|
c790e34b39 | ||
|
|
8428ad1c9c | ||
|
|
efea18327b | ||
|
|
84fb1d1462 | ||
|
|
5dc7b5068d | ||
|
|
ab603240e4 | ||
|
|
2e5c2ec018 | ||
|
|
349e75584f | ||
|
|
bea3a0b680 | ||
|
|
8b6bf92e9a | ||
|
|
133c3613e9 | ||
|
|
ff2e5ee064 | ||
|
|
22aed48d4e | ||
|
|
4c7e45a1d5 | ||
|
|
ca41ae7f85 | ||
|
|
fa61e7b10e | ||
|
|
8397dcacc5 | ||
|
|
3f2d343161 | ||
|
|
d91628f811 | ||
|
|
94bff3aa9d | ||
|
|
7253bdd634 | ||
|
|
8c6a17404f | ||
|
|
98edb54ca4 | ||
|
|
9b780ffac6 | ||
|
|
59f6239ea0 | ||
|
|
c2cb6081e1 | ||
|
|
95943deb82 | ||
|
|
08a71f657f | ||
|
|
2b48544e32 | ||
|
|
f1191d4b3c | ||
|
|
8173429131 | ||
|
|
82be04ad8a | ||
|
|
bb735763c6 | ||
|
|
0bbb82f9ba | ||
|
|
e42eff4e10 | ||
|
|
7e659f11fe | ||
|
|
c667cefd4c | ||
|
|
571c5799e9 | ||
|
|
604a3b2a20 | ||
|
|
7d36460851 | ||
|
|
8f634b9d07 | ||
|
|
12244abb56 | ||
|
|
fc2bbb1d6e | ||
|
|
f03e7670cf | ||
|
|
e202b7eae7 | ||
|
|
3fcbd21104 | ||
|
|
db0f604faf | ||
|
|
e5a6e43333 | ||
|
|
faa5f0b9ed | ||
|
|
7c13727978 | ||
|
|
2dba8b6496 | ||
|
|
2dadd4e064 | ||
|
|
a60ca62eba | ||
|
|
674ffb6bb2 | ||
|
|
5fa0fc037c | ||
|
|
8a537b8204 | ||
|
|
6e25f13e06 | ||
|
|
df8f6b1cb0 | ||
|
|
6050a9a7ac | ||
|
|
0abd9436ad | ||
|
|
af159f5b97 | ||
|
|
a657c5622e | ||
|
|
6f216cd916 | ||
|
|
8bf1864335 | ||
|
|
8931739e97 | ||
|
|
ab46cf7b2f | ||
|
|
d115714a9f | ||
|
|
e787abd3f3 | ||
|
|
1c0fe66944 | ||
|
|
148d18565f | ||
|
|
264d586863 | ||
|
|
76bee65475 | ||
|
|
8dac49548c | ||
|
|
5c5d204d15 | ||
|
|
e1f85973c1 | ||
|
|
3554806741 | ||
|
|
e4cc308d43 | ||
|
|
78271a9ed4 | ||
|
|
8f67188c19 | ||
|
|
c432b27c92 | ||
|
|
30a3bbb198 | ||
|
|
e7f9358f96 | ||
|
|
750c7773ae | ||
|
|
900277f426 | ||
|
|
208adeef6c | ||
|
|
086828b12f | ||
|
|
c73d556e6f | ||
|
|
357e2f60f2 | ||
|
|
243ba70b33 | ||
|
|
640da07089 | ||
|
|
69c38774fe | ||
|
|
44527850f6 | ||
|
|
8d96b2c31f | ||
|
|
0019e8c61c | ||
|
|
545f5fc3e9 | ||
|
|
5108de20c3 | ||
|
|
9656be1dde | ||
|
|
09890e8048 | ||
|
|
b672b4c526 | ||
|
|
6ca49327c3 | ||
|
|
3d47cc44af | ||
|
|
e8b471ac97 | ||
|
|
0e6915b860 | ||
|
|
0d48e517f8 | ||
|
|
f2469ecaaf | ||
|
|
c8451c7d3f | ||
|
|
d3025a34ca | ||
|
|
17426583e0 | ||
|
|
04c4a4c059 | ||
|
|
6ae0aaa6f9 | ||
|
|
d22361f2ac | ||
|
|
e21e608cca | ||
|
|
238658cf69 | ||
|
|
2a361d86e0 | ||
|
|
294fb3d4ce | ||
|
|
342da7e250 | ||
|
|
9c52108035 | ||
|
|
5b954b93e3 | ||
|
|
b596e063f5 | ||
|
|
965b35b78c | ||
|
|
9551b3acb4 | ||
|
|
27ac1c7782 | ||
|
|
f9d0b69bce | ||
|
|
a91ce82d77 | ||
|
|
d8b5d8163b | ||
|
|
6fca06d341 | ||
|
|
44080829e3 | ||
|
|
651e028d5b | ||
|
|
6f3186df2f | ||
|
|
92c61f928e | ||
|
|
d3cb45f879 | ||
|
|
314e2932fb | ||
|
|
8ee5165ccf | ||
|
|
8f2a9e6703 | ||
|
|
f8a6fb9ce2 | ||
|
|
bf248cd645 | ||
|
|
1563814cb0 | ||
|
|
91770998a7 | ||
|
|
d7aca1025f | ||
|
|
b3298d18c8 | ||
|
|
359147e7e8 | ||
|
|
937f1912d7 | ||
|
|
18c0e32309 | ||
|
|
1518cb1155 | ||
|
|
c5b7dd2918 | ||
|
|
b668b7eda2 | ||
|
|
a595ead2c6 | ||
|
|
cc1ee544c3 | ||
|
|
a0862936e8 | ||
|
|
f91f5743b6 | ||
|
|
eb74b90b42 | ||
|
|
c3c108bd50 | ||
|
|
06975f79f5 | ||
|
|
830106a168 | ||
|
|
aa2dfcc42f | ||
|
|
ed71fd6227 | ||
|
|
fd60528567 | ||
|
|
01934d5b00 | ||
|
|
bba0e3ed7d | ||
|
|
7db0b13d30 | ||
|
|
b1c6c8f835 | ||
|
|
7620f40cb2 | ||
|
|
cf9058be04 | ||
|
|
234532ebef | ||
|
|
ccddbb1316 | ||
|
|
4ef315d4e2 | ||
|
|
9b7fe331f6 | ||
|
|
eeff0b40fb | ||
|
|
0c933c0085 | ||
|
|
a3d94fcca6 | ||
|
|
9fe1d190da | ||
|
|
73981c2e75 | ||
|
|
107dec388e | ||
|
|
1df44aea40 | ||
|
|
3bc623be7c | ||
|
|
123e6cf82d | ||
|
|
5e9d134021 | ||
|
|
0a022fddcc | ||
|
|
f18a4dc16f | ||
|
|
9ac754fd40 | ||
|
|
2667053fde | ||
|
|
8874994feb | ||
|
|
5a8b92f556 | ||
|
|
a65c2e6a1f | ||
|
|
2d0304feb1 | ||
|
|
34fec63234 | ||
|
|
1d5f27f567 | ||
|
|
093db8bdac | ||
|
|
04602efd6e | ||
|
|
3265de0c1d | ||
|
|
e4a428e0c5 | ||
|
|
6cc28450a4 | ||
|
|
a0c7fbacb7 | ||
|
|
b750094934 | ||
|
|
d953a964cd | ||
|
|
aa95ffac82 | ||
|
|
9c345828b0 | ||
|
|
59c137c653 | ||
|
|
3d405542af | ||
|
|
88ff4b85ce | ||
|
|
461ac5f226 | ||
|
|
a7d2d0a3f3 | ||
|
|
191ce11d8e | ||
|
|
0d28a3ef9b | ||
|
|
ac34249afe | ||
|
|
d1f87be435 | ||
|
|
cbb6117d69 | ||
|
|
334a1db262 | ||
|
|
e3bee69b27 | ||
|
|
0ff5325e3e | ||
|
|
40efb2f58a | ||
|
|
4b20c7224e | ||
|
|
8e06beced6 | ||
|
|
b043f2a5ed | ||
|
|
3d59bc7a97 | ||
|
|
75dcad8ea4 | ||
|
|
4b38eb848f | ||
|
|
bfce04e63c | ||
|
|
846c5c202b | ||
|
|
600811ff01 | ||
|
|
26ce289e9b | ||
|
|
10a9c0f482 | ||
|
|
498ffa27f9 | ||
|
|
ce8b46f300 | ||
|
|
995eff975f | ||
|
|
2ab35e91e2 | ||
|
|
4867ac2c7c | ||
|
|
a68149a87b | ||
|
|
8be3cdb6a0 | ||
|
|
99760c27af | ||
|
|
4aa76ce444 | ||
|
|
82ae128429 | ||
|
|
92dc611bb1 | ||
|
|
dbf333c971 | ||
|
|
ca5dc3a4cc | ||
|
|
a267a9c0b8 | ||
|
|
21cbb2c697 | ||
|
|
00f49be42c | ||
|
|
37b55bf934 | ||
|
|
f2aca02b82 | ||
|
|
dd101259f0 | ||
|
|
b1f2742422 | ||
|
|
114b44d4b6 | ||
|
|
e07f3f0658 | ||
|
|
b21aef09be | ||
|
|
d964e65a7e | ||
|
|
024856e538 | ||
|
|
e530df7239 | ||
|
|
e817c816c9 | ||
|
|
14f0f8c121 | ||
|
|
4c97948e04 | ||
|
|
360e8185f7 | ||
|
|
da0958d455 | ||
|
|
70f5d24e0a | ||
|
|
6b281ef001 | ||
|
|
3ddd3207b3 | ||
|
|
8e58a3f5e2 | ||
|
|
a95c4b2446 | ||
|
|
97fe1686f5 | ||
|
|
76fcf5d0b7 | ||
|
|
bf1fc58e10 | ||
|
|
3559d825ca | ||
|
|
2d54086d89 | ||
|
|
c31c67a3eb | ||
|
|
e924cbefda | ||
|
|
6867ef78dc | ||
|
|
a76d09d3f6 | ||
|
|
b93eea0e24 | ||
|
|
7332aa4acd | ||
|
|
49e4d2844b | ||
|
|
236373b7d7 | ||
|
|
21a9e4312d | ||
|
|
628d9bc0c1 | ||
|
|
caf5ca04f5 | ||
|
|
7cbbf2bdd7 | ||
|
|
9d6e77fd5a | ||
|
|
e7d251f8a1 | ||
|
|
af950d042a | ||
|
|
23ae908e7d | ||
|
|
38ffaca8cc | ||
|
|
b0bde0eb44 | ||
|
|
763cf8aa0a | ||
|
|
9f0d3e0f3c | ||
|
|
937bb65a14 | ||
|
|
376fd88fc9 | ||
|
|
fca23274bf | ||
|
|
27d038512c | ||
|
|
d8291edae0 | ||
|
|
e6c0ac91cd | ||
|
|
74713f8b7c | ||
|
|
38a43ba7ff | ||
|
|
ca832a38e2 | ||
|
|
54b2b9b339 | ||
|
|
fede78c337 | ||
|
|
8a41dca46d | ||
|
|
d084128297 | ||
|
|
a141f9cbbd | ||
|
|
e415da13e4 | ||
|
|
10d65abf47 | ||
|
|
9ced2921d9 | ||
|
|
fa99a89bbf | ||
|
|
f1a02b8afa | ||
|
|
6e4e60fc64 | ||
|
|
4f5e77d189 | ||
|
|
4fa7ce416b | ||
|
|
4d9f75b7b0 | ||
|
|
bfd83f2e87 | ||
|
|
38b40c0f50 |
166
.gitea/workflows/pr-validation.yml
Normal file
166
.gitea/workflows/pr-validation.yml
Normal 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")}')"
|
||||||
|
|
||||||
|
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
|
||||||
123
.gitea/workflows/push-validation.yml
Normal file
123
.gitea/workflows/push-validation.yml
Normal 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
|
||||||
116
.gitea/workflows/tag-build-artifacts.yml
Normal file
116
.gitea/workflows/tag-build-artifacts.yml
Normal 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
|
||||||
28
.gitignore
vendored
28
.gitignore
vendored
@@ -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
|
||||||
@@ -39,3 +25,15 @@ pkg
|
|||||||
#
|
#
|
||||||
# For vim:
|
# For vim:
|
||||||
*.swp
|
*.swp
|
||||||
|
#
|
||||||
|
# For IDEA:
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
|
||||||
|
# Go scaffolding artifacts
|
||||||
|
dist/
|
||||||
|
*.test
|
||||||
|
*.out
|
||||||
|
|
||||||
|
.github/*
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
# 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
|
|
||||||
15
Gemfile
15
Gemfile
@@ -1,15 +0,0 @@
|
|||||||
source :gemcutter
|
|
||||||
|
|
||||||
# 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"
|
|
||||||
gem "rspec", "~> 2.1.0"
|
|
||||||
gem "jeweler", ">= 1.6.2"
|
|
||||||
gem "rcov", ">= 0"
|
|
||||||
gem "test-construct"
|
|
||||||
gem "ruby-debug"
|
|
||||||
end
|
|
||||||
40
Gemfile.lock
40
Gemfile.lock
@@ -1,40 +0,0 @@
|
|||||||
GEM
|
|
||||||
remote: http://rubygems.org/
|
|
||||||
specs:
|
|
||||||
columnize (0.3.2)
|
|
||||||
diff-lcs (1.1.2)
|
|
||||||
git (1.2.5)
|
|
||||||
jeweler (1.6.2)
|
|
||||||
bundler (~> 1.0)
|
|
||||||
git (>= 1.2.5)
|
|
||||||
rake
|
|
||||||
linecache (0.43)
|
|
||||||
rake (0.9.2)
|
|
||||||
rcov (0.9.9)
|
|
||||||
rspec (2.1.0)
|
|
||||||
rspec-core (~> 2.1.0)
|
|
||||||
rspec-expectations (~> 2.1.0)
|
|
||||||
rspec-mocks (~> 2.1.0)
|
|
||||||
rspec-core (2.1.0)
|
|
||||||
rspec-expectations (2.1.0)
|
|
||||||
diff-lcs (~> 1.1.2)
|
|
||||||
rspec-mocks (2.1.0)
|
|
||||||
ruby-debug (0.10.4)
|
|
||||||
columnize (>= 0.1)
|
|
||||||
ruby-debug-base (~> 0.10.4.0)
|
|
||||||
ruby-debug-base (0.10.4)
|
|
||||||
linecache (>= 0.3)
|
|
||||||
test-construct (1.2.0)
|
|
||||||
thor (0.14.6)
|
|
||||||
|
|
||||||
PLATFORMS
|
|
||||||
ruby
|
|
||||||
|
|
||||||
DEPENDENCIES
|
|
||||||
jeweler (>= 1.6.2)
|
|
||||||
rake
|
|
||||||
rcov
|
|
||||||
rspec (~> 2.1.0)
|
|
||||||
ruby-debug
|
|
||||||
test-construct
|
|
||||||
thor (>= 0.14.0)
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
# homesick
|
|
||||||
|
|
||||||
A man's home (directory) is his castle, so don't leave home with out it.
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
If you use the shorthand syntax for GitHub repositories in your clone, please note you will instead need to run:
|
|
||||||
|
|
||||||
homesick symlink technicalpickles/pickled-vim
|
|
||||||
|
|
||||||
If you're not sure what castles you have around, you can easily list them:
|
|
||||||
|
|
||||||
homesick list
|
|
||||||
|
|
||||||
Not sure what else homesick has up its sleeve? There's always the built in help:
|
|
||||||
|
|
||||||
homesick help
|
|
||||||
|
|
||||||
## 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
79
README.md
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# homesick
|
||||||
|
|
||||||
|
[](https://git.hrafn.xyz/aether/gosick/actions/workflows/push-validation.yml)
|
||||||
|
[](https://git.hrafn.xyz/aether/gosick/actions/workflows/pr-validation.yml)
|
||||||
|
[](https://git.hrafn.xyz/aether/gosick/actions/workflows/tag-build-artifacts.yml)
|
||||||
|
[](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`.
|
||||||
56
Rakefile
56
Rakefile
@@ -1,56 +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{A man's home is his castle. Never leave your dotfiles behind.}
|
|
||||||
gem.description = %Q{
|
|
||||||
A man's home (directory) is his castle, so don't leave home with out it.
|
|
||||||
|
|
||||||
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"
|
|
||||||
gem.homepage = "http://github.com/technicalpickles/homesick"
|
|
||||||
gem.authors = ["Joshua Nichols"]
|
|
||||||
gem.version = "0.7.0"
|
|
||||||
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 :default => :spec
|
|
||||||
|
|
||||||
require 'rake/rdoctask'
|
|
||||||
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
|
|
||||||
|
|
||||||
@@ -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
269
changelog.md
Normal 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
12
cmd/homesick/main.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.hrafn.xyz/aether/gosick/internal/homesick/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
exitCode := cli.Run(os.Args[1:], os.Stdout, os.Stderr)
|
||||||
|
os.Exit(exitCode)
|
||||||
|
}
|
||||||
21
docker/behavior/Dockerfile
Normal file
21
docker/behavior/Dockerfile
Normal 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
35
go.mod
Normal 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
104
go.sum
Normal 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=
|
||||||
@@ -1,75 +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.7.0"
|
|
||||||
|
|
||||||
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
|
||||||
s.authors = ["Joshua Nichols"]
|
|
||||||
s.date = "2012-05-28"
|
|
||||||
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"
|
|
||||||
s.executables = ["homesick"]
|
|
||||||
s.extra_rdoc_files = [
|
|
||||||
"ChangeLog.markdown",
|
|
||||||
"LICENSE",
|
|
||||||
"README.markdown"
|
|
||||||
]
|
|
||||||
s.files = [
|
|
||||||
".document",
|
|
||||||
".rspec",
|
|
||||||
"ChangeLog.markdown",
|
|
||||||
"Gemfile",
|
|
||||||
"Gemfile.lock",
|
|
||||||
"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.15"
|
|
||||||
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"])
|
|
||||||
s.add_development_dependency(%q<rspec>, ["~> 2.1.0"])
|
|
||||||
s.add_development_dependency(%q<jeweler>, [">= 1.6.2"])
|
|
||||||
s.add_development_dependency(%q<rcov>, [">= 0"])
|
|
||||||
s.add_development_dependency(%q<test-construct>, [">= 0"])
|
|
||||||
s.add_development_dependency(%q<ruby-debug>, [">= 0"])
|
|
||||||
else
|
|
||||||
s.add_dependency(%q<thor>, [">= 0.14.0"])
|
|
||||||
s.add_dependency(%q<rake>, [">= 0"])
|
|
||||||
s.add_dependency(%q<rspec>, ["~> 2.1.0"])
|
|
||||||
s.add_dependency(%q<jeweler>, [">= 1.6.2"])
|
|
||||||
s.add_dependency(%q<rcov>, [">= 0"])
|
|
||||||
s.add_dependency(%q<test-construct>, [">= 0"])
|
|
||||||
s.add_dependency(%q<ruby-debug>, [">= 0"])
|
|
||||||
end
|
|
||||||
else
|
|
||||||
s.add_dependency(%q<thor>, [">= 0.14.0"])
|
|
||||||
s.add_dependency(%q<rake>, [">= 0"])
|
|
||||||
s.add_dependency(%q<rspec>, ["~> 2.1.0"])
|
|
||||||
s.add_dependency(%q<jeweler>, [">= 1.6.2"])
|
|
||||||
s.add_dependency(%q<rcov>, [">= 0"])
|
|
||||||
s.add_dependency(%q<test-construct>, [">= 0"])
|
|
||||||
s.add_dependency(%q<ruby-debug>, [">= 0"])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
250
internal/homesick/cli/cli.go
Normal file
250
internal/homesick/cli/cli.go
Normal 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")
|
||||||
|
}
|
||||||
76
internal/homesick/cli/cli_test.go
Normal file
76
internal/homesick/cli/cli_test.go
Normal 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())
|
||||||
|
}
|
||||||
103
internal/homesick/core/clone_test.go
Normal file
103
internal/homesick/core/clone_test.go
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package core_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.hrafn.xyz/aether/gosick/internal/homesick/core"
|
||||||
|
git "github.com/go-git/go-git/v5"
|
||||||
|
"github.com/go-git/go-git/v5/config"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing/object"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CloneSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
tmpDir string
|
||||||
|
homeDir string
|
||||||
|
reposDir string
|
||||||
|
app *core.App
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCloneSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(CloneSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CloneSuite) SetupTest() {
|
||||||
|
s.tmpDir = s.T().TempDir()
|
||||||
|
s.homeDir = filepath.Join(s.tmpDir, "home")
|
||||||
|
s.reposDir = filepath.Join(s.homeDir, ".homesick", "repos")
|
||||||
|
require.NoError(s.T(), os.MkdirAll(s.reposDir, 0o755))
|
||||||
|
|
||||||
|
s.app = &core.App{
|
||||||
|
HomeDir: s.homeDir,
|
||||||
|
ReposDir: s.reposDir,
|
||||||
|
Stdout: io.Discard,
|
||||||
|
Stderr: io.Discard,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CloneSuite) createBareRemote(name string) string {
|
||||||
|
remotePath := filepath.Join(s.tmpDir, name+".git")
|
||||||
|
_, err := git.PlainInit(remotePath, true)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
|
||||||
|
workPath := filepath.Join(s.tmpDir, name+"-work")
|
||||||
|
repo, err := git.PlainInit(workPath, false)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
|
||||||
|
castleFile := filepath.Join(workPath, "home", ".vimrc")
|
||||||
|
require.NoError(s.T(), os.MkdirAll(filepath.Dir(castleFile), 0o755))
|
||||||
|
require.NoError(s.T(), os.WriteFile(castleFile, []byte("set number\n"), 0o644))
|
||||||
|
|
||||||
|
wt, err := repo.Worktree()
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
_, err = wt.Add("home/.vimrc")
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
|
||||||
|
_, err = wt.Commit("initial", &git.CommitOptions{Author: &object.Signature{
|
||||||
|
Name: "Behavior Test",
|
||||||
|
Email: "behavior@test.local",
|
||||||
|
When: time.Now(),
|
||||||
|
}})
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
|
||||||
|
_, err = repo.CreateRemote(&config.RemoteConfig{Name: "origin", URLs: []string{remotePath}})
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
require.NoError(s.T(), repo.Push(&git.PushOptions{RemoteName: "origin"}))
|
||||||
|
|
||||||
|
return remotePath
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CloneSuite) TestClone_FileURLWorks() {
|
||||||
|
remotePath := s.createBareRemote("castle")
|
||||||
|
|
||||||
|
err := s.app.Clone("file://"+remotePath, "parity-castle")
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
require.FileExists(s.T(), filepath.Join(s.reposDir, "parity-castle", "home", ".vimrc"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CloneSuite) TestClone_DerivesDestinationWhenMissing() {
|
||||||
|
remotePath := s.createBareRemote("dotfiles")
|
||||||
|
|
||||||
|
err := s.app.Clone("file://"+remotePath, "")
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
require.DirExists(s.T(), filepath.Join(s.reposDir, "dotfiles"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CloneSuite) TestClone_LocalPathSymlinksDirectory() {
|
||||||
|
localCastle := filepath.Join(s.tmpDir, "local-castle")
|
||||||
|
require.NoError(s.T(), os.MkdirAll(filepath.Join(localCastle, "home"), 0o755))
|
||||||
|
|
||||||
|
err := s.app.Clone(localCastle, "")
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
|
||||||
|
destination := filepath.Join(s.reposDir, "local-castle")
|
||||||
|
info, err := os.Lstat(destination)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
require.True(s.T(), info.Mode()&os.ModeSymlink != 0)
|
||||||
|
}
|
||||||
555
internal/homesick/core/core.go
Normal file
555
internal/homesick/core/core.go
Normal 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
|
||||||
|
}
|
||||||
24
internal/homesick/core/core_test.go
Normal file
24
internal/homesick/core/core_test.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestDeriveDestination(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
uri string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{name: "github short", uri: "technicalpickles/pickled-vim", want: "pickled-vim"},
|
||||||
|
{name: "https", uri: "https://github.com/technicalpickles/pickled-vim.git", want: "pickled-vim"},
|
||||||
|
{name: "git ssh", uri: "git@github.com:technicalpickles/pickled-vim.git", want: "pickled-vim"},
|
||||||
|
{name: "file", uri: "file:///tmp/dotfiles.git", want: "dotfiles"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := deriveDestination(tt.uri); got != tt.want {
|
||||||
|
t.Fatalf("deriveDestination(%q) = %q, want %q", tt.uri, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
76
internal/homesick/core/diff_test.go
Normal file
76
internal/homesick/core/diff_test.go
Normal 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")
|
||||||
|
}
|
||||||
118
internal/homesick/core/link_test.go
Normal file
118
internal/homesick/core/link_test.go
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
package core_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.hrafn.xyz/aether/gosick/internal/homesick/core"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LinkSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
tmpDir string
|
||||||
|
homeDir string
|
||||||
|
reposDir string
|
||||||
|
app *core.App
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLinkSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(LinkSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LinkSuite) SetupTest() {
|
||||||
|
s.tmpDir = s.T().TempDir()
|
||||||
|
s.homeDir = filepath.Join(s.tmpDir, "home")
|
||||||
|
s.reposDir = filepath.Join(s.homeDir, ".homesick", "repos")
|
||||||
|
|
||||||
|
require.NoError(s.T(), os.MkdirAll(s.reposDir, 0o755))
|
||||||
|
|
||||||
|
s.app = &core.App{
|
||||||
|
HomeDir: s.homeDir,
|
||||||
|
ReposDir: s.reposDir,
|
||||||
|
Stdout: io.Discard,
|
||||||
|
Stderr: io.Discard,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LinkSuite) createCastle(castle string) string {
|
||||||
|
castleHome := filepath.Join(s.reposDir, castle, "home")
|
||||||
|
require.NoError(s.T(), os.MkdirAll(castleHome, 0o755))
|
||||||
|
return castleHome
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LinkSuite) writeFile(path string, content string) {
|
||||||
|
require.NoError(s.T(), os.MkdirAll(filepath.Dir(path), 0o755))
|
||||||
|
require.NoError(s.T(), os.WriteFile(path, []byte(content), 0o644))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LinkSuite) TestLink_SymlinksTopLevelFiles() {
|
||||||
|
castleHome := s.createCastle("glencairn")
|
||||||
|
dotfile := filepath.Join(castleHome, ".vimrc")
|
||||||
|
s.writeFile(dotfile, "set number\n")
|
||||||
|
|
||||||
|
err := s.app.Link("glencairn")
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
|
||||||
|
homePath := filepath.Join(s.homeDir, ".vimrc")
|
||||||
|
info, err := os.Lstat(homePath)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
require.True(s.T(), info.Mode()&os.ModeSymlink != 0)
|
||||||
|
|
||||||
|
target, err := os.Readlink(homePath)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
require.Equal(s.T(), dotfile, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LinkSuite) TestLink_RespectsHomesickSubdir() {
|
||||||
|
castleHome := s.createCastle("glencairn")
|
||||||
|
configDir := filepath.Join(castleHome, ".config")
|
||||||
|
appDir := filepath.Join(configDir, "myapp")
|
||||||
|
s.writeFile(filepath.Join(appDir, "config.toml"), "ok=true\n")
|
||||||
|
s.writeFile(filepath.Join(s.reposDir, "glencairn", ".homesick_subdir"), ".config\n")
|
||||||
|
|
||||||
|
err := s.app.Link("glencairn")
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
|
||||||
|
configInfo, err := os.Lstat(filepath.Join(s.homeDir, ".config"))
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
require.False(s.T(), configInfo.Mode()&os.ModeSymlink != 0)
|
||||||
|
|
||||||
|
homeApp := filepath.Join(s.homeDir, ".config", "myapp")
|
||||||
|
appInfo, err := os.Lstat(homeApp)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
require.True(s.T(), appInfo.Mode()&os.ModeSymlink != 0)
|
||||||
|
|
||||||
|
target, err := os.Readlink(homeApp)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
require.Equal(s.T(), appDir, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LinkSuite) TestLink_ForceReplacesExistingFile() {
|
||||||
|
castleHome := s.createCastle("glencairn")
|
||||||
|
dotfile := filepath.Join(castleHome, ".zshrc")
|
||||||
|
s.writeFile(dotfile, "export EDITOR=vim\n")
|
||||||
|
s.writeFile(filepath.Join(s.homeDir, ".zshrc"), "existing\n")
|
||||||
|
|
||||||
|
s.app.Force = true
|
||||||
|
err := s.app.Link("glencairn")
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
|
||||||
|
homePath := filepath.Join(s.homeDir, ".zshrc")
|
||||||
|
info, err := os.Lstat(homePath)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
require.True(s.T(), info.Mode()&os.ModeSymlink != 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LinkSuite) TestLink_NoForceErrorsOnConflict() {
|
||||||
|
castleHome := s.createCastle("glencairn")
|
||||||
|
dotfile := filepath.Join(castleHome, ".gitconfig")
|
||||||
|
s.writeFile(dotfile, "[user]\n")
|
||||||
|
s.writeFile(filepath.Join(s.homeDir, ".gitconfig"), "local\n")
|
||||||
|
|
||||||
|
err := s.app.Link("glencairn")
|
||||||
|
require.Error(s.T(), err)
|
||||||
|
}
|
||||||
72
internal/homesick/core/list_test.go
Normal file
72
internal/homesick/core/list_test.go
Normal 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(),
|
||||||
|
)
|
||||||
|
}
|
||||||
51
internal/homesick/core/show_path_test.go
Normal file
51
internal/homesick/core/show_path_test.go
Normal 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(),
|
||||||
|
)
|
||||||
|
}
|
||||||
79
internal/homesick/core/status_test.go
Normal file
79
internal/homesick/core/status_test.go
Normal 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
|
||||||
101
internal/homesick/core/track_test.go
Normal file
101
internal/homesick/core/track_test.go
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
package core_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.hrafn.xyz/aether/gosick/internal/homesick/core"
|
||||||
|
git "github.com/go-git/go-git/v5"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TrackSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
tmpDir string
|
||||||
|
homeDir string
|
||||||
|
reposDir string
|
||||||
|
app *core.App
|
||||||
|
}
|
||||||
|
|
||||||
|
//NB: this has nothing to do with jogging
|
||||||
|
func TestTrackSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(TrackSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TrackSuite) SetupTest() {
|
||||||
|
s.tmpDir = s.T().TempDir()
|
||||||
|
s.homeDir = filepath.Join(s.tmpDir, "home")
|
||||||
|
s.reposDir = filepath.Join(s.homeDir, ".homesick", "repos")
|
||||||
|
require.NoError(s.T(), os.MkdirAll(s.reposDir, 0o755))
|
||||||
|
|
||||||
|
s.app = &core.App{
|
||||||
|
HomeDir: s.homeDir,
|
||||||
|
ReposDir: s.reposDir,
|
||||||
|
Stdout: io.Discard,
|
||||||
|
Stderr: io.Discard,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TrackSuite) createCastleRepo(castle string) string {
|
||||||
|
castleRoot := filepath.Join(s.reposDir, castle)
|
||||||
|
require.NoError(s.T(), os.MkdirAll(filepath.Join(castleRoot, "home"), 0o755))
|
||||||
|
_, err := git.PlainInit(castleRoot, false)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
return castleRoot
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TrackSuite) writeFile(path string, content string) {
|
||||||
|
require.NoError(s.T(), os.MkdirAll(filepath.Dir(path), 0o755))
|
||||||
|
require.NoError(s.T(), os.WriteFile(path, []byte(content), 0o644))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TrackSuite) TestTrack_AfterRelinkTracksFileAndUpdatesSubdir() {
|
||||||
|
castleRoot := s.createCastleRepo("parity-castle")
|
||||||
|
castleHome := filepath.Join(castleRoot, "home")
|
||||||
|
|
||||||
|
s.writeFile(filepath.Join(castleHome, ".vimrc"), "set number\n")
|
||||||
|
s.writeFile(filepath.Join(castleRoot, ".homesick_subdir"), ".config\n")
|
||||||
|
s.writeFile(filepath.Join(castleHome, ".config", "myapp", "config.toml"), "ok=true\n")
|
||||||
|
|
||||||
|
require.NoError(s.T(), s.app.Link("parity-castle"))
|
||||||
|
require.NoError(s.T(), s.app.Unlink("parity-castle"))
|
||||||
|
require.NoError(s.T(), s.app.Link("parity-castle"))
|
||||||
|
|
||||||
|
toolPath := filepath.Join(s.homeDir, ".local", "bin", "tool")
|
||||||
|
s.writeFile(toolPath, "#!/usr/bin/env bash\n")
|
||||||
|
|
||||||
|
require.NoError(s.T(), s.app.Track(toolPath, "parity-castle"))
|
||||||
|
|
||||||
|
expectedTarget := filepath.Join(castleHome, ".local", "bin", "tool")
|
||||||
|
info, err := os.Lstat(toolPath)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
require.True(s.T(), info.Mode()&os.ModeSymlink != 0)
|
||||||
|
|
||||||
|
target, err := os.Readlink(toolPath)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
require.Equal(s.T(), expectedTarget, target)
|
||||||
|
|
||||||
|
subdirData, err := os.ReadFile(filepath.Join(castleRoot, ".homesick_subdir"))
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
require.Contains(s.T(), string(subdirData), ".local/bin\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TrackSuite) TestTrack_DefaultCastleName() {
|
||||||
|
castleRoot := s.createCastleRepo("dotfiles")
|
||||||
|
castleHome := filepath.Join(castleRoot, "home")
|
||||||
|
|
||||||
|
filePath := filepath.Join(s.homeDir, ".tmux.conf")
|
||||||
|
s.writeFile(filePath, "set -g mouse on\n")
|
||||||
|
|
||||||
|
require.NoError(s.T(), s.app.Track(filePath, ""))
|
||||||
|
|
||||||
|
expectedTarget := filepath.Join(castleHome, ".tmux.conf")
|
||||||
|
require.FileExists(s.T(), expectedTarget)
|
||||||
|
|
||||||
|
linkTarget, err := os.Readlink(filePath)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
require.Equal(s.T(), expectedTarget, linkTarget)
|
||||||
|
}
|
||||||
106
internal/homesick/core/unlink_test.go
Normal file
106
internal/homesick/core/unlink_test.go
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
package core_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.hrafn.xyz/aether/gosick/internal/homesick/core"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UnlinkSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
tmpDir string
|
||||||
|
homeDir string
|
||||||
|
reposDir string
|
||||||
|
app *core.App
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnlinkSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(UnlinkSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UnlinkSuite) SetupTest() {
|
||||||
|
s.tmpDir = s.T().TempDir()
|
||||||
|
s.homeDir = filepath.Join(s.tmpDir, "home")
|
||||||
|
s.reposDir = filepath.Join(s.homeDir, ".homesick", "repos")
|
||||||
|
|
||||||
|
require.NoError(s.T(), os.MkdirAll(s.reposDir, 0o755))
|
||||||
|
|
||||||
|
s.app = &core.App{
|
||||||
|
HomeDir: s.homeDir,
|
||||||
|
ReposDir: s.reposDir,
|
||||||
|
Stdout: io.Discard,
|
||||||
|
Stderr: io.Discard,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UnlinkSuite) createCastle(castle string) string {
|
||||||
|
castleHome := filepath.Join(s.reposDir, castle, "home")
|
||||||
|
require.NoError(s.T(), os.MkdirAll(castleHome, 0o755))
|
||||||
|
return castleHome
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UnlinkSuite) writeFile(path string, content string) {
|
||||||
|
require.NoError(s.T(), os.MkdirAll(filepath.Dir(path), 0o755))
|
||||||
|
require.NoError(s.T(), os.WriteFile(path, []byte(content), 0o644))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UnlinkSuite) TestUnlink_RemovesTopLevelSymlinks() {
|
||||||
|
castleHome := s.createCastle("glencairn")
|
||||||
|
dotfile := filepath.Join(castleHome, ".vimrc")
|
||||||
|
s.writeFile(dotfile, "set number\n")
|
||||||
|
|
||||||
|
require.NoError(s.T(), s.app.Link("glencairn"))
|
||||||
|
require.NoError(s.T(), s.app.Unlink("glencairn"))
|
||||||
|
|
||||||
|
require.NoFileExists(s.T(), filepath.Join(s.homeDir, ".vimrc"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UnlinkSuite) TestUnlink_RemovesNonDotfileSymlinks() {
|
||||||
|
castleHome := s.createCastle("glencairn")
|
||||||
|
binFile := filepath.Join(castleHome, "bin")
|
||||||
|
s.writeFile(binFile, "#!/usr/bin/env bash\n")
|
||||||
|
|
||||||
|
require.NoError(s.T(), s.app.Link("glencairn"))
|
||||||
|
require.NoError(s.T(), s.app.Unlink("glencairn"))
|
||||||
|
|
||||||
|
require.NoFileExists(s.T(), filepath.Join(s.homeDir, "bin"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UnlinkSuite) TestUnlink_RespectsHomesickSubdir() {
|
||||||
|
castleHome := s.createCastle("glencairn")
|
||||||
|
appDir := filepath.Join(castleHome, ".config", "myapp")
|
||||||
|
s.writeFile(filepath.Join(appDir, "config.toml"), "ok=true\n")
|
||||||
|
s.writeFile(filepath.Join(s.reposDir, "glencairn", ".homesick_subdir"), ".config\n")
|
||||||
|
|
||||||
|
require.NoError(s.T(), s.app.Link("glencairn"))
|
||||||
|
require.NoError(s.T(), s.app.Unlink("glencairn"))
|
||||||
|
|
||||||
|
require.DirExists(s.T(), filepath.Join(s.homeDir, ".config"))
|
||||||
|
require.NoFileExists(s.T(), filepath.Join(s.homeDir, ".config", "myapp"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UnlinkSuite) TestUnlink_DefaultCastleName() {
|
||||||
|
castleHome := s.createCastle("dotfiles")
|
||||||
|
dotfile := filepath.Join(castleHome, ".zshrc")
|
||||||
|
s.writeFile(dotfile, "export EDITOR=vim\n")
|
||||||
|
|
||||||
|
require.NoError(s.T(), s.app.Link("dotfiles"))
|
||||||
|
require.NoError(s.T(), s.app.Unlink(""))
|
||||||
|
|
||||||
|
require.NoFileExists(s.T(), filepath.Join(s.homeDir, ".zshrc"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UnlinkSuite) TestUnlink_IgnoresNonSymlinkDestination() {
|
||||||
|
castleHome := s.createCastle("glencairn")
|
||||||
|
dotfile := filepath.Join(castleHome, ".gitconfig")
|
||||||
|
s.writeFile(dotfile, "[user]\n")
|
||||||
|
s.writeFile(filepath.Join(s.homeDir, ".gitconfig"), "local\n")
|
||||||
|
|
||||||
|
require.NoError(s.T(), s.app.Unlink("glencairn"))
|
||||||
|
require.FileExists(s.T(), filepath.Join(s.homeDir, ".gitconfig"))
|
||||||
|
}
|
||||||
46
internal/homesick/core/version_test.go
Normal file
46
internal/homesick/core/version_test.go
Normal 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())
|
||||||
|
}
|
||||||
3
internal/homesick/version/version.go
Normal file
3
internal/homesick/version/version.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package version
|
||||||
|
|
||||||
|
const String = "1.1.6"
|
||||||
21
justfile
Normal file
21
justfile
Normal 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
|
||||||
191
lib/homesick.rb
191
lib/homesick.rb
@@ -1,191 +0,0 @@
|
|||||||
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-z_-]+\/[A-Za-z_-]+)\Z/
|
|
||||||
|
|
||||||
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)
|
|
||||||
raise "Castle already cloned to #{uri}"
|
|
||||||
end
|
|
||||||
|
|
||||||
destination = uri.basename
|
|
||||||
|
|
||||||
ln_s uri, destination
|
|
||||||
elsif uri =~ GITHUB_NAME_REPO_PATTERN
|
|
||||||
destination = Pathname.new($1)
|
|
||||||
git_clone "git://github.com/#{$1}.git", :destination => destination
|
|
||||||
elsif uri =~ /\/([^\/]*)(\.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
|
|
||||||
|
|
||||||
homesickrc = destination.join('.homesickrc').expand_path
|
|
||||||
if homesickrc.exist?
|
|
||||||
proceed = shell.yes?("#{uri} 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
|
|
||||||
end
|
|
||||||
else
|
|
||||||
shell.say_status "eval skip", "not evaling #{homesickrc}, #{destination} may need manual configuration", :blue
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
desc "pull NAME", "Update the specified castle"
|
|
||||||
method_option :all, :type => :boolean, :default => false, :required => false, :desc => "Update all cloned castles"
|
|
||||||
def pull(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 "symlink NAME", "Symlinks all dotfiles from the specified castle"
|
|
||||||
def symlink(name)
|
|
||||||
check_castle_existance(name, "symlink")
|
|
||||||
|
|
||||||
inside castle_dir(name) do
|
|
||||||
files = Pathname.glob('.*').reject{|a| [".",".."].include?(a.to_s)}
|
|
||||||
files.each do |path|
|
|
||||||
absolute_path = path.expand_path
|
|
||||||
|
|
||||||
inside home_dir do
|
|
||||||
adjusted_path = (home_dir + path).basename
|
|
||||||
|
|
||||||
ln_s absolute_path, adjusted_path
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
desc "track FILE CASTLE", "add a file to a castle"
|
|
||||||
def track(file, castle)
|
|
||||||
castle = Pathname.new(castle)
|
|
||||||
file = Pathname.new(file)
|
|
||||||
check_castle_existance(castle, 'track')
|
|
||||||
|
|
||||||
absolute_path = file.expand_path
|
|
||||||
castle_path = castle_dir(castle)
|
|
||||||
mv absolute_path, castle_path
|
|
||||||
|
|
||||||
inside home_dir do
|
|
||||||
absolute_path = castle_dir(castle) + file.basename
|
|
||||||
home_path = home_dir + file
|
|
||||||
ln_s absolute_path, home_path
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
desc "list", "List cloned castles"
|
|
||||||
def list
|
|
||||||
inside_each_castle do |castle|
|
|
||||||
say_status castle.relative_path_from(repos_dir), `git config remote.origin.url`.chomp, :cyan
|
|
||||||
end
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
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")
|
|
||||||
# reject paths that lie inside another castle, like git submodules
|
|
||||||
return dirs.reject do |dir|
|
|
||||||
dirs.any? {|other| dir != other && dir.fnmatch(other.parent.join('*').to_s) }
|
|
||||||
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
|
|
||||||
end
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
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] || begin
|
|
||||||
repo =~ /([^\/]+)\.git$/
|
|
||||||
$1
|
|
||||||
end
|
|
||||||
|
|
||||||
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 #{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
|
|
||||||
unless 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 == ''
|
|
||||||
|
|
||||||
unless 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 >/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 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 ln_s(source, destination, config = {})
|
|
||||||
source = Pathname.new(source)
|
|
||||||
destination = Pathname.new(destination)
|
|
||||||
|
|
||||||
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 -sf #{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 "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
|
|
||||||
@@ -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
|
|
||||||
57
script/run-behavior-suite-docker.sh
Executable file
57
script/run-behavior-suite-docker.sh
Executable file
@@ -0,0 +1,57 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
: "${HOMESICK_CMD:=/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
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
require 'spec_helper'
|
|
||||||
|
|
||||||
describe "homesick" do
|
|
||||||
before do
|
|
||||||
@homesick = Homesick.new
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "clone" do
|
|
||||||
context "of a file" do
|
|
||||||
it "should symlink existing directories" do
|
|
||||||
somewhere = create_construct
|
|
||||||
somewhere.directory('wtf')
|
|
||||||
wtf = somewhere + 'wtf'
|
|
||||||
|
|
||||||
@homesick.should_receive(:ln_s).with(wtf, wtf.basename)
|
|
||||||
|
|
||||||
@homesick.clone wtf
|
|
||||||
end
|
|
||||||
|
|
||||||
context "when it exists in a repo directory" do
|
|
||||||
before do
|
|
||||||
@repos_dir = create_construct
|
|
||||||
@existing_dir = @repos_dir.directory('existing_castle')
|
|
||||||
@homesick.stub!(:repos_dir).and_return(@repos_dir)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should not symlink" do
|
|
||||||
@homesick.should_not_receive(:git_clone)
|
|
||||||
|
|
||||||
@homesick.clone @existing_dir.to_s rescue nil
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should raise an error" do
|
|
||||||
@existing_castle = @homesick.send(:repos_dir) + 'existing_castle'
|
|
||||||
lambda {
|
|
||||||
@homesick.clone @existing_castle.to_s
|
|
||||||
}.should raise_error(/already cloned/i)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
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 not try to clone a malformed uri like malformed" do
|
|
||||||
@homesick.should_not_receive(:git_clone)
|
|
||||||
|
|
||||||
@homesick.clone 'malformed' rescue nil
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should throw an exception when trying to clone a malformed uri like malformed" do
|
|
||||||
lambda {
|
|
||||||
@homesick.clone 'malformed'
|
|
||||||
}.should raise_error
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should clone a github repo" do
|
|
||||||
@homesick.should_receive(:git_clone).with('git://github.com/wfarr/dotfiles.git', :destination => Pathname.new('wfarr/dotfiles'))
|
|
||||||
|
|
||||||
@homesick.clone "wfarr/dotfiles"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "list" do
|
|
||||||
|
|
||||||
# FIXME only passes in isolation. need to setup data a bit better
|
|
||||||
xit "should say each castle in the castle directory" do
|
|
||||||
@user_dir.directory '.homesick/repos' do |repos_dir|
|
|
||||||
repos_dir.directory 'zomg' do |zomg|
|
|
||||||
Dir.chdir do
|
|
||||||
system "git init >/dev/null 2>&1"
|
|
||||||
system "git remote add origin git://github.com/technicalpickles/zomg.git >/dev/null 2>&1"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
repos_dir.directory 'wtf/zomg' do |zomg|
|
|
||||||
Dir.chdir do
|
|
||||||
system "git init >/dev/null 2>&1"
|
|
||||||
system "git remote add origin git://github.com/technicalpickles/zomg.git >/dev/null 2>&1"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@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 "pull" do
|
|
||||||
|
|
||||||
xit "needs testing"
|
|
||||||
|
|
||||||
describe "--all" do
|
|
||||||
xit "needs testing"
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "track" do
|
|
||||||
it "should move the tracked file into the castle" do
|
|
||||||
some_rc_file = @user_dir.file '.some_rc_file'
|
|
||||||
homesickrepo = @user_dir.directory('.homesick').directory('repos').directory('castle_repo')
|
|
||||||
castle_path = homesickrepo.directory 'home'
|
|
||||||
|
|
||||||
# There is some hideous thing going on with construct; rming the file I'm moving works on this test.
|
|
||||||
# Otherwise when track ln_s's it back out, it sees a conflict. Its as if file operations don't
|
|
||||||
# actually effect this thing, or something.
|
|
||||||
system "rm #{some_rc_file.to_s}"
|
|
||||||
Dir.chdir homesickrepo do
|
|
||||||
system "git init >/dev/null 2>&1"
|
|
||||||
end
|
|
||||||
|
|
||||||
@homesick.should_receive(:mv).with(some_rc_file, castle_path)
|
|
||||||
@homesick.should_receive(:ln_s).with(castle_path + some_rc_file.basename, some_rc_file)
|
|
||||||
@homesick.track(some_rc_file.to_s, 'castle_repo')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
--color
|
|
||||||
@@ -1,19 +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'
|
|
||||||
|
|
||||||
Rspec.configure do |config|
|
|
||||||
config.include Construct::Helpers
|
|
||||||
|
|
||||||
config.before do
|
|
||||||
@user_dir = create_construct
|
|
||||||
ENV['HOME'] = @user_dir.to_s
|
|
||||||
end
|
|
||||||
|
|
||||||
config.after do
|
|
||||||
@user_dir.destroy!
|
|
||||||
end
|
|
||||||
end
|
|
||||||
193
test/behavior/behavior_suite.sh
Executable file
193
test/behavior/behavior_suite.sh
Executable file
@@ -0,0 +1,193 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
: "${HOMESICK_CMD:=/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
|
||||||
Reference in New Issue
Block a user