Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
15c1442
git(fix): Preserve filtered remote URLs
tony Sep 20, 2026
4428b73
sync(fix): Report clone failures at source
tony Sep 20, 2026
1b5274b
svn(fix): Keep global options before targets
tony Sep 20, 2026
5954a35
sync(feat): Report working-copy positions
tony Sep 20, 2026
f3d24d3
svn(fix): Exclude externals from position
tony Sep 20, 2026
f31054f
git(feat[filter]): Add typed object filters
tony Sep 20, 2026
6a8a08b
docs(git): Document partial-clone filters
tony Sep 20, 2026
d8f1bd8
git(fix[filter]): Match native filter behavior
tony Sep 20, 2026
9ddac15
docs(git): Correct partial clone filter support
tony Sep 20, 2026
e9159f0
docs(git): Explain auto-filter clone boundary
tony Sep 20, 2026
06d2f4d
git(fix): Keep push and fetch URLs separate
tony Sep 20, 2026
1f30867
vcs(fix): Forward global command options correctly
tony Sep 20, 2026
5344ae5
vcs(feat): Forward sync transport options
tony Sep 20, 2026
48bd66f
sync(feat): Add typed backend options
tony Sep 20, 2026
99f2d6a
git(fix): Place configuration before commands
tony Sep 20, 2026
6c10249
git(fix): Initialize nested shallow submodules
tony Sep 20, 2026
a245910
sync(fix): Validate options before commands
tony Sep 20, 2026
683150c
git(fix): Flatten repeated submodule filters
tony Sep 20, 2026
ab0e9a2
sync(feat): Add recoverable operation contracts
tony Sep 20, 2026
b52f791
git(feat): Retain dirty checkout recovery
tony Sep 20, 2026
bdbf4a6
sync(fix): Validate retained recovery records
tony Sep 20, 2026
16f4dca
git(fix): Guard ignored files in submodules
tony Sep 20, 2026
dc5de3e
git(fix): Fetch pinned submodule commits
tony Sep 20, 2026
48ff0b6
git(fix): Apply fetch transport and remote options
tony Sep 20, 2026
cbc476f
sync(fix): Identify target drift warnings
tony Sep 20, 2026
299bc32
hg(fix): Honor clone transport and revision
tony Sep 20, 2026
1f6c895
hg(feat): Retain local changes during sync
tony Sep 20, 2026
5eaf79f
hg(fix): Retain overlapping schedule states
tony Sep 20, 2026
7b943d6
run(fix): Drain pipes without a timeout
tony Sep 20, 2026
5877b1f
subprocess(fix): Type existing run timeouts
tony Sep 20, 2026
40ab3af
preserve(feat): Own snapshots outside sources
tony Sep 20, 2026
8a5e6a3
svn(feat): Retain independent recovery copies
tony Sep 20, 2026
349affb
svn(fix): Keep failed absence inspection unknown
tony Sep 20, 2026
d1af93b
preserve(test): Kill native update processes
tony Sep 20, 2026
24d9659
hg(feat): Configure native pull and push paths
tony Sep 20, 2026
6548d41
hg(test): Fix remote integration expectations
tony Sep 20, 2026
643bdb9
git(fix): Apply targets to new kept checkouts
tony Sep 20, 2026
3762fb2
sync(docs): Explain policies and recovery
tony Sep 20, 2026
e907db3
sync(fix): Forward Mercurial factory remotes
tony Sep 20, 2026
16d6d54
run(fix): Reap children after callback failure
tony Sep 20, 2026
913719b
sync(feat): Resolve detached updates under lock
tony Sep 20, 2026
7dc7e4b
tests(fix): Isolate detached update remote
tony Sep 20, 2026
59bea49
sync(feat): Own linked worktree creation
tony Sep 20, 2026
27c44b1
ci(fix): Exercise declared VCS matrix
tony Sep 20, 2026
89357b6
docs(fix): Test versioned dataclass behavior
tony Sep 20, 2026
94d7f84
ci(test): Surface first-attempt failures
tony Sep 20, 2026
64254a7
docs(fix): Verify isolated worktree creation
tony Sep 20, 2026
7c10118
git(fix): Filter new submodules on update
tony Sep 20, 2026
62f7457
preserve(test): Reap native hook descendants
tony Sep 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ on:
jobs:
build:
runs-on: ubuntu-latest
env:
UV_PYTHON: ${{ matrix.python-version }}
strategy:
matrix:
python-version: ['3.10', '3.14']
Expand All @@ -27,8 +29,17 @@ jobs:
- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}

- name: Install native VCS tools
run: |
sudo apt-get update
sudo apt-get install --yes --no-install-recommends git mercurial subversion
git --version
hg --version
svn --version --quiet
svnadmin --version --quiet

