vcspull sync - vcspull.cli.sync¶
Synchronization functionality for vcspull.
-
vcspull.cli.sync._visible_length(text)¶
Return the printable length of string stripped of ANSI codes.
-
class vcspull.cli.sync.PlanProgressPrinter¶
Bases:
objectRender incremental plan progress for human-readable dry runs.
-
vcspull.cli.sync._extract_repo_url(repo)¶
Extract the primary repository URL from a config dictionary.
- Parameters:
repo (ConfigDict)
- Return type:
-
vcspull.cli.sync._get_repo_path(repo)¶
Return the resolved filesystem path for a repository entry.
- Parameters:
repo (ConfigDict)
- Return type:
-
vcspull.cli.sync.clamp(n, _min, _max)¶
Clamp a number between a min and max value.
-
vcspull.cli.sync._DEFAULT_REPO_TIMEOUT_SECONDS = 10¶
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
--timeoutor theVCSPULL_SYNC_TIMEOUT_SECONDSenv var.
-
vcspull.cli.sync._get_no_prompt_env()¶
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.environat first access, which meansmonkeypatch.setenvcalls in tests after that point never reach the subprocess. A dict copy on every fetch is cheap.
-
vcspull.cli.sync._maybe_fetch(repo_path, *, config)¶
Optionally fetch remote refs to provide accurate status.
-
vcspull.cli.sync._determine_plan_action(status, *, config)¶
Decide which plan action applies to a repository.
-
vcspull.cli.sync._update_summary(summary, action)¶
Update summary counters for the given plan action.
- Parameters:
summary (PlanSummary)
action (PlanAction)
- Return type:
-
vcspull.cli.sync._build_plan_entry(repo, *, config)¶
Construct a plan entry for a repository configuration.
- Parameters:
repo (ConfigDict)
config (SyncPlanConfig)
- Return type:
PlanEntry
-
async vcspull.cli.sync._build_plan_result_async(repos, *, config, progress)¶
Build a plan asynchronously while updating progress output.
- Parameters:
repos (list[ConfigDict])
config (SyncPlanConfig)
progress (PlanProgressPrinter | None)
- Return type:
PlanResult
-
vcspull.cli.sync._filter_entries_for_display(entries, *, show_unchanged)¶
Filter entries based on whether unchanged repos should be rendered.
-
vcspull.cli.sync._format_detail_text(entry, *, colors, include_extras)¶
Generate the detail text for a plan entry.
-
vcspull.cli.sync._render_plan(formatter, colors, plan, render_options, *, dry_run, total_repos)¶
Render the plan in human-readable format.
-
vcspull.cli.sync._emit_plan_output(formatter, colors, plan, render_options, *, dry_run, total_repos)¶
Emit plan output for the requested format.
-
vcspull.cli.sync.create_sync_subparser(parser)¶
Create
vcspull syncargument subparser.- Parameters:
parser (ArgumentParser)
- Return type:
-
class vcspull.cli.sync._TimedOutRepo¶
Bases:
objectMetadata about a repository that exceeded its per-repo timeout.
-
class vcspull.cli.sync._SyncOutcome¶
Bases:
objectResult of attempting to sync a single repository.
-
vcspull.cli.sync._positive_int_arg(value)¶
Validate
--timeoutaccepts 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_timeoutsilently fell back to the default on these inputs, which masked typos like--timeout -10(intended10) 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')
-
vcspull.cli.sync._resolve_repo_timeout(cli_timeout)¶
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
-
vcspull.cli.sync._DEFAULT_PANEL_LINES = 3¶
Mirrors the default in
vcspull.cli._progress– duplicated here so the--helptext can interpolate the real number without importing the private_DEFAULT_OUTPUT_LINESsymbol.
-
vcspull.cli.sync._panel_lines_arg(value)¶
Validate
--panel-linesaccepts-1,0, or any positive int.The flag uses
-1as the “unbounded panel” sentinel and0as the “hide panel” sentinel. Any other negative value (-2,-7, …) is meaningless;argparsewould silently coerce them to “unbounded” via_resolve_panel_lines()’s permissive branch, which masks user typos. Reject at parse time with a typer-styleArgumentTypeErrorinstead.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')
-
vcspull.cli.sync._resolve_panel_lines(cli_value)¶
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> 0like_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
-
exception vcspull.cli.sync._SyncInterruptedAfterSummary¶
Bases:
KeyboardInterruptInternal marker:
_sync_implalready 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. SubclassingKeyboardInterruptkeeps the exception in the same taxonomy for the outerexcept KeyboardInterruptcatch.Invariant: the only
except KeyboardInterruptbetween the inner raise and process exit issync()’s own handler, which dispatches onisinstance(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()¶
Terminate so the parent shell sees
WIFSIGNALED(SIGINT).Interactive shells (bash, zsh) abort
cmd1; cmd2sequential lists only when the child was killed by a signal – a cleanSystemExit(130)leaves the shell no reason to stop. Match git’s pattern (sigchain.h:20-34;builtin/clone.c:416): cleanup first, installSIG_DFL, self-deliver SIGINT so the kernel reportsWIFSIGNALEDto the parent.Callers MUST complete cleanup before invoking this. Python
atexithooks andfinallyblocks do NOT run onceSIG_DFLterminates the process at the C level.Windows has no
WIFSIGNALEDanalogue and itsraise_signal(SIGINT)underSIG_DFLraisesKeyboardInterruptback at the caller instead of exiting – fall back to the conventionalSystemExit(130).Precondition: must be called from the main thread of the main interpreter.
signal.signalraisesValueErrorotherwise. The CLI dispatcher atvcspull/cli/__init__.pyalways invokessync()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:
-
class vcspull.cli.sync._IndicatorStreamProxy¶
Bases:
objectFile-like adapter that routes writes through a
SyncStatusIndicator.Installed on the
libvcs/vcspullloggerStreamHandlerwhile 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)¶
Rebind the
libvcs/vcspullstream handlers throughindicator.Returns a callable that restores the original streams. Only touches handlers that are
logging.StreamHandlerbut notlogging.FileHandler– the debug log file keeps writing straight to disk regardless of the spinner.Both the install and the restore swap
handler.streamwhile holdinghandler.lock– the same lockHandler.handle(stdliblogging/__init__.py:1011) takes around everyemit(). Directhandler.stream = Xwithout 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 toflush()the old stream. Under pytest, the handler may already point at a finalizedcapsys/CaptureIOfrom a prior test (setup_loggersethandler.stream = sys.stdoutwhen stdout was the live capture); flushing a closed file there raisesValueError. 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
CRITICALfor 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 --shortline carries the same content as libvcs’s|git| (rye) Failed to determine current branchwarning that fires immediately before; printing both breaks the✓ Synced X / ✗ Failed X / - Timed out Xrhythm. The debug-logFileHandlerkeeps DEBUG, so a post-mortem still has the libvcs line for context.-v/-vvusers opted into INFO / DEBUG and keep their explicit level.
-
vcspull.cli.sync._sync_repo_with_watchdog(repo, *, progress_callback, timeout, is_human)¶
Run
update_repo()under a wall-clock watchdog.The libvcs call runs on a daemon
threading.Thread; the main thread uses a completionthreading.Eventas its deadline. Raw threads are deliberate –concurrent.futures.ThreadPoolExecutorregisters its workers inconcurrent.futures.thread._threads_queues, whoseatexithook_python_exitjoins 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:
-
vcspull.cli.sync._emit_rerun_recipe(formatter, colors, *, timed_out_repos, timeout)¶
Print a copyable recipe for rerunning just the timed-out repositories.
Modelled on the way
cargo/npmsurface actionable next steps after a failure. The suggested timeout ismax(120, timeout * 10)so a user who kept the aggressive default still gets a meaningful headroom when they retry.- Parameters:
formatter (OutputFormatter)
colors (Colors)
timed_out_repos (list[_TimedOutRepo])
timeout (int)
- Return type:
-
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)¶
Entry point for
vcspull sync.- Parameters:
dry_run (bool)
output_json (bool)
output_ndjson (bool)
color (str)
exit_on_error (bool)
show_unchanged (bool)
summary_only (bool)
long_view (bool)
relative_paths (bool)
fetch (bool)
offline (bool)
verbosity (int)
sync_all (bool)
parser (ArgumentParser | None)
include_worktrees (bool)
no_log_file (bool)
- Return type:
-
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)¶
Run the core body of
sync().Kept separate so log-file teardown runs through
finallyregardless of where the caller exits the sync.- Parameters:
dry_run (bool)
output_json (bool)
output_ndjson (bool)
color (str)
exit_on_error (bool)
show_unchanged (bool)
summary_only (bool)
long_view (bool)
relative_paths (bool)
fetch (bool)
offline (bool)
verbosity (int)
sync_all (bool)
parser (ArgumentParser | None)
include_worktrees (bool)
repo_timeout (int)
panel_lines (int)
- Return type:
-
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)¶
Iterate the repositories and drive the watchdog + indicator.
- Parameters:
found_repos (list[ConfigDict])
formatter (OutputFormatter)
colors (Colors)
timed_out_repos (list[_TimedOutRepo])
progress_callback (ProgressCallback)
is_human (bool)
repo_timeout (int)
exit_on_error (bool)
include_worktrees (bool)
dry_run (bool)
parser (ArgumentParser | None)
indicator (SyncStatusIndicator)
- Return type:
-
vcspull.cli.sync._emit_summary(formatter, colors, summary)¶
Emit the structured summary event and optional human-readable text.
-
vcspull.cli.sync.progress_cb(output, timestamp)¶
CLI Progress callback for command.
-
vcspull.cli.sync.guess_vcs(url)¶
Guess the VCS from a URL.
-
exception vcspull.cli.sync.CouldNotGuessVCSFromURL¶
Bases:
VCSPullExceptionRaised when no VCS could be guessed from a URL.
-
exception vcspull.cli.sync.SyncFailedError¶
Bases:
VCSPullExceptionRaised when a sync operation completes but with errors.
-
vcspull.cli.sync.update_repo(repo_dict, progress_callback=None)¶
Synchronize a single repository.