vcspull sync - vcspull.cli.sync

Synchronization functionality for vcspull.

class vcspull.cli.sync.SyncPlanConfig

Bases: object

Configuration options for building sync plans.

vcspull.cli.sync._visible_length(text)
function[source]

Return the printable length of string stripped of ANSI codes.

Parameters:

text (str)

Return type:

int

class vcspull.cli.sync.PlanProgressPrinter

Bases: object

Render incremental plan progress for human-readable dry runs.

vcspull.cli.sync._extract_repo_url(repo)
function[source]

Extract the primary repository URL from a config dictionary.

Parameters:

repo (ConfigDict)

Return type:

str | None

vcspull.cli.sync._get_repo_path(repo)
function[source]

Return the resolved filesystem path for a repository entry.

Parameters:

repo (ConfigDict)

Return type:

Path

vcspull.cli.sync.clamp(n, _min, _max)
function[source]

Clamp a number between a min and max value.

Parameters:
Return type:

int

vcspull.cli.sync._DEFAULT_REPO_TIMEOUT_SECONDS = 10
data

Default wall-clock deadline for each repository in vcspull sync.

Kept aggressive on purpose – a healthy fetch/pull against a warm remote is almost always under 10 seconds. Anything over that is “suspect” and should surface as an actionable timeout rather than a silent hang. Callers can override via --timeout or the VCSPULL_SYNC_TIMEOUT_SECONDS env var.

vcspull.cli.sync._get_no_prompt_env()
function[source]

Return an environment dict that prevents git from prompting on stdin.

Built fresh per call rather than cached at module scope: a cache snapshots os.environ at first access, which means monkeypatch.setenv calls in tests after that point never reach the subprocess. A dict copy on every fetch is cheap.

Return type:

dict[str, str]

vcspull.cli.sync._maybe_fetch(repo_path, *, config)
function[source]

Optionally fetch remote refs to provide accurate status.

Parameters:
Return type:

tuple[bool, str | None]

vcspull.cli.sync._determine_plan_action(status, *, config)
function[source]

Decide which plan action applies to a repository.

Parameters:
Return type:

tuple[PlanAction, str | None]

vcspull.cli.sync._update_summary(summary, action)
function[source]

Update summary counters for the given plan action.

Parameters:
  • summary (PlanSummary)

  • action (PlanAction)

Return type:

None

vcspull.cli.sync._build_plan_entry(repo, *, config)
function[source]

Construct a plan entry for a repository configuration.

Parameters:
Return type:

PlanEntry

async vcspull.cli.sync._build_plan_result_async(repos, *, config, progress)
async function[source]

Build a plan asynchronously while updating progress output.

Parameters:
Return type:

PlanResult

vcspull.cli.sync._filter_entries_for_display(entries, *, show_unchanged)
function[source]

Filter entries based on whether unchanged repos should be rendered.

Parameters:
  • entries (list[PlanEntry])

  • show_unchanged (bool)

Return type:

list[PlanEntry]

vcspull.cli.sync._format_detail_text(entry, *, colors, include_extras)
function[source]

Generate the detail text for a plan entry.

Parameters:
  • entry (PlanEntry)

  • colors (Colors)

  • include_extras (bool)

Return type:

str

vcspull.cli.sync._render_plan(formatter, colors, plan, render_options, *, dry_run, total_repos)
function[source]

Render the plan in human-readable format.

Parameters:
  • formatter (OutputFormatter)

  • colors (Colors)

  • plan (PlanResult)

  • render_options (PlanRenderOptions)

  • dry_run (bool)

  • total_repos (int)

Return type:

None

vcspull.cli.sync._emit_plan_output(formatter, colors, plan, render_options, *, dry_run, total_repos)
function[source]

Emit plan output for the requested format.

Parameters:
  • formatter (OutputFormatter)

  • colors (Colors)

  • plan (PlanResult)

  • render_options (PlanRenderOptions)

  • dry_run (bool)

  • total_repos (int)