- name: Install dependencies
run: uv sync --all-extras --dev
run: uv sync --frozen --all-extras --dev

- name: Lint with ruff check
run: uv run ruff check .
Expand All @@ -43,9 +54,10 @@ jobs:
run: |
python -V
uv run python -V
uv run python -c 'import os, sys; assert ".".join(map(str, sys.version_info[:2])) == os.environ["UV_PYTHON"]'

- name: Test with pytest
run: uv run py.test --cov=./ --cov-append --cov-report=xml
run: uv run py.test --no-showlocals --reruns 0 -ra -n 2 --dist worksteal --cov=./ --cov-append --cov-report=xml
env:
COV_CORE_SOURCE: .
COV_CORE_CONFIG: .coveragerc
Expand Down
171 changes: 171 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,177 @@ $ uv add libvcs --prerelease allow
_Notes on the upcoming release will go here._
<!-- END PLACEHOLDER - ADD NEW CHANGELOG ENTRIES BELOW THIS LINE -->

### What's new

#### Create linked Git worktrees under shared ownership

`GitSync.create_worktree()` fetches and creates an absent linked checkout at a
typed target while holding the repository ownership lock. It retains local
commits, rejects divergence and occupied destinations, and supports detached
checkouts and worktree lock reasons. Interrupted recovery records refuse
creation with their retained token; parent working files remain untouched.


`GitSync.update_repo(detach=True)` detaches at the resolved target after
fetching under the ownership lock. Branch targets retain their native
tracking-ref and local-branch fallback rules.

Progress callback exceptions and interrupts terminate and reap the direct
subprocess before propagating. Direct calls own the immediate child; callers
that need descendant cancellation must supervise an owned process group.

#### Subversion retains complete working copies for recovery

`SvnSync.update_repo()` aborts on dirty checkouts by default. Explicit
preservation uses native update or switch and retains a complete working-copy
copy, including schedules, binary properties, unknown files, and ignored files.
Missing paths stay absent only when their complete native and physical scope
is unchanged. Conflicts and failed inspection retain their recovery token.
`recover_changes()` creates independent copies offline; `list_recoveries()`
reports interrupted operations, and `release_changes()` releases verified copies.
Recovery and release work after the original checkout is deleted or replaced.

SVN targets accept `rev`; the configured repository URL controls switching.
Branch, tag, commit, and remote selectors are rejected. Preservation requires
POSIX and SVN working-copy format 31. It rejects externals, nested repositories,
and linked administrative storage. Callers must stop external editors and native
writers.

#### Mercurial remotes keep fetch and push separate

`create_project(vcs="hg", remotes=...)` forwards configured Mercurial paths
through the same factory used by configuration consumers.

`HgRemote` and `HgSync(remotes=...)` expose native `[paths]` aliases with
separate fetch and push URLs. `SyncTarget(remote=...)` chooses where to pull
without changing unrelated push destinations. Configured aliases update
atomically during following syncs; keep, warn, and dirty-abort preflight leave
native configuration unchanged. Comments and includes survive updates, and
equivalent relative or environment-expanded paths do not rewrite the file.
See {doc}`sync/hg`.

#### Shared checkout targets and recovery outcomes

`SyncTarget` selects one branch, tag, commit, or revision. `SyncPolicy` keeps
configured-target drift separate from local-change handling and defaults to
`dirty="abort"`. `SyncResult` now reports update and preservation outcomes,
conflicts, and retained recovery tokens independently.

#### Mercurial retains shelves for independent recovery

`HgSync.update_repo()` aborts on dirty checkouts by default. Explicit
`SyncPolicy(dirty="preserve")` retains native shelves in private storage,
including added, removed, modified, and unknown files. Missing files retain
their unscheduled state; upstream changes to a missing path report a conflict.
Native unshelve conflicts keep their recovery token and caller-owned backups.
`recover_changes()` restores the original branch, bookmark, and native status
in a separate checkout using retained local history, without remote access.
`list_recoveries()` exposes interrupted saves and damaged records;
`release_changes()` removes only verified owned shelf artifacts.

#### Mercurial clone applies transport and revision selection

`HgSync` strips the `hg+` adapter prefix before invoking Mercurial and applies
the constructor's `rev` when checking out a new clone.
An explicit `update_repo(target=...)` overrides that revision. A new checkout
starts at its configured target before keep or warn policy applies.

#### Git retains dirty changes for independent recovery

