Skip to content

Commit ff2344a

Browse files
authored
feat(import) --sync / --prune flags, options.pin, CRDT action model (#520)
`vcspull import` silently skipped repos whose remote URL had changed, forcing users to delete their config and re-run from scratch. Repos removed from the remote stayed in config forever. This branch adds full bidirectional reconciliation to `vcspull import` and applies a consistent CRDT action model (pure classifier -> enum -> apply) across all five config-mutation operations. New flags: --sync Add new repos, update changed URLs, prune entries no longer on the remote (scoped by provenance tag). --prune Prune stale entries only (no URL updates). --prune-untracked Extend --sync/--prune to also remove entries that lack any import provenance (manually added repos or entries from before provenance tracking). Requires --sync or --prune. A confirmation prompt lists exactly what would be removed. Provenance tracking (metadata.imported_from): When --sync or --prune is used, each imported repo is tagged with "{service}:{target}" (e.g. "github:myorg"). Pruning is scoped by this tag -- only entries matching the current import source are candidates for removal. Manually added repos and repos from other sources are never pruned. Pruning is config-only; cloned directories on disk are not deleted. options.pin -- per-repo, per-operation mutation guard: pin: true block ALL operations pin: {import: true} block import only allow_overwrite: false shorthand for pin: {import: true} pin_reason is shown in log output when a repo is skipped. Pin awareness has been added to import, add, discover, fmt, and the duplicate-workspace-root merge resolver. CRDT action model: Every config-mutation command now follows the same three-step pattern: pure classifier -> enum action -> apply. import: ADD, SKIP_UNCHANGED, SKIP_EXISTING, UPDATE_URL, SKIP_PINNED, PRUNE add: ADD, SKIP_EXISTING, SKIP_PINNED discover: ADD, SKIP_EXISTING, SKIP_PINNED fmt: NORMALIZE, NO_CHANGE, SKIP_PINNED merge: KEEP_EXISTING, KEEP_INCOMING Bug fixes: vcspull add, discover, and fmt all called save_config_yaml() unconditionally, silently corrupting .json config files by overwriting them with YAML content. All save sites now check the file extension and call the correct serialiser.
2 parents bdfaa95 + 692ee06 commit ff2344a

26 files changed

Lines changed: 6096 additions & 105 deletions

CHANGES

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,104 @@ $ uvx --from 'vcspull' --prerelease allow vcspull
3333
_Notes on upcoming releases will be added here_
3434
<!-- END PLACEHOLDER - ADD NEW CHANGELOG ENTRIES BELOW THIS LINE -->
3535

36+
### New features
37+
38+
#### `vcspull import`: Add `--sync` and `--prune` (#520)
39+
40+
`--sync` fully reconciles your config with the remote: updates changed URLs,
41+
and prunes entries no longer on the remote. `--prune` does prune-only (no URL
42+
updates). Both respect pinned entries and use provenance tags
43+
(`metadata.imported_from`) to scope pruning — manually added repos are never
44+
removed.
45+
46+
```console
47+
$ vcspull import gh myorg \
48+
--mode org \
49+
--workspace ~/code/ \
50+
--sync
51+
```
52+
53+
Available on all six import providers. Pruning is config-only — cloned
54+
directories on disk are not deleted. See {ref}`cli-import` for details.
55+
56+
#### `vcspull import`: Add `--prune-untracked` flag (#520)
57+
58+
`--prune-untracked` expands `--sync` / `--prune` to also remove config
59+
entries in the target workspace that lack import provenance — repos added
60+
manually or before provenance tracking. Entries imported from other sources
61+
and pinned entries are preserved. A confirmation prompt lists exactly what
62+
would be removed.
63+
64+
```console
65+
$ vcspull import gh myorg \
66+
--workspace ~/code/ \
67+
--sync \
68+
--prune-untracked \
69+
--dry-run
70+
```
71+
72+
#### `options.pin`: Per-repo, per-operation mutation guard (#520)
73+
74+
Any repository entry can carry an `options.pin` block that prevents specific
75+
vcspull operations from mutating it. This is useful for pinned forks, company
76+
mirrors, or any repo whose config should only be changed manually.
77+
78+
Pin all operations:
79+
80+
```yaml
81+
~/code/:
82+
myrepo:
83+
repo: git+git@github.com:me/myrepo.git
84+
options:
85+
pin: true
86+
pin_reason: "pinned to company fork — update manually"
87+
```
88+
89+
Pin only specific operations:
90+
91+
```yaml
92+
~/code/:
93+
other-repo:
94+
repo: git+git@github.com:me/other.git
95+
options:
96+
pin:
97+
import: true # --sync cannot replace this URL
98+
fmt: true # vcspull fmt preserves the entry verbatim
99+
pin_reason: "pinned for import and fmt"
100+
```
101+
102+
Shorthand — equivalent to `pin: {import: true}`:
103+
104+
```yaml
105+
~/code/:
106+
shorthand-repo:
107+
repo: git+git@github.com:me/pinned.git
108+
options:
109+
allow_overwrite: false
110+
```
111+
112+
Pin semantics:
113+
114+
- `pin: true` — all five operations are blocked (`import`, `add`, `discover`,
115+
`fmt`, `merge`)
116+
- `pin: {op: true}` — only the named operations are blocked; unspecified keys
117+
default to `false`
118+
- `allow_overwrite: false` — concise shorthand, equivalent to
119+
`pin: {import: true}`
120+
- `pin_reason` — human-readable string shown in log output when a repo is
121+
skipped due to a pin
122+
123+
Pin awareness has been added to all config-mutation commands: `import`,
124+
`add`, `discover`, `fmt`, and the duplicate-workspace-root `merge` resolver.
125+
126+
### Bug fixes
127+
128+
#### `vcspull add`, `vcspull discover`, `vcspull fmt`: Fix JSON configs saved as YAML (#520)
129+
130+
All three commands called `save_config_yaml()` unconditionally, silently
131+
corrupting `.json` config files by overwriting them with YAML content. Each
132+
save site now checks the file extension and calls the correct serialiser.
133+
36134
## vcspull v1.57.0 (2026-02-22)
37135

38136
### Breaking changes

docs/cli/add.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,32 @@ repositories stay intact. When it collapses multiple sections, the command logs
109109
a summary of the merge. Prefer to inspect duplicates yourself? Add
110110
`--no-merge` to keep every section untouched.
111111

112+
## Pinned entries
113+
114+
Repositories whose configuration includes a pin on the `add` operation are
115+
skipped with a warning. For example, given this configuration:
116+
117+
```yaml
118+
~/code/:
119+
internal-fork:
120+
repo: "git+git@github.com:myorg/internal-fork.git"
121+
options:
122+
pin: true
123+
pin_reason: "pinned to company fork — update manually"
124+
```
125+
126+
Attempting to add a repo that matches an existing pinned entry produces a
127+
warning and leaves the entry untouched:
128+
129+
```vcspull-console
130+
$ vcspull add ~/code/internal-fork
131+
⚠ Repository 'internal-fork' is pinned (pinned to company fork — update manually) — skipping
132+
```
133+
134+
Both `options.pin: true` (global) and `options.pin.add: true` (per-operation)
135+
block the `add` command. The `pin_reason` (if set) is included in the warning.
136+
See {ref}`config-pin` for full pin configuration.
137+
112138
## After adding repositories
113139

114140
1. Run `vcspull fmt --write` to normalize your configuration (see

docs/cli/discover.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,36 @@ Skipped flask (already exists)
225225

226226
You can choose to skip or overwrite the existing entry.
227227

228+
## Pinned entries
229+
230+
Repositories pinned for the `discover` operation are silently skipped during
231+
scanning. For example, given this configuration:
232+
233+
```yaml
234+
~/code/:
235+
internal-fork:
236+
repo: "git+git@github.com:myorg/internal-fork.git"
237+
options:
238+
pin:
239+
discover: true
240+
pin_reason: "pinned to company fork — update manually"
241+
```
242+
243+
When `vcspull discover` encounters `internal-fork` on disk, it groups it with
244+
existing entries and does not prompt for it. A debug-level log message is
245+
emitted, so pass `--log-level debug` to see which entries were skipped due to
246+
pins:
247+
248+
```console
249+
$ vcspull discover ~/code \
250+
--recursive \
251+
--log-level debug
252+
```
253+
254+
Both `options.pin: true` (global) and `options.pin.discover: true`
255+
(per-operation) block discovery. See {ref}`config-pin` for full pin
256+
configuration.
257+
228258
## Migration from vcspull import --scan
229259

230260
If you previously used `vcspull import --scan`:

docs/cli/fmt.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,38 @@ The formatter performs four main tasks:
3232
- Consolidates duplicate workspace roots into a single merged section while
3333
logging any conflicts.
3434

35+
### Pinned entries
36+
37+
Repositories pinned for the `fmt` operation are preserved **verbatim** — no
38+
normalization, no key reordering, no string-to-dict expansion. For example,
39+
given this configuration:
40+
41+
```yaml
42+
~/code/:
43+
pinned-dep:
44+
url: git+https://corp.example.com/team/pinned-dep.git
45+
options:
46+
pin:
47+
fmt: true
48+
libvcs: git+https://github.com/vcspull/libvcs.git
49+
```
50+
51+
After `vcspull fmt --write`, only `libvcs` is normalized. The `pinned-dep` entry
52+
keeps its original `url` key and field order:
53+
54+
```yaml
55+
~/code/:
56+
libvcs:
57+
repo: git+https://github.com/vcspull/libvcs.git
58+
pinned-dep:
59+
url: git+https://corp.example.com/team/pinned-dep.git
60+
options:
61+
pin:
62+
fmt: true
63+
```
64+
65+
See {ref}`config-pin` for full pin configuration.
66+
3567
For example:
3668

3769
```yaml

0 commit comments

Comments
 (0)