Add command internals

vcspull.cli.add is the implementation behind vcspull add — it resolves a checkout on disk into a configuration entry and writes it back.

Add single repository functionality for vcspull.

class vcspull.cli.add.AddAction
class vcspull.cli.add.AddAction

Bases: Enum

Action resolved for a single repo during vcspull add.

vcspull.cli.add._classify_add_action(existing_entry)
function[source]
function[source]
vcspull.cli.add._classify_add_action(existing_entry)

Classify the add action for a single repository.

Parameters:

existing_entry (Any) – Current config entry for this repo name, or None if absent.

Return type:

AddAction

Examples

Not in config:

>>> _classify_add_action(None)
<AddAction.ADD: 'add'>

Already exists (unpinned):

>>> _classify_add_action({"repo": "git+ssh://x"})
<AddAction.SKIP_EXISTING: 'skip_existing'>

Pinned for add:

>>> _classify_add_action({"repo": "git+ssh://x", "options": {"pin": True}})
<AddAction.SKIP_PINNED: 'skip_pinned'>
>>> entry = {"repo": "git+ssh://x", "options": {"pin": {"add": True}}}
>>> _classify_add_action(entry)
<AddAction.SKIP_PINNED: 'skip_pinned'>

Pinned for import only — not pinned for add:

>>> entry = {"repo": "git+ssh://x", "options": {"pin": {"import": True}}}
>>> _classify_add_action(entry)
<AddAction.SKIP_EXISTING: 'skip_existing'>
vcspull.cli.add.create_add_subparser(parser)
function[source]
function[source]
vcspull.cli.add.create_add_subparser(parser)

Create vcspull add argument subparser.

Parameters:

parser (argparse.ArgumentParser) – The parser to configure

Return type:

None

vcspull.cli.add._resolve_workspace_path(workspace_root, repo_path_str, *, cwd)
function[source]
function[source]
vcspull.cli.add._resolve_workspace_path(workspace_root, repo_path_str, *, cwd)

Resolve workspace path from arguments.

Parameters:
  • workspace_root (str | None) – Workspace root path from user

  • repo_path_str (str | None) – Repo path from user

  • cwd (pathlib.Path) – Current working directory

Returns:

Resolved workspace path

Return type:

pathlib.Path

vcspull.cli.add._detect_git_remote(repo_path)
function[source]
function[source]
vcspull.cli.add._detect_git_remote(repo_path)

Return the origin remote URL for a Git repository if available.

Parameters:

repo_path (Path)

Return type:

str | None

vcspull.cli.add._normalize_detected_url(remote)
function[source]
function[source]
vcspull.cli.add._normalize_detected_url(remote)

Return display and config URLs derived from a detected remote.

Parameters:

remote (str | None)

Return type:

tuple[str, str]

class vcspull.cli.add.ParsedRepoUrl
class vcspull.cli.add.ParsedRepoUrl

Bases: NamedTuple

What add needs from a repository URL, as libvcs parses it.

vcspull.cli.add._parse_repo_url(url)
function[source]
function[source]
vcspull.cli.add._parse_repo_url(url)

Split a repository URL into a name, a rev-free URL, and a revision.

add accepts a URL argument on GitURL.is_valid(), so it derives the name and revision from that same parse rather than splitting strings itself. Two libvcs details shape this:

  • GitURL.to_url() re-appends a revision the input already carried, so the rev-free URL drops the parsed revision from the tail instead.

  • The pip file:// rule leaves .git on path instead of moving it to suffix, so the final segment is trimmed unconditionally.

Only pip-style URLs (git+…) carry a revision; libvcs parses @rev on a bare https:// URL as part of the path.

Parameters:

url (str) – Repository URL, already accepted by GitURL.is_valid().

Returns:

name is None when the URL has no path segment to name.

Return type:

ParsedRepoUrl

Examples

>>> _parse_repo_url("https://github.com/pallets/flask.git").name
'flask'

A pip-style revision moves out of the URL:

>>> parsed = _parse_repo_url("git+https://github.com/pallets/[email protected]")
>>> parsed.name, parsed.rev
('flask', 'v1.0')
>>> parsed.url
'git+https://github.com/pallets/flask.git'

Scp-style remotes, trailing slashes, and a missing .git all resolve:

>>> _parse_repo_url("[email protected]:pallets/flask.git").name
'flask'
>>> _parse_repo_url("https://github.com/pallets/flask/").name
'flask'
>>> _parse_repo_url("git+file:///srv/git/flask.git").name
'flask'