Return type:

None

vcspull.cli.sync.create_sync_subparser(parser)
function[source]

Create vcspull sync argument subparser.

Parameters:

parser (ArgumentParser)

Return type:

ArgumentParser

class vcspull.cli.sync._TimedOutRepo

Bases: object

Metadata about a repository that exceeded its per-repo timeout.

class vcspull.cli.sync._SyncOutcome

Bases: object

Result of attempting to sync a single repository.

vcspull.cli.sync._positive_int_arg(value)
function[source]

Validate --timeout accepts only positive integers.

A timeout of zero or negative seconds is meaningless: the watchdog would either return immediately (timed out) or never (logically invalid). The earlier permissive _resolve_repo_timeout silently fell back to the default on these inputs, which masked typos like --timeout -10 (intended 10) where the user assumed the flag had taken effect.

Examples

>>> _positive_int_arg("60")
60
>>> _positive_int_arg("0")
Traceback (most recent call last):
...
argparse.ArgumentTypeError: --timeout must be a positive integer (got 0)
>>> _positive_int_arg("-5")
Traceback (most recent call last):
...
argparse.ArgumentTypeError: --timeout must be a positive integer (got -5)
>>> _positive_int_arg("abc")
Traceback (most recent call last):
...
argparse.ArgumentTypeError: --timeout must be an integer (got 'abc')
Parameters:

value (str)

Return type:

int

vcspull.cli.sync._resolve_repo_timeout(cli_timeout)
function[source]

Resolve the repo timeout from CLI flag / env var / built-in default.

Programmatic callers passing non-positive values still fall back to the env/default ladder; the CLI surface enforces positive-int semantics via _positive_int_arg() at parse time.

Examples

>>> import os
>>> _ = os.environ.pop("VCSPULL_SYNC_TIMEOUT_SECONDS", None)
>>> _resolve_repo_timeout(None)
10
>>> _resolve_repo_timeout(60)
60
>>> _resolve_repo_timeout(0)
10
>>> _resolve_repo_timeout(-5)
10
Parameters:

cli_timeout (int | None)

Return type:

int

vcspull.cli.sync._DEFAULT_PANEL_LINES = 3
data

Mirrors the default in vcspull.cli._progress – duplicated here so the --help text can interpolate the real number without importing the private _DEFAULT_OUTPUT_LINES symbol.

vcspull.cli.sync._panel_lines_arg(value)
function[source]

Validate --panel-lines accepts -1, 0, or any positive int.

The flag uses -1 as the “unbounded panel” sentinel and 0 as the “hide panel” sentinel. Any other negative value (-2, -7, …) is meaningless; argparse would silently coerce them to “unbounded” via _resolve_panel_lines()’s permissive branch, which masks user typos. Reject at parse time with a typer-style ArgumentTypeError instead.

Examples

>>> _panel_lines_arg("0")
0
>>> _panel_lines_arg("-1")
-1
>>> _panel_lines_arg("5")
5
>>> _panel_lines_arg("-2")
Traceback (most recent call last):
...
argparse.ArgumentTypeError: --panel-lines must be -1, 0, or positive (got -2)
>>> _panel_lines_arg("abc")
Traceback (most recent call last):
...
argparse.ArgumentTypeError: --panel-lines must be an integer (got 'abc')
Parameters:

value (str)

Return type:

int

vcspull.cli.sync._resolve_panel_lines(cli_value)
function[source]

Resolve the live-trail panel height: CLI flag > env > default.

The flag accepts 0 (hide panel) and -1 (unbounded), so we can’t short-circuit on > 0 like _resolve_repo_timeout(). We do accept any integer the user passes via the CLI; only the env var override is validated, with a warning on garbage values.

Examples

