hlsync 1.15.4__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.
- hlsync-1.15.4/LICENSE +21 -0
- hlsync-1.15.4/MANIFEST.in +3 -0
- hlsync-1.15.4/PKG-INFO +654 -0
- hlsync-1.15.4/README.md +620 -0
- hlsync-1.15.4/build_backend/hlsync_build.py +32 -0
- hlsync-1.15.4/build_backend/versioning.py +63 -0
- hlsync-1.15.4/pyproject.toml +63 -0
- hlsync-1.15.4/setup.cfg +4 -0
- hlsync-1.15.4/src/hlsync/__init__.py +5 -0
- hlsync-1.15.4/src/hlsync/__main__.py +4 -0
- hlsync-1.15.4/src/hlsync/_version.py +2 -0
- hlsync-1.15.4/src/hlsync/cli.py +2879 -0
- hlsync-1.15.4/src/hlsync/comparison.py +278 -0
- hlsync-1.15.4/src/hlsync/config.py +576 -0
- hlsync-1.15.4/src/hlsync/gitignore.py +59 -0
- hlsync-1.15.4/src/hlsync/pattern_operands.py +33 -0
- hlsync-1.15.4/src/hlsync/progress.py +57 -0
- hlsync-1.15.4/src/hlsync/rules.py +276 -0
- hlsync-1.15.4/src/hlsync/selection.py +159 -0
- hlsync-1.15.4/src/hlsync/snapshot.py +193 -0
- hlsync-1.15.4/src/hlsync/storage.py +29 -0
- hlsync-1.15.4/src/hlsync/transfer.py +465 -0
- hlsync-1.15.4/src/hlsync/transport.py +761 -0
- hlsync-1.15.4/src/hlsync.egg-info/PKG-INFO +654 -0
- hlsync-1.15.4/src/hlsync.egg-info/SOURCES.txt +35 -0
- hlsync-1.15.4/src/hlsync.egg-info/dependency_links.txt +1 -0
- hlsync-1.15.4/src/hlsync.egg-info/entry_points.txt +2 -0
- hlsync-1.15.4/src/hlsync.egg-info/requires.txt +9 -0
- hlsync-1.15.4/src/hlsync.egg-info/top_level.txt +1 -0
- hlsync-1.15.4/tests/test_cli.py +1693 -0
- hlsync-1.15.4/tests/test_comparison.py +179 -0
- hlsync-1.15.4/tests/test_config.py +78 -0
- hlsync-1.15.4/tests/test_gitignore.py +33 -0
- hlsync-1.15.4/tests/test_progress.py +26 -0
- hlsync-1.15.4/tests/test_transport.py +823 -0
- hlsync-1.15.4/tests/test_versioning.py +90 -0
- hlsync-1.15.4/version.json +4 -0
hlsync-1.15.4/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hook Line Sync contributors
|
|
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.
|
hlsync-1.15.4/PKG-INFO
ADDED
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hlsync
|
|
3
|
+
Version: 1.15.4
|
|
4
|
+
Summary: Synchronize local files with remote servers over explicit FTPS
|
|
5
|
+
Author: Hook Line Sync contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ediril/hook-line-sync
|
|
8
|
+
Project-URL: Changelog, https://github.com/ediril/hook-line-sync/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: Issues, https://github.com/ediril/hook-line-sync/issues
|
|
10
|
+
Project-URL: Repository, https://github.com/ediril/hook-line-sync.git
|
|
11
|
+
Keywords: deployment,ftp,ftps,synchronization
|
|
12
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: System :: Archiving :: Backup
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: pathspec<2,>=0.12.1
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: build<2,>=1; extra == "dev"
|
|
28
|
+
Requires-Dist: pyftpdlib[ssl]<3,>=2.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest<10,>=8; extra == "dev"
|
|
30
|
+
Requires-Dist: ruff<1,>=0.12; extra == "dev"
|
|
31
|
+
Requires-Dist: setuptools>=77; extra == "dev"
|
|
32
|
+
Requires-Dist: twine<7,>=6; extra == "dev"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# Hook Line Sync
|
|
36
|
+
|
|
37
|
+
Hook Line Sync (`hlsync`) is a command-line workflow for deploying local
|
|
38
|
+
projects to shared hosting over explicit FTP over TLS (FTPS): map once, preview
|
|
39
|
+
the diff, then push the intended files.
|
|
40
|
+
|
|
41
|
+
HLSync is pre-alpha software. Commands and configuration may change before a
|
|
42
|
+
stable release.
|
|
43
|
+
|
|
44
|
+
## Who is it for?
|
|
45
|
+
|
|
46
|
+
HLSync is for people who still deploy websites or small projects to shared hosting over FTP.
|
|
47
|
+
|
|
48
|
+
If your current process is basically “figure out which files changed and upload them,” HLSync gives you a safer, more repeatable version of that workflow without requiring a full deployment system.
|
|
49
|
+
|
|
50
|
+
It works best when your local project is the source of truth and the remote server is simply where the project gets deployed.
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
HLSync requires Python 3.10 or newer. Install it from PyPI as an isolated
|
|
55
|
+
command-line tool with uv:
|
|
56
|
+
|
|
57
|
+
```console
|
|
58
|
+
uv tool install hlsync
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
If you previously installed `hook-line-sync`, switch with:
|
|
62
|
+
|
|
63
|
+
```console
|
|
64
|
+
uv tool uninstall hook-line-sync
|
|
65
|
+
uv tool install hlsync
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Upgrade later with:
|
|
69
|
+
|
|
70
|
+
```console
|
|
71
|
+
uv tool upgrade hlsync
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The FTPS server must support explicit TLS with protected data connections
|
|
75
|
+
(`AUTH TLS` and `PROT P`). HLSync prefers MLSD listings; servers without MLSD
|
|
76
|
+
use Unix-style LIST listings with SIZE and MDTM for exact file metadata.
|
|
77
|
+
LIST mode includes dotfiles and requires two extra requests per file. Push additionally
|
|
78
|
+
requires MFMT (or writable MDTM on vsFTPd) and MDTM read-back to apply and verify
|
|
79
|
+
uploaded-file timestamps.
|
|
80
|
+
|
|
81
|
+
## Quick start
|
|
82
|
+
|
|
83
|
+
HLSync reads credentials from environment variables. The defaults are:
|
|
84
|
+
|
|
85
|
+
```text
|
|
86
|
+
PROD_FTPS_USERNAME
|
|
87
|
+
PROD_FTPS_PASSWORD
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
From the local project root:
|
|
91
|
+
|
|
92
|
+
```console
|
|
93
|
+
hlsync create prod --host ftp.example.com --remote-root /public_html/site
|
|
94
|
+
hlsync connect
|
|
95
|
+
hlsync exclude .git node_modules
|
|
96
|
+
hlsync diff
|
|
97
|
+
hlsync push
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`create` proposes the current directory as the local root; pressing Enter accepts
|
|
101
|
+
it. Declining prompts for another local folder. Use `--local-root PATH` to
|
|
102
|
+
provide one directly. Both `diff` and `push` recurse by default. Use `-s` /
|
|
103
|
+
`--shallow` to process only the selected folder's immediate contents.
|
|
104
|
+
|
|
105
|
+
## Profiles and mapping
|
|
106
|
+
|
|
107
|
+
A profile identifies one deployment target: protocol, host, port, remote root,
|
|
108
|
+
credential environment-variable names, and one local root. Multiple
|
|
109
|
+
profiles may use the same server. Credential values are never stored in
|
|
110
|
+
`~/.hlsync/configs.json`.
|
|
111
|
+
|
|
112
|
+
FTPS is the default and currently the only protocol. Override credential
|
|
113
|
+
variable names when creating a profile if needed:
|
|
114
|
+
|
|
115
|
+
```console
|
|
116
|
+
hlsync create staging \
|
|
117
|
+
--host ftp.example.com \
|
|
118
|
+
--remote-root /public_html/staging \
|
|
119
|
+
--local-root /path/to/project \
|
|
120
|
+
--username-env STAGING_FTPS_USERNAME \
|
|
121
|
+
--password-env STAGING_FTPS_PASSWORD
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
HLSync stores the canonical absolute local root and maps every descendant to
|
|
125
|
+
the same relative path under the remote root. Roots may not overlap across
|
|
126
|
+
profiles. Remapping requires confirmation and defaults to no.
|
|
127
|
+
|
|
128
|
+
Use `map` to change either side after creation. Omit `--local-root` to use the
|
|
129
|
+
current directory; changing only the remote root preserves the local root:
|
|
130
|
+
|
|
131
|
+
```console
|
|
132
|
+
hlsync map staging --local-root /new/local/path
|
|
133
|
+
hlsync map staging --remote-root /new/remote/root
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The normal workflow is to run HLSync inside a mapped root or one of its
|
|
137
|
+
descendants. The current directory selects the containing profile and relative
|
|
138
|
+
local scope automatically:
|
|
139
|
+
|
|
140
|
+
```console
|
|
141
|
+
hlsync profiles # all profiles; * marks the current one
|
|
142
|
+
hlsync profile # current profile name
|
|
143
|
+
hlsync profile --details # current profile details
|
|
144
|
+
hlsync profile staging # verify and print a named profile
|
|
145
|
+
hlsync profile staging --details
|
|
146
|
+
hlsync root staging # print the mapped local root only
|
|
147
|
+
hlsync connect # verify FTPS, then disconnect
|
|
148
|
+
hlsync remove staging # remove local configuration only
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Outside every mapped root, `hlsync profile` reports that no profile is active
|
|
152
|
+
and suggests `hlsync PROFILE COMMAND`. Commands that require a profile need
|
|
153
|
+
that explicit profile prefix. Named and inferred lookups both require
|
|
154
|
+
`--details` for the full view.
|
|
155
|
+
|
|
156
|
+
Because `root` writes only the canonical path, it can be composed with the
|
|
157
|
+
shell when you want to switch directories:
|
|
158
|
+
|
|
159
|
+
```console
|
|
160
|
+
cd "$(hlsync root staging)"
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
An unqualified profile-aware command fails outside every mapped root. When you
|
|
164
|
+
intentionally need to operate from elsewhere—or override the profile inferred
|
|
165
|
+
from the current directory—put the profile before the command. The override
|
|
166
|
+
lasts for that command only and uses the selected profile's local root as its
|
|
167
|
+
working directory:
|
|
168
|
+
|
|
169
|
+
```console
|
|
170
|
+
hlsync staging list
|
|
171
|
+
hlsync staging diff templates
|
|
172
|
+
hlsync staging push templates -r
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
No profile selection is persisted between commands.
|
|
176
|
+
|
|
177
|
+
## Synchronization rules
|
|
178
|
+
|
|
179
|
+
HLSync automatically honors `.gitignore` files as local exclusions. When the
|
|
180
|
+
mapped local root is inside a Git repository, this includes all ancestor
|
|
181
|
+
`.gitignore` files up to the nearest repository root, plus files inside the
|
|
182
|
+
mapped tree. Without an enclosing repository, ignore scope starts at the mapped
|
|
183
|
+
root. Git worktrees are supported; no Git installation is needed.
|
|
184
|
+
Explicit HLSync rules override this baseline; profile rules override global rules.
|
|
185
|
+
To deploy a Git-ignored path, use `hlsync include vendor/` (or a specific file).
|
|
186
|
+
Ignore files above the repository root, Git's global excludes, and the Git index
|
|
187
|
+
are not consulted; matching tracked files are excluded too.
|
|
188
|
+
|
|
189
|
+
**Review `hlsync push --dry -p` before pruning:** with `-p`, locally excluded
|
|
190
|
+
files that are already deployed are deleted remotely. Without `-p`, their remote
|
|
191
|
+
copies are left alone; remote exclusion rules protect paths even when pruning.
|
|
192
|
+
|
|
193
|
+
Global rules live in `~/.hlsync/rules.json`, apply to every profile, and are
|
|
194
|
+
created with default exclusions for version-control metadata and common logs:
|
|
195
|
+
|
|
196
|
+
```text
|
|
197
|
+
**/.git/**
|
|
198
|
+
**/.svn/**
|
|
199
|
+
**/.hg/**
|
|
200
|
+
**/.DS_Store
|
|
201
|
+
**/Thumbs.db
|
|
202
|
+
**/desktop.ini
|
|
203
|
+
**/.gitignore
|
|
204
|
+
**/.gitmodules
|
|
205
|
+
**/error_log
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Manage global exclusions and inclusions from any directory with `-g` /
|
|
209
|
+
`--global`. Global operands are reusable patterns rooted at every profile's
|
|
210
|
+
local root; append `/` to target a complete directory tree:
|
|
211
|
+
|
|
212
|
+
```console
|
|
213
|
+
hlsync exclude -g --pattern '*.tmp'
|
|
214
|
+
hlsync include -g --pattern 'public/*.tmp'
|
|
215
|
+
hlsync rules -g # inspect global rules
|
|
216
|
+
hlsync rules rm g4 # g prefix selects global rules
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Global rules apply first and profile rules apply afterward, so an ordinary
|
|
220
|
+
profile inclusion can override a global exclusion. `hlsync rules` merges both
|
|
221
|
+
layers into one folder-grouped view, placing matching profile rules after
|
|
222
|
+
global rules. Global rules have `g`-prefixed IDs such as `g4`; profile rule IDs
|
|
223
|
+
remain numeric. IDs identify only current rules and may be reused after a rule
|
|
224
|
+
is removed. HLSync never silently adds new defaults to an existing global
|
|
225
|
+
rules file.
|
|
226
|
+
|
|
227
|
+
Exclude current paths permanently:
|
|
228
|
+
|
|
229
|
+
```console
|
|
230
|
+
hlsync exclude .git node_modules composer.json composer.lock
|
|
231
|
+
hlsync exclude '*.md'
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Normal wildcard operands are expanded against the current local tree and
|
|
235
|
+
recorded as exact paths. Quote them so the shell does not reject or alter them;
|
|
236
|
+
quoted and shell-expanded matches produce the same path rules.
|
|
237
|
+
|
|
238
|
+
Use `--pattern` when future matching paths should also be covered:
|
|
239
|
+
|
|
240
|
+
```console
|
|
241
|
+
hlsync exclude --pattern '*.md' # this directory only
|
|
242
|
+
hlsync exclude --pattern '**/*.log' # every directory below this point
|
|
243
|
+
hlsync include --pattern 'vendor/**' # re-include a subtree
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Use `--anywhere` (or `--any`) to match a name at every depth, including future
|
|
247
|
+
files. It works with both `exclude` and `include`, and replaces `--pattern`:
|
|
248
|
+
|
|
249
|
+
```console
|
|
250
|
+
hlsync exclude --anywhere filename.ext # current directory and descendants
|
|
251
|
+
hlsync exclude --any '*.log' # quote wildcards to preserve the pattern
|
|
252
|
+
hlsync exclude -g --anywhere cache/ # matching directory trees in every profile
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
`*` matches within one path segment; a complete `**` segment crosses directory
|
|
256
|
+
levels. Patterns are rooted where the command runs. HLSync rules are not
|
|
257
|
+
Gitignore syntax: `!`, `?`, bracket patterns, absolute paths, parent traversal,
|
|
258
|
+
and partial-segment `**` are rejected.
|
|
259
|
+
|
|
260
|
+
Local rules define the authoritative local set. Remote rules instead protect
|
|
261
|
+
server-side paths from synchronization:
|
|
262
|
+
|
|
263
|
+
```console
|
|
264
|
+
hlsync exclude --remote subdomains
|
|
265
|
+
hlsync include --remote subdomains
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Remote operands are declarative and never require a connection when recorded.
|
|
269
|
+
`subdomains` and `subdomains/` store the same exact boundary. During diff or
|
|
270
|
+
push, an existing remote file is left untouched; an existing remote directory
|
|
271
|
+
and everything beneath it are left untouched without traversing the directory.
|
|
272
|
+
A matching remote inclusion removes or overrides that boundary and returns the
|
|
273
|
+
path to normal push policy. Remote directory boundaries cannot be pierced by a
|
|
274
|
+
more specific child inclusion. `hlsync rules` groups the two rule targets under
|
|
275
|
+
`Local` and `Remote`.
|
|
276
|
+
|
|
277
|
+
Local rules keep the original compact JSON form. A missing stored `target`
|
|
278
|
+
means local; only remote rules add `"target": "remote"`.
|
|
279
|
+
|
|
280
|
+
Rules have stable IDs and the highest matching ID wins. HLSync removes provably
|
|
281
|
+
redundant exact rules but preserves ambiguous wildcard overlaps.
|
|
282
|
+
|
|
283
|
+
```console
|
|
284
|
+
hlsync rules
|
|
285
|
+
hlsync rules rm 3
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Multiple operands and comma-separated groups are accepted. A literal filename
|
|
289
|
+
containing a comma cannot be addressed because commas delimit groups.
|
|
290
|
+
|
|
291
|
+
## Paths and traversal
|
|
292
|
+
|
|
293
|
+
Paths are relative to the current directory inside the mapped root. Multiple
|
|
294
|
+
paths and wildcard patterns form one deterministic selection. Absolute paths,
|
|
295
|
+
parent traversal, and paths escaping the mapped root are rejected.
|
|
296
|
+
|
|
297
|
+
| Command | No path | Explicit directory | Depth option |
|
|
298
|
+
| --- | --- | --- | --- |
|
|
299
|
+
| `list` / `list --remote` | Current directory, one level | Directory contents, one level | `-r` includes descendants |
|
|
300
|
+
| `diff` | Complete current subtree | Complete directory subtree | `-s` limits to immediate contents |
|
|
301
|
+
| `push` | Complete current subtree | Complete directory subtree | `-s` limits to immediate contents |
|
|
302
|
+
| `pull` | Error: path required | Directory contents, one level | `-r` includes descendants |
|
|
303
|
+
|
|
304
|
+
Diff and push accept `-s` / `--shallow`; `-r` / `--recursive` remains available
|
|
305
|
+
to explicitly select the default. The two flags cannot be combined.
|
|
306
|
+
|
|
307
|
+
`.` explicitly selects the current directory. An immediate child directory
|
|
308
|
+
outside the traversal depth appears with `▸` but is diagnostic-only: it is not
|
|
309
|
+
created, replaced, deleted, or entered. The selected directory itself may be
|
|
310
|
+
created when selected files require it as their parent. An explicitly selected
|
|
311
|
+
remote-only directory is the deletion target itself, so recursive push
|
|
312
|
+
enumerates that subtree and removes its contents deepest-first. With `-s`,
|
|
313
|
+
deeper folders and their deletion ancestors are retained.
|
|
314
|
+
|
|
315
|
+
## Local and remote listing
|
|
316
|
+
|
|
317
|
+
`list` reads only the local filesystem, includes dotfiles, and applies the
|
|
318
|
+
configured rules:
|
|
319
|
+
|
|
320
|
+
```console
|
|
321
|
+
hlsync list
|
|
322
|
+
hlsync list templates
|
|
323
|
+
hlsync list '*.md'
|
|
324
|
+
hlsync list -r
|
|
325
|
+
hlsync list -r -i
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Use `--remote`—or the `lsr` shorthand—to inspect the equivalent FTPS
|
|
329
|
+
tree with the same operands and traversal controls:
|
|
330
|
+
|
|
331
|
+
```console
|
|
332
|
+
hlsync list --remote
|
|
333
|
+
hlsync list --remote templates -r
|
|
334
|
+
hlsync lsr
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Remote listing connects read-only. Remote-excluded paths use `#` and their
|
|
338
|
+
directories are shown as boundaries without being entered.
|
|
339
|
+
|
|
340
|
+
Directories appear before files at each level, with each group sorted by name.
|
|
341
|
+
Directories end in `/`; local exclusions use `x`, remote exclusions use `#`.
|
|
342
|
+
Single-side listings do not report whether the other side has a copy. `-i`, `--inc`, and
|
|
343
|
+
`--included-only` hide excluded paths. `hlsync ls` remains an unadvertised
|
|
344
|
+
compatibility spelling.
|
|
345
|
+
|
|
346
|
+
## Diff
|
|
347
|
+
|
|
348
|
+
`diff` is read-only and shows what the corresponding push would do:
|
|
349
|
+
|
|
350
|
+
```console
|
|
351
|
+
hlsync diff
|
|
352
|
+
hlsync diff templates
|
|
353
|
+
hlsync diff -s
|
|
354
|
+
hlsync diff templates --shallow
|
|
355
|
+
hlsync diff index.html app.js styles.css
|
|
356
|
+
hlsync diff '**/*.css'
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Local path existence decides what is uploaded or replaced. In the default
|
|
360
|
+
push view, remote-only paths are retained because push never deletes without
|
|
361
|
+
`-p`. Preview a pruning push with:
|
|
362
|
+
|
|
363
|
+
```console
|
|
364
|
+
hlsync diff --prune
|
|
365
|
+
hlsync diff -p
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
`--pull` changes the perspective for changed existing files and retains
|
|
369
|
+
remote-only paths.
|
|
370
|
+
|
|
371
|
+
By default, diff shows only what the matching push or pull would do: uploads,
|
|
372
|
+
updates, deletions, and conflicts, under their folder headings. A final
|
|
373
|
+
`Not shown:` line counts what was hidden. Widen the view with:
|
|
374
|
+
|
|
375
|
+
```console
|
|
376
|
+
hlsync diff -x # also show excluded paths (--excluded)
|
|
377
|
+
hlsync diff -a # show everything, including unchanged and kept paths (--all)
|
|
378
|
+
```
|
|
379
|
+
Recursive diff keeps file-browser order in both views: directories and their
|
|
380
|
+
indented contents appear before files at the parent level.
|
|
381
|
+
Rows without status markers retain blank status columns so names stay aligned.
|
|
382
|
+
Status columns stay fixed at the left; tree indentation applies to path names.
|
|
383
|
+
The status has three slots: action (`+`, `~`, `-`, `?`, `=`), exclusion
|
|
384
|
+
(`x` local, `#` remote), and presence (`l`, `r`, or blank for both sides), so
|
|
385
|
+
`-x` marks an excluded path that `--prune` would delete.
|
|
386
|
+
The `▸` marker means contents were not inspected, including excluded folders.
|
|
387
|
+
Diff reads directories as it traverses the tree and groups their entries under
|
|
388
|
+
folder headings, without separate per-directory read announcements.
|
|
389
|
+
|
|
390
|
+
For pruning, a locally excluded file is treated as absent. If it exists
|
|
391
|
+
remotely, default diff marks it ` x` and retains it; `diff -p` marks its
|
|
392
|
+
deletion as `-x`. The presence column still reports the actual copies:
|
|
393
|
+
blank when both exist, `r` when only the remote copy exists. An excluded
|
|
394
|
+
directory is a traversal boundary and is retained with `x`. An explicit local
|
|
395
|
+
inclusion for a descendant permits traversal;
|
|
396
|
+
remote exclusions remain hard boundaries. Hiding exclusions never hides a
|
|
397
|
+
deletion: with `-p`, an excluded path that would be deleted always shows as `-x`.
|
|
398
|
+
|
|
399
|
+
Diff enters eligible child folders by default. `diff -s` or
|
|
400
|
+
`diff templates --shallow` reads only immediate contents. Unentered child
|
|
401
|
+
folders retain the `▸` marker and are retained, matching `push -s`. Unentered
|
|
402
|
+
folders appear by default only when they could still hold changes; an
|
|
403
|
+
unentered remote-only folder appears as ` r folder/ ▸` with `-a`. Without
|
|
404
|
+
`-p`, diff does not enter remote-only folders, since push would neither change
|
|
405
|
+
nor delete their contents.
|
|
406
|
+
|
|
407
|
+
Show the current status and directory notation without connecting:
|
|
408
|
+
|
|
409
|
+
```console
|
|
410
|
+
hlsync --legend
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
Diff prints each directory as it is compared. For a shell-driven review,
|
|
414
|
+
`--paged` prints one directory, exits, and provides the exact stateless
|
|
415
|
+
`--resume` command for the next deterministic directory.
|
|
416
|
+
|
|
417
|
+
Colors are automatic on terminals, disabled for pipes and redirection, and
|
|
418
|
+
suppressed when `NO_COLOR` is set. Text markers retain the core meaning without
|
|
419
|
+
color. The left column reports action or status: `x` means locally excluded
|
|
420
|
+
and `#` means remotely excluded. The right column reports presence: `l` means
|
|
421
|
+
local only, `r` means remote only, and a blank means both sides. For example,
|
|
422
|
+
`x notes/ ▸` exists on both sides but is locally excluded, while
|
|
423
|
+
`# r millionminds/ ▸` exists only remotely and is remotely excluded. Exclusion
|
|
424
|
+
source never changes the presence column.
|
|
425
|
+
|
|
426
|
+
## Push and pull
|
|
427
|
+
|
|
428
|
+
Push local changes or replace explicitly selected existing local files from the
|
|
429
|
+
remote side:
|
|
430
|
+
|
|
431
|
+
```console
|
|
432
|
+
hlsync push
|
|
433
|
+
hlsync push templates
|
|
434
|
+
hlsync push templates -r
|
|
435
|
+
hlsync pull index.html
|
|
436
|
+
hlsync pull templates -r
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
Push uploads local-only files and replaces changed remote files. Pull replaces
|
|
440
|
+
changed existing local files but never restores a missing local path. A changed
|
|
441
|
+
file is replaced only when the source copy is strictly newer than the
|
|
442
|
+
destination. If the destination is newer or the same age (for example, a file
|
|
443
|
+
edited on the server after your last push), diff shows `?` and push or pull
|
|
444
|
+
stops before changing anything, listing the conflicting paths. Use `--force` to
|
|
445
|
+
overwrite them, or select other paths. Because HLSync copies timestamps on every
|
|
446
|
+
transfer, a later edit on either side makes that side newer; the check assumes
|
|
447
|
+
the local and server clocks roughly agree. Locally
|
|
448
|
+
excluded paths are never uploaded or pulled; remote-excluded paths are not
|
|
449
|
+
traversed or changed. Push never deletes unless `-p` / `--prune` is given;
|
|
450
|
+
then it deletes selected remote-only paths, including remote copies of locally
|
|
451
|
+
excluded files. Excluded directories on
|
|
452
|
+
either side are retained and never entered; an explicit local inclusion beneath
|
|
453
|
+
an excluded local directory is the only reason to enter that local boundary.
|
|
454
|
+
Push recurses into selected directories by default, including remote-only
|
|
455
|
+
deletion targets when pruning. `-s` /
|
|
456
|
+
`--shallow` processes immediate contents without entering child folders,
|
|
457
|
+
creating them, or deleting them. A selected remote-only directory containing
|
|
458
|
+
an unentered child folder is also retained.
|
|
459
|
+
|
|
460
|
+
Preview the exact push scope and operation order without changing either side:
|
|
461
|
+
|
|
462
|
+
```console
|
|
463
|
+
hlsync push --dry
|
|
464
|
+
hlsync push templates --dry
|
|
465
|
+
hlsync push templates -s --dry
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
Both `diff` and `push --dry` inherit push's recursive default. Dry push
|
|
469
|
+
honors `-s`, `-p`, remote exclusions, and explicit directory scope exactly as a
|
|
470
|
+
real push would. Interrupted-upload recovery is projected and reported but not
|
|
471
|
+
performed. After planning, dry push runs the live transfer executor and skips
|
|
472
|
+
only its mutation calls, preserving preflight, operation order, feedback, and
|
|
473
|
+
counts.
|
|
474
|
+
|
|
475
|
+
Dry and live push identify each local and remote directory as they scan it. A
|
|
476
|
+
parent is fully classified before HLSync enters eligible children, and excluded
|
|
477
|
+
directories are never entered. Dry-run plans use diff's compact colored `+`,
|
|
478
|
+
`~`, and `-` action markers under an explicit dry-push heading.
|
|
479
|
+
|
|
480
|
+
Live transfers print the colored `+`, `~`, or `-` action and path after each
|
|
481
|
+
operation succeeds. Failures and skips appear immediately; the final summary
|
|
482
|
+
contains counts without repeating errors. Dry runs use the same feedback to
|
|
483
|
+
show planned operations without performing them.
|
|
484
|
+
When no operation is needed, HLSync prints `Nothing to push` or `Nothing to
|
|
485
|
+
pull` without announcing an empty transfer phase. An empty push also reports
|
|
486
|
+
how many included files are up to date in the selected scope. Push summaries
|
|
487
|
+
report the outcome without a retained-path reminder.
|
|
488
|
+
|
|
489
|
+
After scanning, push shows the planned upload count and total size. Interactive
|
|
490
|
+
terminals show remote reads as completed/discovered directories (such as 2/12)
|
|
491
|
+
and the current path, without animation. The discovered total grows as eligible
|
|
492
|
+
subdirectories are found. During uploads, interactive
|
|
493
|
+
terminals display a progress bar with bytes sent and files installed. File
|
|
494
|
+
counts advance only after timestamp verification and installation; failures
|
|
495
|
+
remain visible immediately. Redirected output keeps ordinary per-file logs.
|
|
496
|
+
Dry runs show planned totals without simulating byte progress.
|
|
497
|
+
|
|
498
|
+
Delete selected remote-only paths explicitly with:
|
|
499
|
+
|
|
500
|
+
```console
|
|
501
|
+
hlsync push --prune
|
|
502
|
+
hlsync push -p 'generated/*.html'
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
Deletion is limited to the selected scope, runs only after every upload
|
|
506
|
+
succeeds, and is suppressed after any upload failure. Pull never deletes remote
|
|
507
|
+
paths.
|
|
508
|
+
|
|
509
|
+
### Safe file replacement
|
|
510
|
+
|
|
511
|
+
Remote ownership and permissions are controlled by the server. HLSync does not
|
|
512
|
+
apply local modes or issue remote chmod/chown commands. Staging files are created
|
|
513
|
+
beside their destinations and retain server-assigned permissions when installed.
|
|
514
|
+
For shared writable app-data trees (for example, `ftps-deploy` and `www-data`),
|
|
515
|
+
configure setgid directories and an appropriate umask or default ACL to create
|
|
516
|
+
`664` files and `2775` directories. Setgid alone does not grant group write;
|
|
517
|
+
replacement files receive the staging file's mode, not the old file's mode.
|
|
518
|
+
|
|
519
|
+
HLSync never uploads directly over a live destination. A direct FTP upload can
|
|
520
|
+
truncate or expose a partial live file when the connection fails. Instead,
|
|
521
|
+
HLSync:
|
|
522
|
+
|
|
523
|
+
1. Uploads to a uniquely named staging file beside the destination.
|
|
524
|
+
2. Verifies its size.
|
|
525
|
+
3. Applies the local whole-second UTC timestamp with MFMT (writable MDTM on
|
|
526
|
+
vsFTPd) and independently
|
|
527
|
+
reads it back with MDTM.
|
|
528
|
+
4. Moves the existing destination to a temporary backup.
|
|
529
|
+
5. Renames the verified staging file into place, then removes the backup.
|
|
530
|
+
|
|
531
|
+
This makes each file replacement recoverable, not the complete project
|
|
532
|
+
transactional—FTPS has no project-wide transaction. A later failure does not
|
|
533
|
+
roll back files already installed.
|
|
534
|
+
|
|
535
|
+
During a push, HLSync recovers its exact reserved artifacts as each selected
|
|
536
|
+
remote directory is read. It does not perform a separate recursive cleanup
|
|
537
|
+
scan. Abandoned upload files are deleted; obsolete backups are deleted when
|
|
538
|
+
their destination exists; a sole backup is restored when its destination is
|
|
539
|
+
missing. This cleanup is independent of remote-only deletion policy and does
|
|
540
|
+
not affect ordinary remote-only files. Concurrent pushes to one profile are not
|
|
541
|
+
supported.
|
|
542
|
+
|
|
543
|
+
A path-scoped permission failure skips that path or unwritable subtree while
|
|
544
|
+
independent paths continue. The command exits nonzero and suppresses all
|
|
545
|
+
pruning. Type or symlink conflicts, connection failures, and failed replacement
|
|
546
|
+
recovery on selected paths stop the operation when continued state cannot be
|
|
547
|
+
trusted.
|
|
548
|
+
|
|
549
|
+
## Command behavior
|
|
550
|
+
|
|
551
|
+
Commands accept the shortest unique prefix. Exact names win. An ambiguous
|
|
552
|
+
prefix prompts for a numbered choice on an interactive terminal and fails with
|
|
553
|
+
the candidate list in noninteractive use.
|
|
554
|
+
|
|
555
|
+
Use `hlsync help [command]`, `hlsync version`, and `hlsync --legend` for
|
|
556
|
+
built-in reference. `hlsync v`, `hlsync -v`, and `hlsync --version` also print
|
|
557
|
+
the installed version.
|
|
558
|
+
|
|
559
|
+
## License
|
|
560
|
+
|
|
561
|
+
HLSync is available under the [MIT License](LICENSE) for personal and commercial
|
|
562
|
+
use. A future voluntary Business subscription may fund development and provide
|
|
563
|
+
support or services; it is not required for commercial use.
|
|
564
|
+
|
|
565
|
+
## Development and maintenance
|
|
566
|
+
|
|
567
|
+
Install the development environment and run the test suite:
|
|
568
|
+
|
|
569
|
+
```console
|
|
570
|
+
python -m pip install -e '.[dev]'
|
|
571
|
+
pytest
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
To try local changes using your installed `hlsync` command, run this from the
|
|
575
|
+
repository root:
|
|
576
|
+
|
|
577
|
+
```console
|
|
578
|
+
uv tool install --force --reinstall .
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
This replaces the installed tool with a build of your current checkout. Rerun
|
|
582
|
+
it after making changes. Use `hlsync --legend` for a quick display check, or
|
|
583
|
+
run `hlsync diff` from a mapped project to preview its differences.
|
|
584
|
+
|
|
585
|
+
The PyPI distribution, installed command, and Python package are all `hlsync`.
|
|
586
|
+
See [`TODO.md`](TODO.md) for the ordered work queue and [`CHANGELOG.md`](CHANGELOG.md) for completed changes.
|
|
587
|
+
[`decisions/`](decisions/) holds substantial architectural choices and their
|
|
588
|
+
rationale. CLI conventions and current usage are documented here; smaller
|
|
589
|
+
changes are explained in commit messages.
|
|
590
|
+
|
|
591
|
+
The self-contained PHP 8.3 project site is in [`website/`](website/README.md).
|
|
592
|
+
|
|
593
|
+
### Releasing to PyPI
|
|
594
|
+
|
|
595
|
+
Releases use `<major>.<work-day>.<increment>` without leading zeroes. Versions
|
|
596
|
+
come from Git history, not the build date or manually maintained counters:
|
|
597
|
+
|
|
598
|
+
- [`version.json`](version.json) selects the major number and its baseline
|
|
599
|
+
commit. Version 1 starts at `b309f3e` ("done with mvp", August 22, 2026).
|
|
600
|
+
- The baseline day is `0`. Each subsequent date with commits advances the
|
|
601
|
+
work-day number by one, regardless of how many idle days pass.
|
|
602
|
+
- The increment counts commits on that date, starting at `1`. On the baseline
|
|
603
|
+
day, only the baseline commit and later commits count.
|
|
604
|
+
- Dates use each commit's recorded committer date and timezone. Only the
|
|
605
|
+
first-parent history of the current checkout counts; a merge counts once.
|
|
606
|
+
All commits count, including documentation and maintenance changes.
|
|
607
|
+
|
|
608
|
+
For example, two commits on the baseline day are `1.0.1` and `1.0.2`.
|
|
609
|
+
The next day with a commit produces `1.1.1`, even after a week without changes.
|
|
610
|
+
The September 30 commit `778ba7d` is `1.13.9` under this scheme.
|
|
611
|
+
|
|
612
|
+
Builds require history back to the baseline and reject backwards-moving commit
|
|
613
|
+
dates. Wheels and source archives preserve the computed version; installed
|
|
614
|
+
commands do not access Git. Reinstall editable checkouts after committing to
|
|
615
|
+
refresh their installed version. To start a new major, update `version.json`
|
|
616
|
+
with the new major and an existing baseline commit on the same first-parent
|
|
617
|
+
history. That baseline starts at `<major>.0.1`.
|
|
618
|
+
|
|
619
|
+
Use an existing PyPI account with two-factor authentication and an API token.
|
|
620
|
+
Twine uses credentials from `~/.pypirc` or `TWINE_USERNAME`/`TWINE_PASSWORD`,
|
|
621
|
+
and prompts if needed. Do not store the token in the repository.
|
|
622
|
+
|
|
623
|
+
1. Commit the changes to release. Use a checkout with history back to the
|
|
624
|
+
baseline in `version.json`. Do not edit the generated `_version.py`.
|
|
625
|
+
2. Update [`CHANGELOG.md`](CHANGELOG.md) when useful. Release notes and dated
|
|
626
|
+
headings are optional and do not block publishing.
|
|
627
|
+
3. Commit any release-note changes. From that clean checkout, with development
|
|
628
|
+
dependencies installed, run:
|
|
629
|
+
|
|
630
|
+
```console
|
|
631
|
+
./scripts/publish.sh
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
The script uses the repository's `.venv/bin/python` when available, otherwise
|
|
635
|
+
`python3`; set `PYTHON` to select another interpreter. It builds through
|
|
636
|
+
helpers in `scripts/helpers/`, which run identity checks, lint, tests, isolated builds,
|
|
637
|
+
metadata checks, and a clean wheel installation, then uploads both artifacts.
|
|
638
|
+
If `dist/hlsync/<version>/` already exists, it reuses those prepared artifacts after
|
|
639
|
+
checking their metadata. New commits advance the version;
|
|
640
|
+
existing artifacts are not rebuilt automatically.
|
|
641
|
+
|
|
642
|
+
To prepare artifacts without publishing, run `./scripts/publish.sh --prepare`.
|
|
643
|
+
|
|
644
|
+
4. A Git tag is not required by PyPI. It is strongly recommended for source
|
|
645
|
+
provenance: tag the release commit as `v<version>`, push that tag, and create
|
|
646
|
+
a corresponding GitHub Release.
|
|
647
|
+
5. Install the published version in a clean environment and run `hlsync --version`
|
|
648
|
+
before announcing it.
|
|
649
|
+
|
|
650
|
+
PyPI does not allow a published version to be replaced. If publication fails
|
|
651
|
+
after accepting either artifact, create another commit (an empty release-retry
|
|
652
|
+
commit is sufficient) and build a new release; do not reuse the failed version.
|
|
653
|
+
Publish from one release history: separate branches can compute the same
|
|
654
|
+
version and are not separate publication streams.
|