check-symlinks 0.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- check_symlinks-0.6.0/.gitignore +41 -0
- check_symlinks-0.6.0/LICENSE +21 -0
- check_symlinks-0.6.0/PKG-INFO +152 -0
- check_symlinks-0.6.0/README.md +116 -0
- check_symlinks-0.6.0/build-constraints.txt +71 -0
- check_symlinks-0.6.0/cli/cli.go +126 -0
- check_symlinks-0.6.0/cli/ignore.go +81 -0
- check_symlinks-0.6.0/cli/path.go +60 -0
- check_symlinks-0.6.0/cli/walk.go +140 -0
- check_symlinks-0.6.0/go.mod +19 -0
- check_symlinks-0.6.0/gocompat/go.mod +16 -0
- check_symlinks-0.6.0/gocompat/so/bufio/bufio.go +30 -0
- check_symlinks-0.6.0/gocompat/so/conc/conc.go +41 -0
- check_symlinks-0.6.0/gocompat/so/flag/flag.go +110 -0
- check_symlinks-0.6.0/gocompat/so/fmt/fmt.go +35 -0
- check_symlinks-0.6.0/gocompat/so/mem/mem.go +26 -0
- check_symlinks-0.6.0/gocompat/so/os/os.go +204 -0
- check_symlinks-0.6.0/gocompat/so/runtime/runtime.go +14 -0
- check_symlinks-0.6.0/gocompat/so/slices/slices.go +64 -0
- check_symlinks-0.6.0/gocompat/so/strings/strings.go +45 -0
- check_symlinks-0.6.0/gocompat/so/sync/atomic/atomic.go +36 -0
- check_symlinks-0.6.0/gocompat/so/sync/sync.go +37 -0
- check_symlinks-0.6.0/gocompat/so/testing/testing.go +79 -0
- check_symlinks-0.6.0/hatch_build.py +119 -0
- check_symlinks-0.6.0/main.go +12 -0
- check_symlinks-0.6.0/main_test.go +90 -0
- check_symlinks-0.6.0/pyproject.toml +51 -0
- check_symlinks-0.6.0/scripts/zigcc +9 -0
- check_symlinks-0.6.0/so/go.mod +21 -0
- check_symlinks-0.6.0/so/go.sum +4 -0
- check_symlinks-0.6.0/so/gotest.mod +19 -0
- check_symlinks-0.6.0/so/main.go +12 -0
- check_symlinks-0.6.0/so/test/check.go +237 -0
- check_symlinks-0.6.0/so/test/helpers.go +67 -0
- check_symlinks-0.6.0/so/test/main.go +26 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# If you prefer the allow list template instead of the deny list, see community template:
|
|
2
|
+
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
|
3
|
+
#
|
|
4
|
+
# Binaries for programs and plugins
|
|
5
|
+
*.exe
|
|
6
|
+
*.exe~
|
|
7
|
+
*.dll
|
|
8
|
+
*.so
|
|
9
|
+
*.dylib
|
|
10
|
+
|
|
11
|
+
# Test binary, built with `go test -c`
|
|
12
|
+
*.test
|
|
13
|
+
|
|
14
|
+
# Output of the go coverage tool, specifically when used with LiteIDE
|
|
15
|
+
*.out
|
|
16
|
+
|
|
17
|
+
# Dependency directories (remove the comment below to include it)
|
|
18
|
+
# vendor/
|
|
19
|
+
|
|
20
|
+
# Go workspace file
|
|
21
|
+
go.work
|
|
22
|
+
go.work.sum
|
|
23
|
+
|
|
24
|
+
# env file
|
|
25
|
+
.env
|
|
26
|
+
|
|
27
|
+
# python packaging
|
|
28
|
+
dist/
|
|
29
|
+
*.egg-info/
|
|
30
|
+
__pycache__
|
|
31
|
+
*.whl
|
|
32
|
+
|
|
33
|
+
# build artifacts: check-symlinks is the go/gocompat binary main_test.go
|
|
34
|
+
# builds, -go and -so the two toolchains' pre-commit outputs, .so-bin the
|
|
35
|
+
# pinned solod toolchain the hatch hook installs.
|
|
36
|
+
/check-symlinks
|
|
37
|
+
/check-symlinks-go
|
|
38
|
+
/check-symlinks-so
|
|
39
|
+
/.so-bin/
|
|
40
|
+
|
|
41
|
+
.aider*
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Jamison Lahman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: check-symlinks
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: Check for broken symlinks
|
|
5
|
+
Project-URL: Repository, https://github.com/jmelahman/check-symlinks
|
|
6
|
+
Author-email: Jamison Lahman <jamison@lahman.dev>
|
|
7
|
+
License: MIT License
|
|
8
|
+
|
|
9
|
+
Copyright (c) 2025 Jamison Lahman
|
|
10
|
+
|
|
11
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
12
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
13
|
+
in the Software without restriction, including without limitation the rights
|
|
14
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
15
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
16
|
+
furnished to do so, subject to the following conditions:
|
|
17
|
+
|
|
18
|
+
The above copyright notice and this permission notice shall be included in all
|
|
19
|
+
copies or substantial portions of the Software.
|
|
20
|
+
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
22
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
23
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
24
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
25
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
26
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
27
|
+
SOFTWARE.
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Keywords: ci,continuous-integration,linter,symlinks,tooling,tools
|
|
30
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
31
|
+
Classifier: Operating System :: MacOS
|
|
32
|
+
Classifier: Operating System :: POSIX
|
|
33
|
+
Classifier: Programming Language :: Go
|
|
34
|
+
Requires-Python: >=3.6
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# Check Symlinks
|
|
38
|
+
|
|
39
|
+
[](https://github.com/jmelahman/check-symlinks/actions)
|
|
40
|
+
[](https://github.com/jmelahman/check-symlinks/actions)
|
|
41
|
+
[](https://aur.archlinux.org/packages/check-symlinks)
|
|
42
|
+
[](https://pypi.org/project/check-symlinks/)
|
|
43
|
+
|
|
44
|
+
Check for broken symbolic links.
|
|
45
|
+
|
|
46
|
+
```shell
|
|
47
|
+
$ check-symlinks
|
|
48
|
+
Broken symlink: some/path/broken_link
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`check-symlinks` is optimized for large codebases as well as small, incremental checks,
|
|
52
|
+
|
|
53
|
+
<p align="center">
|
|
54
|
+
<picture align="center">
|
|
55
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/benchmark-dark.svg">
|
|
56
|
+
<source media="(prefers-color-scheme: light)" srcset="docs/benchmark-light.svg">
|
|
57
|
+
<img alt="Bar chart of benchmark results: check-symlinks 4.0 ms, fd 15.6 ms, check_symlinks.py 104.3 ms, find 150.1 ms, shell loop 225.4 ms." src="docs/benchmark-light.svg">
|
|
58
|
+
</picture>
|
|
59
|
+
</p>
|
|
60
|
+
|
|
61
|
+
where the full commands are respectively,
|
|
62
|
+
|
|
63
|
+
```shell
|
|
64
|
+
check-symlinks
|
|
65
|
+
|
|
66
|
+
fd --type symlink --exec sh -c 'test -e "$0"'
|
|
67
|
+
|
|
68
|
+
git ls-files | xargs pre_commit_hooks/check_symlinks.py
|
|
69
|
+
|
|
70
|
+
find . -type l ! -exec test -e {} \; -print0 | xargs --no-run-if-empty -0 git ls-files
|
|
71
|
+
|
|
72
|
+
while read file; do test -e "$file"; done < <(git ls-files)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
and `check_symlinks.py` is from [https://github.com/pre-commit/pre-commit-hooks](https://github.com/pre-commit/pre-commit-hooks/blob/main/pre_commit_hooks/check_symlinks.py).
|
|
76
|
+
Regenerate the chart with `scripts/gen-benchmark-chart.py`, which documents the exact
|
|
77
|
+
hyperfine invocations.
|
|
78
|
+
|
|
79
|
+
## Install
|
|
80
|
+
|
|
81
|
+
**pre-commit:**
|
|
82
|
+
|
|
83
|
+
```yaml
|
|
84
|
+
repos:
|
|
85
|
+
- repo: https://github.com/jmelahman/check-symlinks
|
|
86
|
+
rev: v0.6.0
|
|
87
|
+
hooks:
|
|
88
|
+
- id: check-symlinks
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The `check-symlinks` hook is built from source by pre-commit's Go toolchain and has no
|
|
92
|
+
other dependencies. Use `check-symlinks-system` instead to run a binary already on
|
|
93
|
+
`PATH`.
|
|
94
|
+
|
|
95
|
+
**AUR:**
|
|
96
|
+
|
|
97
|
+
`check-symlinks` is available from the [Arch User Repository](https://aur.archlinux.org/packages/check-symlinks).
|
|
98
|
+
|
|
99
|
+
```shell
|
|
100
|
+
yay -S check-symlinks
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**pip:**
|
|
104
|
+
|
|
105
|
+
`check-symlinks` is available as a [pypi package](https://pypi.org/project/check-symlinks/).
|
|
106
|
+
|
|
107
|
+
```shell
|
|
108
|
+
pip install check-symlinks
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Binaries:**
|
|
112
|
+
|
|
113
|
+
Static binaries for Linux and macOS (amd64 and arm64) are attached to every
|
|
114
|
+
[release](https://github.com/jmelahman/check-symlinks/releases).
|
|
115
|
+
|
|
116
|
+
## Usage
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
check-symlinks [flags] [path ...]
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
With no paths, the current directory is walked. Flags must precede the paths.
|
|
123
|
+
|
|
124
|
+
| Flag | Description |
|
|
125
|
+
| --- | --- |
|
|
126
|
+
| `--hidden` | Include hidden files and directories in the walk. |
|
|
127
|
+
| `--no-ignore` | Ignore `.symlinkignore` / `.config/symlinkignore`. |
|
|
128
|
+
| `--threads N` | Number of worker threads (0 = one per CPU). |
|
|
129
|
+
| `-q`, `--quiet` | Don't print broken links. |
|
|
130
|
+
| `--debug` | Report which ignore file was loaded and which paths it skipped. |
|
|
131
|
+
| `--version` | Print the version and exit. |
|
|
132
|
+
|
|
133
|
+
Exit status is `0` when every symlink resolves, `1` when at least one is broken, and
|
|
134
|
+
`2` on a usage error.
|
|
135
|
+
|
|
136
|
+
Paths listed in `.symlinkignore` (or `.config/symlinkignore`) at the top of the
|
|
137
|
+
repository are skipped. Hidden entries are skipped while walking, but a hidden path
|
|
138
|
+
named on the command line is always checked.
|
|
139
|
+
|
|
140
|
+
## Building
|
|
141
|
+
|
|
142
|
+
`check-symlinks` is written in [Solod](https://solod.dev), a strict subset of Go that
|
|
143
|
+
translates to C: release binaries are a few hundred kilobytes, start instantly, and
|
|
144
|
+
have no runtime dependencies. The same sources build with either toolchain — see
|
|
145
|
+
[CONTRIBUTING.md](CONTRIBUTING.md).
|
|
146
|
+
|
|
147
|
+
```shell
|
|
148
|
+
so build -o check-symlinks ./so # solod (needs the so tool and a C compiler)
|
|
149
|
+
go build . # Go, via the gocompat shims
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Windows is not supported: Solod's `os` package is POSIX-only.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Check Symlinks
|
|
2
|
+
|
|
3
|
+
[](https://github.com/jmelahman/check-symlinks/actions)
|
|
4
|
+
[](https://github.com/jmelahman/check-symlinks/actions)
|
|
5
|
+
[](https://aur.archlinux.org/packages/check-symlinks)
|
|
6
|
+
[](https://pypi.org/project/check-symlinks/)
|
|
7
|
+
|
|
8
|
+
Check for broken symbolic links.
|
|
9
|
+
|
|
10
|
+
```shell
|
|
11
|
+
$ check-symlinks
|
|
12
|
+
Broken symlink: some/path/broken_link
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`check-symlinks` is optimized for large codebases as well as small, incremental checks,
|
|
16
|
+
|
|
17
|
+
<p align="center">
|
|
18
|
+
<picture align="center">
|
|
19
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/benchmark-dark.svg">
|
|
20
|
+
<source media="(prefers-color-scheme: light)" srcset="docs/benchmark-light.svg">
|
|
21
|
+
<img alt="Bar chart of benchmark results: check-symlinks 4.0 ms, fd 15.6 ms, check_symlinks.py 104.3 ms, find 150.1 ms, shell loop 225.4 ms." src="docs/benchmark-light.svg">
|
|
22
|
+
</picture>
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
where the full commands are respectively,
|
|
26
|
+
|
|
27
|
+
```shell
|
|
28
|
+
check-symlinks
|
|
29
|
+
|
|
30
|
+
fd --type symlink --exec sh -c 'test -e "$0"'
|
|
31
|
+
|
|
32
|
+
git ls-files | xargs pre_commit_hooks/check_symlinks.py
|
|
33
|
+
|
|
34
|
+
find . -type l ! -exec test -e {} \; -print0 | xargs --no-run-if-empty -0 git ls-files
|
|
35
|
+
|
|
36
|
+
while read file; do test -e "$file"; done < <(git ls-files)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
and `check_symlinks.py` is from [https://github.com/pre-commit/pre-commit-hooks](https://github.com/pre-commit/pre-commit-hooks/blob/main/pre_commit_hooks/check_symlinks.py).
|
|
40
|
+
Regenerate the chart with `scripts/gen-benchmark-chart.py`, which documents the exact
|
|
41
|
+
hyperfine invocations.
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
**pre-commit:**
|
|
46
|
+
|
|
47
|
+
```yaml
|
|
48
|
+
repos:
|
|
49
|
+
- repo: https://github.com/jmelahman/check-symlinks
|
|
50
|
+
rev: v0.6.0
|
|
51
|
+
hooks:
|
|
52
|
+
- id: check-symlinks
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The `check-symlinks` hook is built from source by pre-commit's Go toolchain and has no
|
|
56
|
+
other dependencies. Use `check-symlinks-system` instead to run a binary already on
|
|
57
|
+
`PATH`.
|
|
58
|
+
|
|
59
|
+
**AUR:**
|
|
60
|
+
|
|
61
|
+
`check-symlinks` is available from the [Arch User Repository](https://aur.archlinux.org/packages/check-symlinks).
|
|
62
|
+
|
|
63
|
+
```shell
|
|
64
|
+
yay -S check-symlinks
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**pip:**
|
|
68
|
+
|
|
69
|
+
`check-symlinks` is available as a [pypi package](https://pypi.org/project/check-symlinks/).
|
|
70
|
+
|
|
71
|
+
```shell
|
|
72
|
+
pip install check-symlinks
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Binaries:**
|
|
76
|
+
|
|
77
|
+
Static binaries for Linux and macOS (amd64 and arm64) are attached to every
|
|
78
|
+
[release](https://github.com/jmelahman/check-symlinks/releases).
|
|
79
|
+
|
|
80
|
+
## Usage
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
check-symlinks [flags] [path ...]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
With no paths, the current directory is walked. Flags must precede the paths.
|
|
87
|
+
|
|
88
|
+
| Flag | Description |
|
|
89
|
+
| --- | --- |
|
|
90
|
+
| `--hidden` | Include hidden files and directories in the walk. |
|
|
91
|
+
| `--no-ignore` | Ignore `.symlinkignore` / `.config/symlinkignore`. |
|
|
92
|
+
| `--threads N` | Number of worker threads (0 = one per CPU). |
|
|
93
|
+
| `-q`, `--quiet` | Don't print broken links. |
|
|
94
|
+
| `--debug` | Report which ignore file was loaded and which paths it skipped. |
|
|
95
|
+
| `--version` | Print the version and exit. |
|
|
96
|
+
|
|
97
|
+
Exit status is `0` when every symlink resolves, `1` when at least one is broken, and
|
|
98
|
+
`2` on a usage error.
|
|
99
|
+
|
|
100
|
+
Paths listed in `.symlinkignore` (or `.config/symlinkignore`) at the top of the
|
|
101
|
+
repository are skipped. Hidden entries are skipped while walking, but a hidden path
|
|
102
|
+
named on the command line is always checked.
|
|
103
|
+
|
|
104
|
+
## Building
|
|
105
|
+
|
|
106
|
+
`check-symlinks` is written in [Solod](https://solod.dev), a strict subset of Go that
|
|
107
|
+
translates to C: release binaries are a few hundred kilobytes, start instantly, and
|
|
108
|
+
have no runtime dependencies. The same sources build with either toolchain — see
|
|
109
|
+
[CONTRIBUTING.md](CONTRIBUTING.md).
|
|
110
|
+
|
|
111
|
+
```shell
|
|
112
|
+
so build -o check-symlinks ./so # solod (needs the so tool and a C compiler)
|
|
113
|
+
go build . # Go, via the gocompat shims
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Windows is not supported: Solod's `os` package is POSIX-only.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# This file was autogenerated by uv via the following command:
|
|
2
|
+
# uv pip compile - -o build-constraints.txt --universal --generate-hashes
|
|
3
|
+
go-bin==1.26.6 \
|
|
4
|
+
--hash=sha256:13a3a133530ffd2005b2ce805a93b8bf3f4712807af16f31aeea6aecfaa55721 \
|
|
5
|
+
--hash=sha256:1f4b3a6f8d7987c032d821ebb69e84ab9be7913b1cd8c45514d9aea21e3dfb7d \
|
|
6
|
+
--hash=sha256:2440ec27b056a19e9ce7c7e4c323f93a1ec0aa220b83838b9adf319487091582 \
|
|
7
|
+
--hash=sha256:2f8d76337bdf2e8e12b655c2f2c4827a202d4198c5cb87f0fde8b286bfa5b816 \
|
|
8
|
+
--hash=sha256:46658f18ce37d71122d25fa48bd7457cf46a3389345e86c5b6decabf1d5ec6ad \
|
|
9
|
+
--hash=sha256:565537475730612936bf42edddff5d627133c108d9a01561f8033ed09dc0c2ff \
|
|
10
|
+
--hash=sha256:7f30d0141d8052d9349b6e26c679ff51d14465ae3362ca9d1623ba1076e376a4 \
|
|
11
|
+
--hash=sha256:ab703942cbb119637e5306085b6f1f14eb51bd6a98b56dd2ff0a06b5afb742f8 \
|
|
12
|
+
--hash=sha256:c0dbc08e9a5a827966c8f2d6fb627668e78ab0f9732035564e24d046a185d92b \
|
|
13
|
+
--hash=sha256:efc17389b78138c27ce3c74275a146ce5e2b39c3969f57a170bbcfe303baf91b
|
|
14
|
+
hatch-vcs==0.5.0 \
|
|
15
|
+
--hash=sha256:0395fa126940340215090c344a2bf4e2a77bcbe7daab16f41b37b98c95809ff9 \
|
|
16
|
+
--hash=sha256:b49677dbdc597460cc22d01b27ab3696f5e16a21ecf2700fb01bc28e1f2a04a7
|
|
17
|
+
hatchling==1.32.0 \
|
|
18
|
+
--hash=sha256:0bdbde4a52b06c37e3eca395f85a762bf0ef06fe374fd8ae429dc6be10230f5f \
|
|
19
|
+
--hash=sha256:0e17c9c3b9aa7c625acc8d0f5b622f107d5049af9ecf5ada4de1aada5be7cdbc
|
|
20
|
+
# via hatch-vcs
|
|
21
|
+
manygo==0.2.0 \
|
|
22
|
+
--hash=sha256:322567f555f0c9896e163f4cecdcd5aa6402996933a6a8ec1cea7f67dae9accd \
|
|
23
|
+
--hash=sha256:90a951179f0d294d7063a4fc5446b52646cff140c386f9c92ca391116a287098
|
|
24
|
+
packaging==26.3 \
|
|
25
|
+
--hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \
|
|
26
|
+
--hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c
|
|
27
|
+
# via
|
|
28
|
+
# hatchling
|
|
29
|
+
# setuptools-scm
|
|
30
|
+
# vcs-versioning
|
|
31
|
+
pathspec==1.1.1 \
|
|
32
|
+
--hash=sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a \
|
|
33
|
+
--hash=sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189
|
|
34
|
+
# via hatchling
|
|
35
|
+
pluggy==1.6.0 \
|
|
36
|
+
--hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \
|
|
37
|
+
--hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746
|
|
38
|
+
# via hatchling
|
|
39
|
+
setuptools==84.0.0 \
|
|
40
|
+
--hash=sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670 \
|
|
41
|
+
--hash=sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73
|
|
42
|
+
# via setuptools-scm
|
|
43
|
+
setuptools-scm==10.2.1 \
|
|
44
|
+
--hash=sha256:4fa7dd82cf8c800df59c9a288c90299b1657ff1ecfc3f5cc00287c5dbf5e27a9 \
|
|
45
|
+
--hash=sha256:b7c82f4102d389ee57dc66ccdb4f9b4bca3c40ba83b43f1f63d68ccd72db2580
|
|
46
|
+
# via hatch-vcs
|
|
47
|
+
tomlkit==0.15.1 \
|
|
48
|
+
--hash=sha256:177a05aece5a8ca5266fd3c448abb47b8d352f09d477d3ca8332db4d89b24304 \
|
|
49
|
+
--hash=sha256:e25bbf38843005246210a12982776f27f99cb9be67160e14434d0c0d21ee1e97
|
|
50
|
+
# via hatchling
|
|
51
|
+
trove-classifiers==2026.6.1.19 \
|
|
52
|
+
--hash=sha256:ab4c4ec93cc4a4e7815fa759906e05e6bb3f2fbd92ea0f897288c6a43efd15b3 \
|
|
53
|
+
--hash=sha256:c5132b4b61a829d11cfbd2d72e97f20a45ed6edb95e45c5efdeb5e00836b2745
|
|
54
|
+
# via hatchling
|
|
55
|
+
vcs-versioning==2.3.1 \
|
|
56
|
+
--hash=sha256:806635bd0ea653c96af98a70624be758d408a989f2cd0b390e63474d99f96b63 \
|
|
57
|
+
--hash=sha256:a11299394e101569a62ab061375260d08bc56cd33d685b7067d85312368f4457
|
|
58
|
+
# via setuptools-scm
|
|
59
|
+
ziglang==0.16.0 \
|
|
60
|
+
--hash=sha256:089a16a4eb5a2f45151993342f8fabad24ff1a0723dc146642895c29208a3939 \
|
|
61
|
+
--hash=sha256:0fd671c599f6961638cc302cf1f9461486696385311d4c0276171dcf7d2b67f5 \
|
|
62
|
+
--hash=sha256:18e14f6b25678d7b7c65708c82501ee6090fe39ed5c783477d56267af1fa5629 \
|
|
63
|
+
--hash=sha256:7249779f0f916cfd2d1a9d54eb7600f3901384dc8dcbe1eea226bd32a8237fb5 \
|
|
64
|
+
--hash=sha256:8b83661247430aa335b3cbc29569084900412dde5633b02c80a6d2ff734de3d2 \
|
|
65
|
+
--hash=sha256:9fcda73f62b851dd72a54b710ad40a209896db14cfb13649e62191243556342b \
|
|
66
|
+
--hash=sha256:b61e5413c49508d9d62e5dcea543e3af491594154d74a00ff52f84ed508260cc \
|
|
67
|
+
--hash=sha256:d4bd8197344fffe276e1d6446e4ef7bc38637d821b4685fe96a936503d4b0619 \
|
|
68
|
+
--hash=sha256:df0e6599e41d087c912b0d3d7cedef6c9cb1f0032465c8fc820e9986772d11e4 \
|
|
69
|
+
--hash=sha256:e27d409812b11e0fb89ed0200cf2e55b6464d43f9461553104e4a4f9a94a1fd5 \
|
|
70
|
+
--hash=sha256:e44a5271f7b72f7980017bb615823ed52e765f96b6482d93a2fd338cd53101bf \
|
|
71
|
+
--hash=sha256:f978c12b5337cf418034964de00023b7ec5ec484383dc97c6a6585f4a9105840
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Package cli implements check-symlinks: a recursive check for broken
|
|
2
|
+
// symbolic links.
|
|
3
|
+
//
|
|
4
|
+
// The package is written in the Solod subset of Go, so it builds with both
|
|
5
|
+
// toolchains. `so build` translates it to C for the release binaries; a plain
|
|
6
|
+
// `go build` resolves solod.dev to the gocompat/ shims (see the root go.mod)
|
|
7
|
+
// and produces an equivalent binary with no C compiler in sight.
|
|
8
|
+
package cli
|
|
9
|
+
|
|
10
|
+
import (
|
|
11
|
+
"solod.dev/so/conc"
|
|
12
|
+
"solod.dev/so/flag"
|
|
13
|
+
"solod.dev/so/fmt"
|
|
14
|
+
"solod.dev/so/mem"
|
|
15
|
+
"solod.dev/so/os"
|
|
16
|
+
"solod.dev/so/runtime"
|
|
17
|
+
"solod.dev/so/slices"
|
|
18
|
+
"solod.dev/so/strings"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
// Version is the release version. Solod has no ldflags-style stamping, so
|
|
22
|
+
// this constant is the single source of truth; scripts/check-version.sh
|
|
23
|
+
// guards it against the pushed tag.
|
|
24
|
+
const Version = "0.6.0"
|
|
25
|
+
|
|
26
|
+
// defaultRoots is used when no paths are given on the command line.
|
|
27
|
+
var defaultRoots = [1]string{"."}
|
|
28
|
+
|
|
29
|
+
// Run checks the given paths and returns the process exit code:
|
|
30
|
+
// 0 if every symlink resolves, 1 if any is broken, 2 on a usage error.
|
|
31
|
+
func Run(args []string) int {
|
|
32
|
+
var st state
|
|
33
|
+
st.mu.Init()
|
|
34
|
+
st.cond.Init(&st.mu)
|
|
35
|
+
defer st.mu.Free()
|
|
36
|
+
defer st.cond.Free()
|
|
37
|
+
|
|
38
|
+
var noIgnore bool
|
|
39
|
+
var showVersion bool
|
|
40
|
+
var threads int
|
|
41
|
+
fs := flag.NewFlagSet("check-symlinks", flag.ContinueOnError)
|
|
42
|
+
fs.BoolVar(&st.includeHidden, "hidden", false, "include hidden files and directories in the check")
|
|
43
|
+
fs.BoolVar(&noIgnore, "no-ignore", false, "don't use ignore files")
|
|
44
|
+
fs.BoolVar(&st.debug, "debug", false, "run in debug mode")
|
|
45
|
+
fs.BoolVar(&st.quiet, "quiet", false, "run in quiet mode")
|
|
46
|
+
fs.BoolVar(&st.quiet, "q", false, "run in quiet mode")
|
|
47
|
+
fs.IntVar(&threads, "threads", 0, "number of worker threads (0 = one per CPU)")
|
|
48
|
+
fs.BoolVar(&showVersion, "version", false, "print the version and exit")
|
|
49
|
+
if err := fs.Parse(args[1:]); err != nil {
|
|
50
|
+
if err == flag.ErrHelp {
|
|
51
|
+
return 0
|
|
52
|
+
}
|
|
53
|
+
return 2
|
|
54
|
+
}
|
|
55
|
+
if showVersion {
|
|
56
|
+
fmt.Printf("check-symlinks %s\n", Version)
|
|
57
|
+
return 0
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// cwdBuf backs both the working directory and the top-level path sliced
|
|
61
|
+
// out of it, so it has to outlive the walk.
|
|
62
|
+
var cwdBuf [os.MaxPathLen]byte
|
|
63
|
+
cwd, err := os.Getwd(cwdBuf[:])
|
|
64
|
+
if err != nil {
|
|
65
|
+
fmt.Fprintf(os.Stderr, "Error: cannot get working directory\n")
|
|
66
|
+
return 2
|
|
67
|
+
}
|
|
68
|
+
st.topLevel = findTopLevel(cwd)
|
|
69
|
+
if st.topLevel == "" && !st.quiet {
|
|
70
|
+
fmt.Fprintf(os.Stderr, "Failed to find toplevel directory: not a git repository\n")
|
|
71
|
+
}
|
|
72
|
+
if !noIgnore && st.topLevel != "" {
|
|
73
|
+
loadIgnorePatterns(&st)
|
|
74
|
+
}
|
|
75
|
+
defer freePatterns(&st)
|
|
76
|
+
|
|
77
|
+
roots := fs.Args()
|
|
78
|
+
if len(roots) == 0 {
|
|
79
|
+
roots = defaultRoots[:]
|
|
80
|
+
}
|
|
81
|
+
// Seed the work stack. A root that is not a directory is checked
|
|
82
|
+
// directly, without the hidden and ignore filters; a directory root goes
|
|
83
|
+
// through the same filters as any other entry.
|
|
84
|
+
for i := range roots {
|
|
85
|
+
root := roots[i]
|
|
86
|
+
fi, serr := os.Stat(root)
|
|
87
|
+
if serr != nil || !fi.IsDir() {
|
|
88
|
+
st.checked.Add(1)
|
|
89
|
+
checkOne(&st, root)
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
if !st.includeHidden && isHidden(baseName(root)) {
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
if shouldIgnore(&st, root) {
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
st.checked.Add(1)
|
|
99
|
+
st.stack = slices.Append(mem.System, st.stack, strings.Clone(mem.System, root))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if threads <= 0 {
|
|
103
|
+
threads = runtime.NumCPU()
|
|
104
|
+
}
|
|
105
|
+
if threads == 1 {
|
|
106
|
+
// Single-threaded: run the worker loop on the main thread.
|
|
107
|
+
work(&st)
|
|
108
|
+
} else {
|
|
109
|
+
ths := make([]conc.Thread, threads)
|
|
110
|
+
for i := 0; i < threads; i++ {
|
|
111
|
+
ths[i] = conc.Go(workEntry, &st)
|
|
112
|
+
}
|
|
113
|
+
for i := 0; i < threads; i++ {
|
|
114
|
+
ths[i].Wait()
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
slices.Free(mem.System, st.stack)
|
|
118
|
+
|
|
119
|
+
if !st.quiet {
|
|
120
|
+
fmt.Printf("Total files checked: %d\n", st.checked.Load())
|
|
121
|
+
}
|
|
122
|
+
if st.broken.Load() {
|
|
123
|
+
return 1
|
|
124
|
+
}
|
|
125
|
+
return 0
|
|
126
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
package cli
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"solod.dev/so/bufio"
|
|
5
|
+
"solod.dev/so/fmt"
|
|
6
|
+
"solod.dev/so/mem"
|
|
7
|
+
"solod.dev/so/os"
|
|
8
|
+
"solod.dev/so/slices"
|
|
9
|
+
"solod.dev/so/strings"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
// shouldIgnore reports whether a path matches any ignore pattern.
|
|
13
|
+
func shouldIgnore(st *state, p string) bool {
|
|
14
|
+
if len(st.patterns) == 0 {
|
|
15
|
+
return false
|
|
16
|
+
}
|
|
17
|
+
rel := relTo(st.topLevel, p)
|
|
18
|
+
base := baseName(rel)
|
|
19
|
+
for i := range st.patterns {
|
|
20
|
+
pat := strings.TrimSuffix(st.patterns[i], "/")
|
|
21
|
+
if strings.HasPrefix(rel, pat) || base == pat {
|
|
22
|
+
return true
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// relTo strips the top-level prefix from an absolute path.
|
|
29
|
+
func relTo(top string, p string) string {
|
|
30
|
+
if top == "" || len(p) <= len(top) {
|
|
31
|
+
return p
|
|
32
|
+
}
|
|
33
|
+
if strings.HasPrefix(p, top) && p[len(top)] == '/' {
|
|
34
|
+
return p[len(top)+1:]
|
|
35
|
+
}
|
|
36
|
+
return p
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// loadIgnorePatterns loads patterns from .symlinkignore or
|
|
40
|
+
// .config/symlinkignore under the top-level directory.
|
|
41
|
+
func loadIgnorePatterns(st *state) {
|
|
42
|
+
var buf [os.MaxPathLen]byte
|
|
43
|
+
p := joinPath(buf[:], st.topLevel, ".symlinkignore")
|
|
44
|
+
if loadPatternsFromFile(st, p) {
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
p = joinPath(buf[:], st.topLevel, ".config/symlinkignore")
|
|
48
|
+
loadPatternsFromFile(st, p)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// loadPatternsFromFile reads patterns from a file, one per line, and reports
|
|
52
|
+
// whether it found any.
|
|
53
|
+
func loadPatternsFromFile(st *state, name string) bool {
|
|
54
|
+
f, err := os.Open(name)
|
|
55
|
+
if err != nil {
|
|
56
|
+
return false
|
|
57
|
+
}
|
|
58
|
+
defer f.Close()
|
|
59
|
+
|
|
60
|
+
sc := bufio.NewScanner(mem.System, &f)
|
|
61
|
+
defer sc.Free()
|
|
62
|
+
for sc.Scan() {
|
|
63
|
+
line := strings.TrimSpace(sc.Text())
|
|
64
|
+
if line == "" || line[0] == '#' {
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
st.patterns = slices.Append(mem.System, st.patterns, strings.Clone(mem.System, line))
|
|
68
|
+
}
|
|
69
|
+
if st.debug && len(st.patterns) > 0 {
|
|
70
|
+
fmt.Printf("Found ignore file: %s\n", name)
|
|
71
|
+
}
|
|
72
|
+
return len(st.patterns) > 0
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// freePatterns releases the ignore patterns.
|
|
76
|
+
func freePatterns(st *state) {
|
|
77
|
+
for i := range st.patterns {
|
|
78
|
+
mem.FreeString(mem.System, st.patterns[i])
|
|
79
|
+
}
|
|
80
|
+
slices.Free(mem.System, st.patterns)
|
|
81
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
package cli
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"solod.dev/so/os"
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
// joinPath writes dir + "/" + name into buf and returns it as a string view.
|
|
8
|
+
// The result is only valid until the next call with the same buffer.
|
|
9
|
+
func joinPath(buf []byte, dir string, name string) string {
|
|
10
|
+
n := copy(buf, dir)
|
|
11
|
+
if n > 0 && buf[n-1] != '/' && n < len(buf) {
|
|
12
|
+
buf[n] = '/'
|
|
13
|
+
n++
|
|
14
|
+
}
|
|
15
|
+
n += copy(buf[n:], name)
|
|
16
|
+
return string(buf[:n])
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// isHidden reports whether a path element is hidden.
|
|
20
|
+
func isHidden(name string) bool {
|
|
21
|
+
return len(name) > 0 && name[0] == '.' && name != "." && name != ".."
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// baseName returns the last element of a slash-separated path.
|
|
25
|
+
func baseName(p string) string {
|
|
26
|
+
i := lastSlash(p)
|
|
27
|
+
if i < 0 {
|
|
28
|
+
return p
|
|
29
|
+
}
|
|
30
|
+
return p[i+1:]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// lastSlash returns the index of the last '/' in p, or -1.
|
|
34
|
+
func lastSlash(p string) int {
|
|
35
|
+
for i := len(p) - 1; i >= 0; i-- {
|
|
36
|
+
if p[i] == '/' {
|
|
37
|
+
return i
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return -1
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// findTopLevel walks up from start looking for a .git directory.
|
|
44
|
+
// The result is a view into start.
|
|
45
|
+
func findTopLevel(start string) string {
|
|
46
|
+
var buf [os.MaxPathLen]byte
|
|
47
|
+
dir := start
|
|
48
|
+
for {
|
|
49
|
+
p := joinPath(buf[:], dir, ".git")
|
|
50
|
+
fi, err := os.Stat(p)
|
|
51
|
+
if err == nil && fi.IsDir() {
|
|
52
|
+
return dir
|
|
53
|
+
}
|
|
54
|
+
i := lastSlash(dir)
|
|
55
|
+
if i <= 0 {
|
|
56
|
+
return ""
|
|
57
|
+
}
|
|
58
|
+
dir = dir[:i]
|
|
59
|
+
}
|
|
60
|
+
}
|