>>> import os
>>> _ = os.environ.pop("VCSPULL_PROGRESS_LINES", None)
>>> _resolve_panel_lines(None)
3
>>> _resolve_panel_lines(0)
0
>>> _resolve_panel_lines(-1)
-1
>>> _resolve_panel_lines(5)
5
Parameters:

cli_value (int | None)

Return type:

int

exception vcspull.cli.sync._SyncInterruptedAfterSummary
exception[source]

Bases: KeyboardInterrupt

Internal marker: _sync_impl already emitted the interrupt summary.

Lets the outer sync() handler skip the plain-stderr fallback when the inner layer has already printed a coloured partial summary through the formatter. Subclassing KeyboardInterrupt keeps the exception in the same taxonomy for the outer except KeyboardInterrupt catch.

Invariant: the only except KeyboardInterrupt between the inner raise and process exit is sync()’s own handler, which dispatches on isinstance(err, _SyncInterruptedAfterSummary). A future broader catch higher up the stack must either re-raise the marker unchanged or perform the same isinstance check – otherwise the inner partial-summary print AND the outer plain-stderr fallback both fire, restoring the double-print artefact this marker exists to suppress.

vcspull.cli.sync._exit_on_sigint()
function[source]

Terminate so the parent shell sees WIFSIGNALED(SIGINT).

Interactive shells (bash, zsh) abort cmd1; cmd2 sequential lists only when the child was killed by a signal – a clean SystemExit(130) leaves the shell no reason to stop. Match git’s pattern (sigchain.h:20-34; builtin/clone.c:416): cleanup first, install SIG_DFL, self-deliver SIGINT so the kernel reports WIFSIGNALED to the parent.

Callers MUST complete cleanup before invoking this. Python atexit hooks and finally blocks do NOT run once SIG_DFL terminates the process at the C level.

Windows has no WIFSIGNALED analogue and its raise_signal(SIGINT) under SIG_DFL raises KeyboardInterrupt back at the caller instead of exiting – fall back to the conventional SystemExit(130).

Precondition: must be called from the main thread of the main interpreter. signal.signal raises ValueError otherwise. The CLI dispatcher at vcspull/cli/__init__.py always invokes sync() on the main thread, so this is a callsite contract rather than a runtime check – a silent non-main-thread fallback would only kill that thread, not the process, and bash would still see a clean exit and continue the ; chain, defeating the whole change.

Return type:

NoReturn

class vcspull.cli.sync._IndicatorStreamProxy

Bases: object

File-like adapter that routes writes through a SyncStatusIndicator.

Installed on the libvcs / vcspull logger StreamHandler while the spinner is active so logged lines clear the spinner before they print, instead of appending to its in-flight \r-redrawn line.

vcspull.cli.sync._install_indicator_log_diverter(indicator)
function[source]

Rebind the libvcs / vcspull stream handlers through indicator.

Returns a callable that restores the original streams. Only touches handlers that are logging.StreamHandler but not logging.FileHandler – the debug log file keeps writing straight to disk regardless of the spinner.

Both the install and the restore swap handler.stream while holding handler.lock – the same lock Handler.handle (stdlib logging/__init__.py:1011) takes around every emit(). Direct handler.stream = X without the lock technically violates that contract even if CPython’s GIL hides the race in practice.

We don’t go through logging.StreamHandler.setStream() because its first action is to flush() the old stream. Under pytest, the handler may already point at a finalized capsys/CaptureIO from a prior test (setup_logger set handler.stream = sys.stdout when stdout was the live capture); flushing a closed file there raises ValueError. Skipping the flush is safe – we are not closing the stream, only redirecting future writes.

On top of the stream swap we also raise the libvcs StreamHandler level above CRITICAL for the duration of the sync, but only when the user kept the default verbosity (handler at WARNING). vcspull’s own Failed syncing rye: Command failed with code 128: git symbolic-ref HEAD --short line carries the same content as libvcs’s |git| (rye) Failed to determine current branch warning that fires immediately before; printing both breaks the Synced X / Failed X / - Timed out X rhythm. The debug-log FileHandler keeps DEBUG, so a post-mortem still has the libvcs line for context. -v / -vv users opted into INFO / DEBUG and keep their explicit level.