`GitSync.update_repo()` aborts on dirty checkouts by default. Explicit
`SyncPolicy(dirty="preserve")` retains staged, unstaged, and untracked changes
in an owned stash and applies them with the index after a fast-forward update.
Conflicts and failed updates retain a recovery token. `list_recoveries()`
finds interrupted saves; `recover_changes()` creates a separate original-base
checkout from local Git objects; `release_changes()` removes owned material.
Recovery requires those local objects and never fetches from the remote.
Malformed recovery records remain visible as errors carrying their token.
Ignored target collisions, divergent commits, active native operations,
dirty submodule scopes, and nested repositories fail before capture.
Clean recursive submodule updates reject ignored-file collisions in initialized
children before any checkout advances. Target resolution refreshes every
configured remote; clone and fetch honor command-scoped TLS configuration.

New Git checkouts follow the configured target before keep or warn policy
applies. Those policies continue to leave existing checkouts unchanged.

#### Git command configuration precedes subcommands

{meth}`~libvcs.cmd.git.Git.run` places global flags before the subcommand and
uses native `-c` arguments for command-scoped configuration. Sync operations
with `GitOptions(tls_verify=False)` now run clone, fetch, and submodule commands
without misreading the TLS setting as a repository argument.

#### Sync backends expose typed options

{class}`~libvcs.sync.git.GitOptions`, {class}`~libvcs.sync.hg.HgOptions`, and
{class}`~libvcs.sync.svn.SvnOptions` are frozen backend configuration objects.
Each sync class exposes its option class as `options_type` for dataclass
introspection. Constructors now reject unknown fields and option objects from
another backend.

Mercurial and Subversion string options reject NUL before command creation.
Git filter normalization preserves the nesting limit accepted by validation,
including when the immutable options object is passed to clone.

Git depth and filter settings apply to the initial clone and its submodules.
Submodule initialization follows nested gitlinks and forwards the history depth.
Git and Mercurial verify TLS certificates by default; setting `tls_verify` to
false forwards the native verification override to network operations.
Mercurial clone transport settings and Subversion authentication, certificate,
external, and ambient-depth settings now reach their native commands.

#### Typed Git filters validate partial clones before execution

{mod}`libvcs.cmd.git_filter` provides frozen models for Git's `blob:none`,
`blob:limit`, `tree`, `object:type`, `sparse:oid`, `auto`, and `combine`
filters. Specs and kind-tagged mappings parse into the same models, with
validation for numeric ranges, combine escaping, recursion, incompatible
`auto` filters, and field names.

{meth}`~libvcs.cmd.git.Git.clone`, {meth}`~libvcs.cmd.git.Git.fetch`, and
{meth}`~libvcs.cmd.git.GitSubmoduleCmd.update` accept models, strings,
mappings, and sequences through `_filter`. Clone and fetch preserve repeated
filter arguments; submodule update combines them so Git applies every filter.
{meth}`~libvcs.cmd.git.Git.pull` rejects filters because native Git does not
support them.

{class}`~libvcs.sync.git.GitOptions` accepts the same values through `filter`,
validates them before creating a checkout, and applies them to the initial
clone and its submodules. `Auto` can clone a repository without gitlinks. If
the cloned index contains gitlinks, obtain reports that `Auto` cannot be
forwarded and retains the parent clone.

#### Inspect working-copy positions

`GitSync.get_position()`, `HgSync.get_position()`, and
`SvnSync.get_position()` report the checkout's local revision and ref without
contacting a remote. Subversion also reports mixed revisions and switched
subtrees. See {doc}`sync/index` for an example.

### Fixes

#### Drain command output while the process runs

Commands with large stdout or stderr output complete without filling an
unread pipe. Both streams drain concurrently with or without a timeout,
and progress callbacks retain command diagnostics for failure reporting.

#### Keep partial-clone remotes usable

Git remote listing retains fetch URLs annotated with a partial-clone filter.
A failed `GitSync.obtain()` reports the clone command and Git's original
message before attempting submodule or remote setup.

#### Keep fetch and push destinations separate

`GitSync` configures a remote's push URL without replacing its fetch URL.

#### Keep Subversion options before command targets

`Svn.run()` places global options before command arguments, so an explicit
`--` separator does not turn `--non-interactive` into a filename.

## libvcs 0.46.0 (2026-08-30)

libvcs 0.46.0 hardens the command wrappers against argument injection: `Git.pull()`, a `GitSync` revision, and `Svn.checkout()` now reject a value a VCS binary would parse as an option in the positions an end-of-options `--` cannot guard, closing paths that could otherwise run a command or truncate a file. It also gives progress-callback timestamps an aware UTC value, documents what every field holds in the API reference, and moves the lint and CI toolchain to current majors.
Expand Down
105 changes: 98 additions & 7 deletions MIGRATION
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,93 @@ _Notes on the upcoming release will be added here_

<!-- Maintainers, insert migration notes for the next release here -->

### Sync results and dirty checkout policy

`update_repo()` creates or updates a checkout and returns `SyncResult`.
Check `result.ok` and its ordered `errors` before proceeding. `obtain()` only
creates the initial checkout.