A URL with no path segment yields no name:

>>> _parse_repo_url("https://host/.git").name is None
True

A revision on a URL libvcs parses no revision for is reported, not kept:

>>> _parse_repo_url("https://github.com/pallets/[email protected]").unparsed_rev
'v1.0'
>>> _parse_repo_url("https://user@host/pallets/flask.git").unparsed_rev is None
True
class vcspull.cli.add.RepoPlacement
class vcspull.cli.add.RepoPlacement

Bases: NamedTuple

Where a repository lands, for the preview and the write to agree on.

vcspull.cli.add._resolve_placement(workspace_root_input, repo_name, repo_path, *, url_mode, cwd)
function[source]
function[source]
vcspull.cli.add._resolve_placement(workspace_root_input, repo_name, repo_path, *, url_mode, cwd)

Derive the workspace label and destination path for a workspace root.

Called once the workspace root is settled — after the prompt, which may swap in a different declared root — so the preview cannot announce a workspace the entry is not written under.

Parameters:
  • workspace_root_input (str) – Workspace root as the user, the config, or the prompt supplied it.

  • repo_name (str) – Repository name that becomes the config key.

  • repo_path (pathlib.Path) – Existing checkout. Unused in URL mode, where nothing is on disk.

  • url_mode (bool)True when declaring from a URL rather than importing a checkout.

  • cwd (pathlib.Path) – Current working directory, for resolving relative roots.

Returns:

Label and destination path to preview and to write under.

Return type:

RepoPlacement

Examples

In URL mode the destination is the workspace root joined with the name:

>>> placement = _resolve_placement(
...     "~/code/",
...     "flask",
...     pathlib.Path("/nonexistent"),
...     url_mode=True,
...     cwd=pathlib.Path.cwd(),
... )
>>> placement.workspace_label
'~/code/'
>>> placement.display_path
'~/code/flask'

In path mode the destination is the checkout itself:

>>> checkout = tmp_path / "workspace" / "flask"
>>> placement = _resolve_placement(
...     str(tmp_path / "workspace"),
...     "flask",
...     checkout,
...     url_mode=False,
...     cwd=pathlib.Path.cwd(),
... )
>>> placement.display_path == str(checkout)
True
class vcspull.cli.add.ConfigFileResolution
class vcspull.cli.add.ConfigFileResolution

Bases: NamedTuple

Outcome of deciding which config file add should write to.

vcspull.cli.add._resolve_config_file(config_file_path_str)
function[source]
function[source]
vcspull.cli.add._resolve_config_file(config_file_path_str)

Resolve which config file add should write to.

Reports discovery outcomes rather than logging them, so callers that only need the path (to read declared workspace roots, say) do not emit the discovery messages a second time.

Parameters:

config_file_path_str (str | None) – Value of -f/--file, or None to discover a default.

Returns:

path is None only when ambiguous is True.

Return type:

ConfigFileResolution

Examples

An explicit -f/--file path is taken as given:

>>> resolution = _resolve_config_file(str(tmp_path / "custom.yaml"))
>>> resolution.path.name
'custom.yaml'
>>> resolution.creates_new_default, resolution.ambiguous
(False, False)

With no explicit path and no config in the home directory, add falls back to creating one in the current directory:

>>> resolution = _resolve_config_file(None)
>>> resolution.path.name
'.vcspull.yaml'
>>> resolution.creates_new_default
True
vcspull.cli.add._declared_workspace_labels(config_file_path)
function[source]
function[source]
vcspull.cli.add._declared_workspace_labels(config_file_path)

Return workspace root labels already declared in a config file.

Used to offer the workspace roots a user already keeps repositories under when adding by URL, where there is no parent directory to infer from. Duplicate labels collapse to their first occurrence, preserving file order.

Parameters:

config_file_path (pathlib.Path) – Config file to inspect. A missing or unreadable file yields [].

Returns:

Workspace root labels in the order they appear in the file.

Return type:

list[str]

Examples

A missing file has no declared roots:

>>> _declared_workspace_labels(tmp_path / "absent.yaml")
[]

Labels come back in file order, without duplicates:

>>> config_file = tmp_path / "declared.yaml"
>>> _ = config_file.write_text(
...     "~/code/:\n  a: git+https://example.com/a.git\n"
...     "~/study/:\n  b: git+https://example.com/b.git\n",
...     encoding="utf-8",
... )
>>> _declared_workspace_labels(config_file)
['~/code/', '~/study/']
vcspull.cli.add._build_ordered_items(top_level_items, raw_config)
function[source]
function[source]
vcspull.cli.add._build_ordered_items(top_level_items, raw_config)

Return deep-copied top-level items preserving original ordering.

Parameters:
Return type:

list[dict[str, Any]]

vcspull.cli.add._aggregate_from_ordered_items(items)
function[source]
function[source]
vcspull.cli.add._aggregate_from_ordered_items(items)

Collapse ordered top-level items into a mapping grouped by label.

Parameters:

items (list[dict[str, Any]])

Return type:

dict[str, Any]

vcspull.cli.add._collapse_ordered_items_to_dict(ordered_items)
function[source]
function[source]
vcspull.cli.add._collapse_ordered_items_to_dict(ordered_items)

Collapse ordered items into a flat dict for JSON serialization.

JSON does not support duplicate keys, so sections with the same workspace label are merged at the repo level via dict.update() (last occurrence of a repo name wins).

Examples

Distinct labels pass through unchanged:

>>> _collapse_ordered_items_to_dict([
...     {"label": "~/code/", "section": {"repo1": {"repo": "git+x"}}},
...     {"label": "~/work/", "section": {"repo2": {"repo": "git+y"}}},
... ])
{'~/code/': {'repo1': {'repo': 'git+x'}}, '~/work/': {'repo2': {'repo': 'git+y'}}}

Duplicate labels are merged (repos from both sections appear):

>>> result = _collapse_ordered_items_to_dict([
...     {"label": "~/code/", "section": {"repo1": {"repo": "git+a"}}},
...     {"label": "~/code/", "section": {"repo2": {"repo": "git+b"}}},
... ])
>>> sorted(result["~/code/"].keys())
['repo1', 'repo2']
Parameters:

ordered_items (list[dict[str, Any]])

Return type:

dict[str, Any]

vcspull.cli.add._collect_duplicate_sections(items)
function[source]
function[source]
vcspull.cli.add._collect_duplicate_sections(items)

Return mapping of labels to their repeated sections (>= 2 occurrences).

Parameters:

items (list[dict[str, Any]])

Return type:

dict[str, list[Any]]

vcspull.cli.add._save_ordered_items(config_file_path, ordered_items)
function[source]
function[source]
vcspull.cli.add._save_ordered_items(config_file_path, ordered_items)

Persist ordered items in the format matching the config file extension.

Parameters:
  • config_file_path (pathlib.Path) – Path to config file (.yaml or .json).

  • ordered_items (list of dict) – Each dict has "label" and "section" keys.

Return type:

None

Examples

YAML output:

>>> import pathlib
>>> config_file = tmp_path / "test.yaml"
>>> items = [{"label": "~/code/", "section": {"myrepo": "git+https://example.com/r.git"}}]
>>> _save_ordered_items(config_file, items)
>>> config_file.read_text().strip()
'~/code/...'

JSON output:

>>> config_file = tmp_path / "test.json"
>>> _save_ordered_items(config_file, items)
>>> import json
>>> data = json.loads(config_file.read_text())
>>> "~/code/" in data
True
vcspull.cli.add.handle_add_command(args)
function[source]
function[source]
vcspull.cli.add.handle_add_command(args)

Entry point for the vcspull add CLI command.

Parameters:

args (Namespace)

Return type:

None

vcspull.cli.add.add_repo(name, url, config_file_path_str, path, workspace_root_path, dry_run, *, merge_duplicates=True, rev=None, shallow=False, depth=None)
function[source]
function[source]
vcspull.cli.add.add_repo(name, url, config_file_path_str, path, workspace_root_path, dry_run, *, merge_duplicates=True, rev=None, shallow=False, depth=None)

Add a repository to the vcspull configuration.

Parameters:
  • name (str) – Repository name for the config

  • url (str) – Repository URL

  • config_file_path_str (str | None) – Path to config file, or None to use default

  • path (str | None) – Local path where repo will be cloned

  • workspace_root_path (str | None) – Workspace root to use in config

  • dry_run (bool) – If True, preview changes without writing

  • rev (str | None) – Commit, tag, or branch to record as options.rev.

  • shallow (bool) – If True, record options.shallow: true for the repository.

  • depth (int | None) – If set, record options.depth: N for the repository.

  • merge_duplicates (bool)

Return type:

None