Parameters:

indicator (SyncStatusIndicator)

Return type:

Callable[[], None]

vcspull.cli.sync._sync_repo_with_watchdog(repo, *, progress_callback, timeout, is_human)
function[source]

Run update_repo() under a wall-clock watchdog.

The libvcs call runs on a daemon threading.Thread; the main thread uses a completion threading.Event as its deadline. Raw threads are deliberate – concurrent.futures.ThreadPoolExecutor registers its workers in concurrent.futures.thread._threads_queues, whose atexit hook _python_exit joins every worker on interpreter shutdown. If the user hits Ctrl-C while a libvcs subprocess is wedged, that join hangs the process forever. Daemon threads skip the join entirely: they’re forcibly terminated at shutdown.

Parameters:
  • repo (ConfigDict)

  • progress_callback (ProgressCallback)

  • timeout (int)

  • is_human (bool)

Return type:

_SyncOutcome

vcspull.cli.sync._emit_rerun_recipe(formatter, colors, *, timed_out_repos, timeout)
function[source]

Print a copyable recipe for rerunning just the timed-out repositories.

Modelled on the way cargo/npm surface actionable next steps after a failure. The suggested timeout is max(120, timeout * 10) so a user who kept the aggressive default still gets a meaningful headroom when they retry.

Parameters:
Return type:

None

vcspull.cli.sync.sync(repo_patterns, config, workspace_root, dry_run, output_json, output_ndjson, color, exit_on_error, show_unchanged, summary_only, long_view, relative_paths, fetch, offline, verbosity, sync_all=False, parser=None, include_worktrees=False, timeout=None, log_file=None, no_log_file=False, panel_lines=None)
function[source]

Entry point for vcspull sync.

Parameters:
Return type:

None

vcspull.cli.sync._sync_impl(*, repo_patterns, config, workspace_root, dry_run, output_json, output_ndjson, color, exit_on_error, show_unchanged, summary_only, long_view, relative_paths, fetch, offline, verbosity, sync_all, parser, include_worktrees, repo_timeout, log_file_path, panel_lines)
function[source]

Run the core body of sync().

Kept separate so log-file teardown runs through finally regardless of where the caller exits the sync.

Parameters:
Return type:

None

vcspull.cli.sync._run_sync_loop(*, found_repos, formatter, colors, summary, timed_out_repos, progress_callback, is_human, repo_timeout, exit_on_error, include_worktrees, dry_run, parser, log_file_path, indicator)
function[source]

Iterate the repositories and drive the watchdog + indicator.

Parameters:
Return type:

None

vcspull.cli.sync._emit_summary(formatter, colors, summary)
function[source]

Emit the structured summary event and optional human-readable text.

Parameters:
  • formatter (OutputFormatter)

  • colors (Colors)

  • summary (dict[str, int])

Return type:

None

vcspull.cli.sync.progress_cb(output, timestamp)
function[source]

CLI Progress callback for command.

Parameters:
Return type:

None

vcspull.cli.sync.guess_vcs(url)
function[source]

Guess the VCS from a URL.

Parameters:

url (str)

Return type:

VCSLiteral | None

exception vcspull.cli.sync.CouldNotGuessVCSFromURL
exception[source]

Bases: VCSPullException

Raised when no VCS could be guessed from a URL.

exception vcspull.cli.sync.SyncFailedError
exception[source]

Bases: VCSPullException

Raised when a sync operation completes but with errors.

vcspull.cli.sync.update_repo(repo_dict, progress_callback=None)
function[source]

Synchronize a single repository.

Parameters:
  • repo_dict (Any)

  • progress_callback (ProgressCallback | None)

Return type:

GitSync | HgSync | SvnSync