The default is now `SyncPolicy(drift="follow", dirty="abort")`. Automation
that updates dirty checkouts must choose `dirty="preserve"` to retain
recoverable changes or explicitly choose `dirty="discard"` to remove them.
The library does not prompt for discard. Preservation may report conflicts;
it never falls back to discard.

Pass `target=SyncTarget(...)` and `policy=SyncPolicy(...)` to `update_repo()`.
A method target replaces the constructor's `rev` default. Existing Git/Hg
keep/warn operations do not fetch or change attachment; new checkouts first
establish their configured target. SVN keep/warn requires a numeric revision
for local comparison.

Retain `result.recovery` on both successful and failed updates. Tokens stay
available until `release_changes()`; `list_recoveries()` discovers them and
`recover_changes()` writes a separate new checkout. See the executing
{ref}`recovery example <sync-recovery>` and backend limits before deleting
source repositories or recovery storage.

### Mercurial remotes and selected pull sources

Use `HgSync(remotes={"upstream": HgRemote("upstream", fetch_url, push_url)})`
to declare native Mercurial paths. A string value sets both destinations to
that URL. Pass `SyncTarget(branch="default", remote="upstream")` to choose
the pull source independently of the push URL.

Following updates apply configured paths. On existing checkouts, keep/warn
policies and dirty-abort preflight leave `.hg/hgrc` unchanged. The library owns a final marked `[paths]`
block; comments and includes outside it remain intact. Leave that block last
when editing native configuration manually.

### Sync backends use typed options

Backend-specific constructor arguments move into frozen option objects. Import
the option class from `libvcs` or beside its sync class:

```python
>>> from libvcs import GitOptions, GitSync, HgOptions, SvnOptions
>>> git = GitSync(
... url="https://example.com/project.git",
... path=tmp_path / "project",
... options=GitOptions(depth=1, tls_verify=True),
... )
>>> git.options
GitOptions(depth=1, filter=None, tls_verify=True)
```

Replace the released Git constructor arguments as follows:

- `git_shallow=True` becomes `options=GitOptions(depth=1)`.
- `depth=n` becomes `options=GitOptions(depth=n)`.
- `git_filter=value` becomes `options=GitOptions(filter=value)`.
- The old `tls_verify=True` disabled verification. Use
`options=GitOptions(tls_verify=False)` to retain that behavior. The new
default, `tls_verify=True`, verifies certificates.

Mercurial transport settings belong to `HgOptions`. Subversion `username`,
`password`, and `svn_trust_cert` become `SvnOptions(username=...)`,
`SvnOptions(password=...)`, and `SvnOptions(trust_server_cert=...)`.
Constructors and `create_project()` reject unknown keywords instead of
silently retaining or discarding them. Use `dataclasses.fields(Sync.options_type)`
to enumerate a backend's accepted configuration fields.

### Git partial-clone filters have typed models

String `_filter` arguments remain valid. Callers that build filters from
configuration can replace unchecked strings with models or kind-tagged
mappings from `libvcs.cmd.git_filter`:

```python
>>> from libvcs.cmd.git_filter import BlobLimit
>>> BlobLimit("4m")
BlobLimit(limit='4m')
```

`GitOptions(filter=...)` validates the value during construction, applies it
to the initial clone and its submodules, and leaves existing checkout filters
unchanged during updates. `Auto` requires Git 2.54 or newer. If the cloned
repository contains gitlinks, `GitSync` retains the parent clone and raises
before submodule initialization because Git's submodule command rejects
`auto`. Use an explicit filter for repositories with submodules.

### pytest fixtures: `gitconfig` / `hgconfig` renamed to `vcs_gitconfig` / `vcs_hgconfig` (#528)

- pytest: `gitconfig` renamed to `vcs_gitconfig`
Expand Down Expand Up @@ -69,17 +156,21 @@ references these fixtures.

`URL.rule_map` is now a class attribute rather than a dataclass attribute.

Before Python 3.11 rejected mutable dataclass defaults:
Python 3.11 and newer reject a `RuleMap` instance as a mutable dataclass
default. Python 3.10 accepts the field:

```python
>>> import dataclasses
>>> import sys
>>> from libvcs.url.base import RuleMap
>>> @dataclasses.dataclass(repr=False)
... class GitLabURL:
... rule_map: RuleMap = RuleMap(_rule_map={})
Traceback (most recent call last):
...
ValueError: mutable default <class 'libvcs.url.base.RuleMap'> for field rule_map is not allowed: use default_factory
>>> try:
... @dataclasses.dataclass(repr=False)
... class GitLabURL:
... rule_map: RuleMap = RuleMap(_rule_map={})
... except ValueError as error:
... assert "mutable default" in str(error)
... else:
... assert sys.version_info < (3, 11)
```

After release:
Expand Down
Loading
Loading