chezmoi-autosync 0.1.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.
- chezmoi_autosync-0.1.0/.github/workflows/publish.yml +84 -0
- chezmoi_autosync-0.1.0/.gitignore +13 -0
- chezmoi_autosync-0.1.0/LICENSE +21 -0
- chezmoi_autosync-0.1.0/PKG-INFO +202 -0
- chezmoi_autosync-0.1.0/README.md +177 -0
- chezmoi_autosync-0.1.0/justfile +50 -0
- chezmoi_autosync-0.1.0/pyproject.toml +51 -0
- chezmoi_autosync-0.1.0/src/chezmoi_autosync/__init__.py +3 -0
- chezmoi_autosync-0.1.0/src/chezmoi_autosync/cli.py +117 -0
- chezmoi_autosync-0.1.0/src/chezmoi_autosync/daemon.py +500 -0
- chezmoi_autosync-0.1.0/systemd/chezmoi-autosync.service +17 -0
- chezmoi_autosync-0.1.0/tests/__init__.py +0 -0
- chezmoi_autosync-0.1.0/tests/test_cli.py +302 -0
- chezmoi_autosync-0.1.0/tests/test_daemon.py +1050 -0
- chezmoi_autosync-0.1.0/uv.lock +224 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
# Publishes on a version tag (v*) using PyPI "trusted publishing" via OIDC —
|
|
4
|
+
# no PYPI_API_TOKEN secret is stored, and PEP 740 attestations (provenance) are
|
|
5
|
+
# attached automatically by pypa/gh-action-pypi-publish.
|
|
6
|
+
#
|
|
7
|
+
# One-time setup on pypi.org (the project can be registered as a "pending"
|
|
8
|
+
# publisher before it exists, and is created on first publish):
|
|
9
|
+
# pypi.org -> Account settings -> Publishing -> Add a pending publisher
|
|
10
|
+
# -> GitHub Actions:
|
|
11
|
+
# Owner: bootswithdefer
|
|
12
|
+
# Repository: chezmoi-autosync
|
|
13
|
+
# Workflow filename: publish.yml
|
|
14
|
+
# Environment name: pypi
|
|
15
|
+
# Then require manual approval on the "pypi" environment:
|
|
16
|
+
# repo Settings -> Environments -> pypi -> Required reviewers.
|
|
17
|
+
#
|
|
18
|
+
# Release flow:
|
|
19
|
+
# 1. bump "version" in pyproject.toml
|
|
20
|
+
# 2. git tag -a vX.Y.Z -m "vX.Y.Z" && git push origin vX.Y.Z
|
|
21
|
+
|
|
22
|
+
on:
|
|
23
|
+
push:
|
|
24
|
+
tags:
|
|
25
|
+
- "v*"
|
|
26
|
+
workflow_dispatch:
|
|
27
|
+
|
|
28
|
+
permissions:
|
|
29
|
+
contents: read
|
|
30
|
+
|
|
31
|
+
jobs:
|
|
32
|
+
build:
|
|
33
|
+
name: Build distribution
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
37
|
+
with:
|
|
38
|
+
persist-credentials: false
|
|
39
|
+
|
|
40
|
+
- name: Install uv
|
|
41
|
+
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
42
|
+
|
|
43
|
+
# Guard against publishing a tag whose version does not match the package
|
|
44
|
+
# metadata. Skipped on manual (workflow_dispatch) runs that are not tags.
|
|
45
|
+
- name: Verify tag matches pyproject.toml version
|
|
46
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
47
|
+
run: |
|
|
48
|
+
tag="${GITHUB_REF_NAME#v}"
|
|
49
|
+
pkg="$(uv version --short)"
|
|
50
|
+
if [ "$tag" != "$pkg" ]; then
|
|
51
|
+
echo "::error::Tag v$tag does not match pyproject.toml version $pkg"
|
|
52
|
+
exit 1
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
- name: Build sdist and wheel
|
|
56
|
+
run: uv build
|
|
57
|
+
|
|
58
|
+
- name: Store the distribution packages
|
|
59
|
+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
60
|
+
with:
|
|
61
|
+
name: python-package-distributions
|
|
62
|
+
path: dist/
|
|
63
|
+
|
|
64
|
+
publish-to-pypi:
|
|
65
|
+
name: Publish to PyPI
|
|
66
|
+
# Only publish from a version tag, never from a manual dispatch on a branch.
|
|
67
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
68
|
+
needs:
|
|
69
|
+
- build
|
|
70
|
+
runs-on: ubuntu-latest
|
|
71
|
+
environment:
|
|
72
|
+
name: pypi
|
|
73
|
+
url: https://pypi.org/p/chezmoi-autosync
|
|
74
|
+
permissions:
|
|
75
|
+
id-token: write # mandatory for OIDC trusted publishing
|
|
76
|
+
steps:
|
|
77
|
+
- name: Download the dists
|
|
78
|
+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
79
|
+
with:
|
|
80
|
+
name: python-package-distributions
|
|
81
|
+
path: dist/
|
|
82
|
+
|
|
83
|
+
- name: Publish distribution to PyPI
|
|
84
|
+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jesse DeFer
|
|
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,202 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: chezmoi-autosync
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Daemon that watches chezmoi source directory and auto-pushes changes to a hostname-based branch for review
|
|
5
|
+
Project-URL: Homepage, https://github.com/bootswithdefer/chezmoi-autosync
|
|
6
|
+
Project-URL: Repository, https://github.com/bootswithdefer/chezmoi-autosync
|
|
7
|
+
Project-URL: Issues, https://github.com/bootswithdefer/chezmoi-autosync/issues
|
|
8
|
+
Author: Jesse DeFer
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: chezmoi,daemon,dotfiles,sync
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: No Input/Output (Daemon)
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: System Administrators
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: System :: Systems Administration
|
|
21
|
+
Classifier: Topic :: Utilities
|
|
22
|
+
Requires-Python: >=3.12
|
|
23
|
+
Requires-Dist: watchdog<6,>=4.0.0
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# chezmoi-autosync
|
|
27
|
+
|
|
28
|
+
Daemon that watches your chezmoi-managed dotfiles for local edits and automatically pushes them to a hostname-based branch for review. Never lose a dotfile change because you forgot to commit.
|
|
29
|
+
|
|
30
|
+
## How it works
|
|
31
|
+
|
|
32
|
+
1. On startup, queries `chezmoi managed` to discover which files in `$HOME` chezmoi tracks
|
|
33
|
+
2. Watches those files for changes using inotify (via [watchdog](https://github.com/gorakhargosh/watchdog))
|
|
34
|
+
3. When a change is detected, debounces for a few seconds (coalesces rapid edits)
|
|
35
|
+
4. Runs `chezmoi re-add` to capture the change into the chezmoi source directory
|
|
36
|
+
5. Commits and pushes to a branch named `auto/<hostname>` on the remote
|
|
37
|
+
|
|
38
|
+
You then review and merge the branch at your convenience — the daemon never touches `main` or `master`.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
uv tool install chezmoi-autosync
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Or with pip:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install chezmoi-autosync
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Start with defaults (watches ~/.local/share/chezmoi, pushes to auto/<hostname>)
|
|
56
|
+
chezmoi-autosync
|
|
57
|
+
|
|
58
|
+
# Custom source directory
|
|
59
|
+
chezmoi-autosync --source-dir ~/dotfiles
|
|
60
|
+
|
|
61
|
+
# Custom branch name
|
|
62
|
+
chezmoi-autosync --branch auto/my-laptop
|
|
63
|
+
|
|
64
|
+
# Custom remote and debounce interval
|
|
65
|
+
chezmoi-autosync --remote upstream --debounce 10
|
|
66
|
+
|
|
67
|
+
# Perform a single sync-and-push cycle and exit (no watching)
|
|
68
|
+
chezmoi-autosync --once
|
|
69
|
+
|
|
70
|
+
# Preview what would be synced without re-adding, committing, or pushing
|
|
71
|
+
chezmoi-autosync --once --dry-run
|
|
72
|
+
|
|
73
|
+
# Debug logging
|
|
74
|
+
chezmoi-autosync -v
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Options
|
|
78
|
+
|
|
79
|
+
| Flag | Default | Description |
|
|
80
|
+
|------|---------|-------------|
|
|
81
|
+
| `--source-dir` | `~/.local/share/chezmoi` | Path to chezmoi source directory |
|
|
82
|
+
| `--remote` | `origin` | Git remote name |
|
|
83
|
+
| `--branch` | `auto/<hostname>` | Branch to push to |
|
|
84
|
+
| `--debounce` | `5.0` | Seconds to wait after last change before syncing |
|
|
85
|
+
| `--once` | off | Perform a single sync-and-push cycle and exit, without watching |
|
|
86
|
+
| `--dry-run` | off | Report what would be synced without re-adding, committing, or pushing |
|
|
87
|
+
| `-v` / `--verbose` | off | Enable debug logging |
|
|
88
|
+
|
|
89
|
+
## Systemd user service
|
|
90
|
+
|
|
91
|
+
A systemd user service unit is included for running chezmoi-autosync as a persistent background service.
|
|
92
|
+
|
|
93
|
+
### Install the service
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# Copy the unit file
|
|
97
|
+
mkdir -p ~/.config/systemd/user
|
|
98
|
+
cp systemd/chezmoi-autosync.service ~/.config/systemd/user/
|
|
99
|
+
|
|
100
|
+
# Or if installed via uv tool install, link from the package:
|
|
101
|
+
# The service file expects chezmoi-autosync to be in ~/.local/bin/
|
|
102
|
+
|
|
103
|
+
# Enable and start
|
|
104
|
+
systemctl --user daemon-reload
|
|
105
|
+
systemctl --user enable --now chezmoi-autosync
|
|
106
|
+
|
|
107
|
+
# Check status
|
|
108
|
+
systemctl --user status chezmoi-autosync
|
|
109
|
+
|
|
110
|
+
# View logs
|
|
111
|
+
journalctl --user -u chezmoi-autosync -f
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Override settings
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
systemctl --user edit chezmoi-autosync
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Then add overrides:
|
|
121
|
+
|
|
122
|
+
```ini
|
|
123
|
+
[Service]
|
|
124
|
+
ExecStart=
|
|
125
|
+
ExecStart=%h/.local/bin/chezmoi-autosync --debounce 10 --branch auto/work-laptop -v
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Branch safety
|
|
129
|
+
|
|
130
|
+
The daemon has multiple layers of protection against accidentally modifying `main` or `master`:
|
|
131
|
+
|
|
132
|
+
- **Startup validation** — refuses to start if the target branch is `main` or `master`
|
|
133
|
+
- **Pre-push validation** — checks the target branch name before every push
|
|
134
|
+
- **Commits never touch your checkout** — the daemon writes each snapshot directly onto the `auto/<hostname>` branch using git plumbing (`write-tree` in a temporary index, `commit-tree`, then `update-ref`). It never runs `git commit` against your current checkout and never moves `HEAD`, so the source repo can stay on `main` during normal operation. Your working index is left untouched (staging happens in a throwaway index).
|
|
135
|
+
- **Mergeable history** — each snapshot is parented so the `auto/<hostname>` branch stays a clean, mergeable delta against your base branch. The daemon builds on the previous auto-branch tip only while that tip is *ahead* of the base (stacked, un-merged snapshots); once the base branch moves forward — because you merged the auto branch, or committed dotfiles to `main` directly — the next snapshot **re-roots on the base branch tip** instead of stacking on a now-stale sibling. This keeps the branch reviewable and mergeable with a normal pull request and prevents it from going stale after a merge.
|
|
136
|
+
- **Explicit refspec** — pushes use `refs/heads/<branch>:refs/heads/<branch>` to be explicit about the target.
|
|
137
|
+
- **Force-push to auto branches** — the `auto/<hostname>` branch is a per-machine scratch branch. The local state is always authoritative, so the daemon force-pushes. This means if the remote branch diverges (e.g., from a prior session or a GitHub edit), the local version wins. This is intentional — the branch exists to capture the latest state of *this machine's* dotfiles.
|
|
138
|
+
|
|
139
|
+
If any safety check fails, the daemon logs a critical error but stays running — it will retry on the next file change once the issue is resolved.
|
|
140
|
+
|
|
141
|
+
## Error handling
|
|
142
|
+
|
|
143
|
+
The daemon is designed to stay alive through transient failures:
|
|
144
|
+
|
|
145
|
+
- **chezmoi re-add fails** — logged, sync skipped, retries on next change
|
|
146
|
+
- **git push fails** — logged (e.g., network down), retries on next change
|
|
147
|
+
- **Initial sync fails** — logged, daemon continues watching
|
|
148
|
+
- **Watch directory doesn't exist** — skipped with a warning
|
|
149
|
+
|
|
150
|
+
Only critical startup errors cause the daemon to exit:
|
|
151
|
+
- Source directory doesn't exist
|
|
152
|
+
- Source directory isn't a git repo
|
|
153
|
+
- Target branch is protected (`main`/`master`)
|
|
154
|
+
- chezmoi reports no managed files
|
|
155
|
+
|
|
156
|
+
## Development
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
# Clone
|
|
160
|
+
git clone https://github.com/bootswithdefer/chezmoi-autosync
|
|
161
|
+
cd chezmoi-autosync
|
|
162
|
+
|
|
163
|
+
# Install dev dependencies
|
|
164
|
+
uv sync --group dev
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Common tasks are wrapped in a [`justfile`](https://github.com/casey/just). Run `just` (or `just --list`) to see everything available:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
just test # run the test suite (uv run pytest)
|
|
171
|
+
just lint # ruff check
|
|
172
|
+
just fmt # ruff format --line-length 160
|
|
173
|
+
just fmt-check # ruff format --check
|
|
174
|
+
just typecheck # ty check
|
|
175
|
+
just check # lint + fmt-check + typecheck + test
|
|
176
|
+
|
|
177
|
+
# run the tool from the repo without installing
|
|
178
|
+
just watch # watch mode with debug logging
|
|
179
|
+
just once # single sync-and-push cycle
|
|
180
|
+
just preview # --once --dry-run (read-only)
|
|
181
|
+
|
|
182
|
+
# install as a user tool + systemd service
|
|
183
|
+
just install
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The task recipes accept pass-through arguments, e.g. `just once --branch auto/laptop` or `just test -k dry_run`.
|
|
187
|
+
|
|
188
|
+
If you don't have `just`, the underlying commands are plain `uv`/`uvx` invocations — see the `justfile` for the exact commands.
|
|
189
|
+
|
|
190
|
+
## Installing as a service
|
|
191
|
+
|
|
192
|
+
`just install` installs the tool with `uv tool install` (placing `chezmoi-autosync` in `~/.local/bin`), installs the systemd user unit, and enables + starts it:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
just install
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
This is equivalent to the manual steps in [Systemd user service](#systemd-user-service) above. After installing, manage it with the usual `systemctl --user` / `journalctl --user` commands.
|
|
199
|
+
|
|
200
|
+
## License
|
|
201
|
+
|
|
202
|
+
MIT
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# chezmoi-autosync
|
|
2
|
+
|
|
3
|
+
Daemon that watches your chezmoi-managed dotfiles for local edits and automatically pushes them to a hostname-based branch for review. Never lose a dotfile change because you forgot to commit.
|
|
4
|
+
|
|
5
|
+
## How it works
|
|
6
|
+
|
|
7
|
+
1. On startup, queries `chezmoi managed` to discover which files in `$HOME` chezmoi tracks
|
|
8
|
+
2. Watches those files for changes using inotify (via [watchdog](https://github.com/gorakhargosh/watchdog))
|
|
9
|
+
3. When a change is detected, debounces for a few seconds (coalesces rapid edits)
|
|
10
|
+
4. Runs `chezmoi re-add` to capture the change into the chezmoi source directory
|
|
11
|
+
5. Commits and pushes to a branch named `auto/<hostname>` on the remote
|
|
12
|
+
|
|
13
|
+
You then review and merge the branch at your convenience — the daemon never touches `main` or `master`.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
uv tool install chezmoi-autosync
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or with pip:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install chezmoi-autosync
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Start with defaults (watches ~/.local/share/chezmoi, pushes to auto/<hostname>)
|
|
31
|
+
chezmoi-autosync
|
|
32
|
+
|
|
33
|
+
# Custom source directory
|
|
34
|
+
chezmoi-autosync --source-dir ~/dotfiles
|
|
35
|
+
|
|
36
|
+
# Custom branch name
|
|
37
|
+
chezmoi-autosync --branch auto/my-laptop
|
|
38
|
+
|
|
39
|
+
# Custom remote and debounce interval
|
|
40
|
+
chezmoi-autosync --remote upstream --debounce 10
|
|
41
|
+
|
|
42
|
+
# Perform a single sync-and-push cycle and exit (no watching)
|
|
43
|
+
chezmoi-autosync --once
|
|
44
|
+
|
|
45
|
+
# Preview what would be synced without re-adding, committing, or pushing
|
|
46
|
+
chezmoi-autosync --once --dry-run
|
|
47
|
+
|
|
48
|
+
# Debug logging
|
|
49
|
+
chezmoi-autosync -v
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Options
|
|
53
|
+
|
|
54
|
+
| Flag | Default | Description |
|
|
55
|
+
|------|---------|-------------|
|
|
56
|
+
| `--source-dir` | `~/.local/share/chezmoi` | Path to chezmoi source directory |
|
|
57
|
+
| `--remote` | `origin` | Git remote name |
|
|
58
|
+
| `--branch` | `auto/<hostname>` | Branch to push to |
|
|
59
|
+
| `--debounce` | `5.0` | Seconds to wait after last change before syncing |
|
|
60
|
+
| `--once` | off | Perform a single sync-and-push cycle and exit, without watching |
|
|
61
|
+
| `--dry-run` | off | Report what would be synced without re-adding, committing, or pushing |
|
|
62
|
+
| `-v` / `--verbose` | off | Enable debug logging |
|
|
63
|
+
|
|
64
|
+
## Systemd user service
|
|
65
|
+
|
|
66
|
+
A systemd user service unit is included for running chezmoi-autosync as a persistent background service.
|
|
67
|
+
|
|
68
|
+
### Install the service
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Copy the unit file
|
|
72
|
+
mkdir -p ~/.config/systemd/user
|
|
73
|
+
cp systemd/chezmoi-autosync.service ~/.config/systemd/user/
|
|
74
|
+
|
|
75
|
+
# Or if installed via uv tool install, link from the package:
|
|
76
|
+
# The service file expects chezmoi-autosync to be in ~/.local/bin/
|
|
77
|
+
|
|
78
|
+
# Enable and start
|
|
79
|
+
systemctl --user daemon-reload
|
|
80
|
+
systemctl --user enable --now chezmoi-autosync
|
|
81
|
+
|
|
82
|
+
# Check status
|
|
83
|
+
systemctl --user status chezmoi-autosync
|
|
84
|
+
|
|
85
|
+
# View logs
|
|
86
|
+
journalctl --user -u chezmoi-autosync -f
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Override settings
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
systemctl --user edit chezmoi-autosync
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Then add overrides:
|
|
96
|
+
|
|
97
|
+
```ini
|
|
98
|
+
[Service]
|
|
99
|
+
ExecStart=
|
|
100
|
+
ExecStart=%h/.local/bin/chezmoi-autosync --debounce 10 --branch auto/work-laptop -v
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Branch safety
|
|
104
|
+
|
|
105
|
+
The daemon has multiple layers of protection against accidentally modifying `main` or `master`:
|
|
106
|
+
|
|
107
|
+
- **Startup validation** — refuses to start if the target branch is `main` or `master`
|
|
108
|
+
- **Pre-push validation** — checks the target branch name before every push
|
|
109
|
+
- **Commits never touch your checkout** — the daemon writes each snapshot directly onto the `auto/<hostname>` branch using git plumbing (`write-tree` in a temporary index, `commit-tree`, then `update-ref`). It never runs `git commit` against your current checkout and never moves `HEAD`, so the source repo can stay on `main` during normal operation. Your working index is left untouched (staging happens in a throwaway index).
|
|
110
|
+
- **Mergeable history** — each snapshot is parented so the `auto/<hostname>` branch stays a clean, mergeable delta against your base branch. The daemon builds on the previous auto-branch tip only while that tip is *ahead* of the base (stacked, un-merged snapshots); once the base branch moves forward — because you merged the auto branch, or committed dotfiles to `main` directly — the next snapshot **re-roots on the base branch tip** instead of stacking on a now-stale sibling. This keeps the branch reviewable and mergeable with a normal pull request and prevents it from going stale after a merge.
|
|
111
|
+
- **Explicit refspec** — pushes use `refs/heads/<branch>:refs/heads/<branch>` to be explicit about the target.
|
|
112
|
+
- **Force-push to auto branches** — the `auto/<hostname>` branch is a per-machine scratch branch. The local state is always authoritative, so the daemon force-pushes. This means if the remote branch diverges (e.g., from a prior session or a GitHub edit), the local version wins. This is intentional — the branch exists to capture the latest state of *this machine's* dotfiles.
|
|
113
|
+
|
|
114
|
+
If any safety check fails, the daemon logs a critical error but stays running — it will retry on the next file change once the issue is resolved.
|
|
115
|
+
|
|
116
|
+
## Error handling
|
|
117
|
+
|
|
118
|
+
The daemon is designed to stay alive through transient failures:
|
|
119
|
+
|
|
120
|
+
- **chezmoi re-add fails** — logged, sync skipped, retries on next change
|
|
121
|
+
- **git push fails** — logged (e.g., network down), retries on next change
|
|
122
|
+
- **Initial sync fails** — logged, daemon continues watching
|
|
123
|
+
- **Watch directory doesn't exist** — skipped with a warning
|
|
124
|
+
|
|
125
|
+
Only critical startup errors cause the daemon to exit:
|
|
126
|
+
- Source directory doesn't exist
|
|
127
|
+
- Source directory isn't a git repo
|
|
128
|
+
- Target branch is protected (`main`/`master`)
|
|
129
|
+
- chezmoi reports no managed files
|
|
130
|
+
|
|
131
|
+
## Development
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
# Clone
|
|
135
|
+
git clone https://github.com/bootswithdefer/chezmoi-autosync
|
|
136
|
+
cd chezmoi-autosync
|
|
137
|
+
|
|
138
|
+
# Install dev dependencies
|
|
139
|
+
uv sync --group dev
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Common tasks are wrapped in a [`justfile`](https://github.com/casey/just). Run `just` (or `just --list`) to see everything available:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
just test # run the test suite (uv run pytest)
|
|
146
|
+
just lint # ruff check
|
|
147
|
+
just fmt # ruff format --line-length 160
|
|
148
|
+
just fmt-check # ruff format --check
|
|
149
|
+
just typecheck # ty check
|
|
150
|
+
just check # lint + fmt-check + typecheck + test
|
|
151
|
+
|
|
152
|
+
# run the tool from the repo without installing
|
|
153
|
+
just watch # watch mode with debug logging
|
|
154
|
+
just once # single sync-and-push cycle
|
|
155
|
+
just preview # --once --dry-run (read-only)
|
|
156
|
+
|
|
157
|
+
# install as a user tool + systemd service
|
|
158
|
+
just install
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The task recipes accept pass-through arguments, e.g. `just once --branch auto/laptop` or `just test -k dry_run`.
|
|
162
|
+
|
|
163
|
+
If you don't have `just`, the underlying commands are plain `uv`/`uvx` invocations — see the `justfile` for the exact commands.
|
|
164
|
+
|
|
165
|
+
## Installing as a service
|
|
166
|
+
|
|
167
|
+
`just install` installs the tool with `uv tool install` (placing `chezmoi-autosync` in `~/.local/bin`), installs the systemd user unit, and enables + starts it:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
just install
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
This is equivalent to the manual steps in [Systemd user service](#systemd-user-service) above. After installing, manage it with the usual `systemctl --user` / `journalctl --user` commands.
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
MIT
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# chezmoi-autosync task runner
|
|
2
|
+
# Run `just` or `just --list` to see available recipes.
|
|
3
|
+
|
|
4
|
+
# Show available recipes
|
|
5
|
+
default:
|
|
6
|
+
@just --list
|
|
7
|
+
|
|
8
|
+
# Run the daemon (watch mode) with debug logging
|
|
9
|
+
watch *args:
|
|
10
|
+
uv run chezmoi-autosync -v {{ args }}
|
|
11
|
+
|
|
12
|
+
# Perform a single sync-and-push cycle and exit
|
|
13
|
+
once *args:
|
|
14
|
+
uv run chezmoi-autosync --once {{ args }}
|
|
15
|
+
|
|
16
|
+
# Preview what would be synced without re-adding, committing, or pushing
|
|
17
|
+
preview *args:
|
|
18
|
+
uv run chezmoi-autosync --once --dry-run {{ args }}
|
|
19
|
+
|
|
20
|
+
# Run the test suite
|
|
21
|
+
test *args:
|
|
22
|
+
uv run pytest {{ args }}
|
|
23
|
+
|
|
24
|
+
# Lint with ruff
|
|
25
|
+
lint:
|
|
26
|
+
uvx ruff check .
|
|
27
|
+
|
|
28
|
+
# Format with ruff
|
|
29
|
+
fmt:
|
|
30
|
+
uvx ruff format --line-length 160 .
|
|
31
|
+
|
|
32
|
+
# Check formatting without modifying files
|
|
33
|
+
fmt-check:
|
|
34
|
+
uvx ruff format --line-length 160 --check .
|
|
35
|
+
|
|
36
|
+
# Type-check with ty
|
|
37
|
+
typecheck:
|
|
38
|
+
uvx ty check
|
|
39
|
+
|
|
40
|
+
# Run all checks: lint, format check, type check, tests
|
|
41
|
+
check: lint fmt-check typecheck test
|
|
42
|
+
|
|
43
|
+
# Install as a user tool and enable the systemd user service
|
|
44
|
+
install:
|
|
45
|
+
uv tool install --force .
|
|
46
|
+
mkdir -p ~/.config/systemd/user
|
|
47
|
+
cp systemd/chezmoi-autosync.service ~/.config/systemd/user/
|
|
48
|
+
systemctl --user daemon-reload
|
|
49
|
+
systemctl --user enable --now chezmoi-autosync
|
|
50
|
+
systemctl --user status --no-pager chezmoi-autosync
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "chezmoi-autosync"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Daemon that watches chezmoi source directory and auto-pushes changes to a hostname-based branch for review"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.12"
|
|
12
|
+
authors = [{ name = "Jesse DeFer" }]
|
|
13
|
+
keywords = ["chezmoi", "dotfiles", "sync", "daemon"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Environment :: No Input/Output (Daemon)",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Intended Audience :: System Administrators",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Operating System :: POSIX :: Linux",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Topic :: System :: Systems Administration",
|
|
24
|
+
"Topic :: Utilities",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"watchdog>=4.0.0,<6",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/bootswithdefer/chezmoi-autosync"
|
|
32
|
+
Repository = "https://github.com/bootswithdefer/chezmoi-autosync"
|
|
33
|
+
Issues = "https://github.com/bootswithdefer/chezmoi-autosync/issues"
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
chezmoi-autosync = "chezmoi_autosync.cli:main"
|
|
37
|
+
|
|
38
|
+
[tool.ruff]
|
|
39
|
+
line-length = 160
|
|
40
|
+
|
|
41
|
+
[tool.ruff.lint]
|
|
42
|
+
select = ["E", "F", "W", "I", "UP", "B", "SIM", "RUF"]
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
testpaths = ["tests"]
|
|
46
|
+
|
|
47
|
+
[dependency-groups]
|
|
48
|
+
dev = [
|
|
49
|
+
"pytest>=8.0,<9",
|
|
50
|
+
"pytest-cov>=5.0,<7",
|
|
51
|
+
]
|