Skip to content

Conversation

@tony
Copy link
Member

@tony tony commented Feb 7, 2026

Summary

update_repo() across all three VCS sync classes (Git, Hg, SVN) now returns a SyncResult instead of None. This lets callers distinguish successful syncs from failures and inspect structured error details — instead of errors being silently swallowed or bubbling as uncaught exceptions.

What changed

  • New dataclasses in sync/base.py: SyncResult (tracks ok status + error list) and SyncError (records step name, message, and original exception)
  • GitSync.update_repo(): All 10+ except CommandError: return paths now record errors in SyncResult with labeled steps (fetch, rebase, checkout, stash-save, etc.) and preserve the original early-return vs. continue-on-error control flow
  • HgSync.update_repo() and SvnSync.update_repo(): Updated for API consistency — wrap obtain and pull/update failures in SyncResult
  • GitRemoteRefNotFound: Added __str__() override to avoid AttributeError when the parent CommandError.__str__() expects cmd/returncode attributes that this subclass doesn't set

Additional fixes included

  • Git.rev_list(): Fixed _all parameter reference that was shadowed by the builtin all
  • rev-list disambiguation: Use fully-qualified refs/heads/ paths when a local branch name collides with a filesystem path
  • Invalid-upstream rebase: Return early with error instead of falling through to stash-pop on an already-failed rebase
  • Ruff config: Moved A002 suppression from inline comment to per-file-ignores in pyproject.toml
  • Test isolation: test_update_repo_pull_failure_returns_sync_result (hg) now uses the create_hg_remote_repo factory instead of deleting the session-scoped hg_remote_repo fixture

Design decisions

  • Errors as values, not exceptions: SyncResult follows the structured result pattern. Git's update_repo() already caught and discarded CommandError at every step — this makes those outcomes inspectable rather than silent
  • Preserved control flow: Each error handler keeps the original behavior. fetch failure returns early (fatal). stash-save failure continues (non-fatal). rev-list HEAD failure on initial repos returns ok=True (benign — nothing to do)
  • Best-effort recovery: Uses contextlib.suppress(CommandError) for cleanup operations that may fail (rebase abort, stash pop, reset) — matching git's own philosophy of trying cleanup but not failing if it doesn't work

Test plan

  • test_update_repo_success_returns_sync_result — ok=True on successful sync
  • test_update_repo_fetch_failure_returns_sync_result — fetch failure captured in SyncResult
  • test_update_repo_symbolic_ref_failure_returns_sync_result — symbolic-ref failure path
  • test_update_repo_checkout_failure_returns_sync_result — checkout error detection
  • test_update_repo_stash_save_failure_returns_sync_result — stash-save failure continues
  • test_update_repo_rev_list_head_failure_returns_sync_result — rev-list HEAD failure path
  • test_update_repo_pull_failure_returns_sync_result (hg) — pull failure with disposable remote
  • test_update_repo_update_failure_returns_sync_result (svn) — update failure path
  • test_rev_list_all_parameter — verifies _all parameter fix
  • All 577 tests pass, mypy clean, ruff clean

Companion PR

why: GitSync.update_repo() catches CommandError at 10+ locations and
silently returns None, making it impossible for callers to detect
sync failures. This causes vcspull to report "✓ Synced" even when
git operations (fetch, rebase, checkout) fail.

what:
- Add SyncResult and SyncError dataclasses to sync/base.py
- Change BaseSync.update_repo() return type from None to SyncResult
- Transform GitSync.update_repo() except handlers to record errors
  in SyncResult while preserving existing control flow
- Update SvnSync and HgSync for API consistency
- Add tests for success and fetch-failure scenarios
why: The old `# ruff: NOQA: A002` file-level comment syntax is no
longer recognized by ruff (RUF103), causing CI to fail.

what:
- Remove obsolete `# ruff: NOQA: A002` from subprocess.py
- Add per-file-ignores entry in pyproject.toml for A002 on subprocess.py
@codecov
Copy link

codecov bot commented Feb 7, 2026

Codecov Report

❌ Patch coverage is 89.21569% with 55 lines in your changes missing coverage. Please review.
✅ Project coverage is 57.07%. Comparing base (3d850eb) to head (bca6778).
⚠️ Report is 22 commits behind head on master.

Files with missing lines Patch % Lines
src/libvcs/sync/svn.py 0.00% 15 Missing ⚠️
tests/sync/test_svn.py 0.00% 14 Missing ⚠️
src/libvcs/sync/base.py 18.75% 13 Missing ⚠️
src/libvcs/sync/git.py 88.37% 9 Missing and 1 partial ⚠️
src/libvcs/sync/hg.py 82.35% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #514      +/-   ##
==========================================
+ Coverage   53.48%   57.07%   +3.58%     
==========================================
  Files          38       38              
  Lines        5704     6166     +462     
  Branches     1062     1069       +7     
==========================================
+ Hits         3051     3519     +468     
+ Misses       2143     2126      -17     
- Partials      510      521      +11     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

…rrored sync tests

why: cmd.checkout() without check_returncode=True silently swallowed
checkout failures, making the except CommandError block dead code.
Also, SVN and HG had no error-path tests for update_repo().
what:
- Add check_returncode=True to both cmd.checkout() calls in GitSync.update_repo()
- Add test_update_repo_checkout_failure_returns_sync_result for git (nonexistent branch)
- Add test_update_repo_checkout_failure_returns_sync_result for svn (deleted remote)
- Add test_update_repo_pull_failure_returns_sync_result for hg (deleted remote)
@tony tony marked this pull request as ready for review February 7, 2026 13:33
@tony

This comment has been minimized.

tony added 3 commits February 7, 2026 08:05
why: Code review identified incomplete docstrings missing Parameters
and Returns sections required by project standards.
what:
- Add Parameters/Returns to SyncResult.__bool__() and add_error()
- Add Parameters/Returns to GitSync.update_repo()
- Add Returns to HgSync.update_repo()
- Add Parameters/Returns to SvnSync.update_repo()
why: Stash-save error handler was missing return statement, allowing
checkout/rebase to proceed with unstashed changes and stash.pop()
to fail on a non-existent stash.
what:
- Add return result after stash-save error recording
why: CommandError from obtain() propagated uncaught, bypassing the
structured SyncResult error reporting that all other sync steps use.
what:
- Wrap obtain() in try/except in GitSync.update_repo()
- Wrap obtain() in try/except in HgSync.update_repo()
- Wrap obtain() in try/except in SvnSync.update_repo()
- Update command injection test to check SyncResult instead of raised exception
@tony

This comment has been minimized.

tony added 5 commits February 7, 2026 08:56
why: The rev_list HEAD error handler caught CommandError and returned
result without calling add_error(), leaving ok=True on failure.
what:
- Capture exception and call result.add_error() in rev-list HEAD handler
why: The rev-list HEAD error handler had no test coverage, which
allowed the missing add_error() bug to go undetected.
what:
- Add test_update_repo_rev_list_head_failure_returns_sync_result
…ing paths

why: When a git repo has a local branch whose name matches a directory
(e.g. branch "notes" + directory "notes/"), git rev-list fails with
"fatal: ambiguous argument". The error was caught but not recorded in
SyncResult, so vcspull reported the sync as successful despite the
visible fatal error.

what:
- Check show_ref output for refs/heads/<tag> before calling rev-list
- Use fully-qualified refs/heads/ path when available, avoiding the
  ambiguity entirely (no fatal error emitted, no retry needed)
- Fall back to bare tag name when no refs/heads/ match exists (e.g.
  tags, SHAs, or refs not yet fetched)
why: Several error paths in GitSync.update_repo() are not wrapped in
try/except, so failures propagate as uncaught exceptions instead of
being recorded in SyncResult. This causes vcspull to either crash or
report false successes.
what:
- Add test_update_repo_submodule_failure_recorded (line 589 gap)
- Add test_update_repo_symbolic_ref_failure_recorded (line 420-424 gap)
- Add test_update_repo_remote_ref_not_found_recorded (line 462 gap)
- All marked xfail(strict=True) pending fix
why: Several error paths in GitSync.update_repo() were not wrapped in
try/except, causing failures to propagate as uncaught exceptions instead
of being recorded in SyncResult. This made vcspull report false successes
(e.g. "✓ Synced cpython" when git had a fatal error).
what:
- Wrap symbolic_ref() in try/except with check_returncode=True (detached HEAD)
- Wrap get_current_remote_name() in try/except
- Catch GitRemoteRefNotFound instead of bare raise; record in SyncResult
- Wrap submodule.update() in try/except (non-fatal: records but doesn't abort)
- Use contextlib.suppress for best-effort recovery paths (rebase --abort, etc.)
- Fix GitRemoteRefNotFound.__str__ to not require cmd attribute
- Add comment explaining intentional rev_list(tag) catch behavior
- Update existing test assertions for new check_returncode=True kwarg
- Remove xfail from three new error-path tests
@tony
Copy link
Member Author

tony commented Feb 7, 2026

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

🤖 Generated with Claude Code

tony added 4 commits February 7, 2026 11:38
why: rev is typed str | None, so assigning None needs no suppression.
mypy flags the unused comment as an error.
what:
- Remove unused type: ignore[assignment] on git_repo.rev = None
why: The invalid_upstream handler recorded the error (ok=False) but
fell through to stash pop, creating inconsistent state.
what:
- Add return result after add_error in the invalid_upstream branch
why: The flag processing loop used bare `all` (Python builtin, always
truthy) instead of the `_all` parameter, causing --all to be appended
unconditionally twice to every rev-list command.
what:
- Change (all, "--all") to (_all, "--all") at both occurrences
- Add test_rev_list_all_parameter to verify _all flag behavior
why: test_update_repo_pull_failure_returns_sync_result deleted the
session-scoped hg_remote_repo, breaking all subsequent tests that
depend on it (e.g. tests/url/test_hg.py::test_hg_url).
what:
- Use create_hg_remote_repo factory to create a disposable remote
- Matches the pattern used by the equivalent git test
@tony
Copy link
Member Author

tony commented Feb 7, 2026

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

tony added 5 commits February 7, 2026 12:44
why: Document the changes introduced in #514 for the upcoming release.
what:
- New feature: update_repo() returns SyncResult across Git, Hg, SVN
- Bug fixes: rev_list _all parameter, rev-list disambiguation, rebase early return
- Tests: Fix hg test destroying session-scoped fixture
…d base class

why: README showed a bare update_repo() call that ignored errors, and
BaseSync.update_repo() docstring lacked a Returns section for autodoc.
what:
- Update README sync example to capture SyncResult and check .ok/.errors
- Add NumPy-style Returns section to BaseSync.update_repo() docstring
…otation

why: Project style favors explicit class-level annotations over
inferred instance attributes.
what:
- Add `_message: str` annotation to GitRemoteRefNotFound class body
…l package

why: Users currently must import from libvcs.sync.base directly.
Exporting from __init__.py makes the public API explicit.
what:
- Add SyncResult and SyncError to libvcs.__init__ imports
- Add both to __all__
why: 7 of 13 error steps in GitSync.update_repo() had no test coverage.
what:
- Add test_update_repo_obtain_failure_recorded
- Add test_update_repo_set_remotes_failure_recorded
- Add test_update_repo_remote_name_failure_recorded
- Add test_update_repo_status_failure_recorded
- Add test_update_repo_stash_save_failure_recorded
- Add test_update_repo_rebase_invalid_upstream_recorded
- Add test_update_repo_rebase_conflict_recorded
- Add test_update_repo_stash_pop_failure_recorded
- Add test_sync_result_multiple_errors for multi-error accumulation
Copy link
Member Author

@tony tony left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

Reviewed: error handling in sync/git.py, sync/hg.py, sync/svn.py; SyncResult/SyncError dataclasses in sync/base.py; rev_list parameter fix in cmd/git.py; GitRemoteRefNotFound exception refactor; test coverage across 13 error steps; public exports in init.py.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@tony tony merged commit 8990ccb into master Feb 7, 2026
8 checks passed
@tony tony deleted the errored-sync-behavior branch February 7, 2026 19:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant