Config - vcspull.config

Configuration functionality for vcspull.

vcspull.config.expand_dir(dir_, cwd=pathlib.Path.cwd)[source]

Return path with environmental variables and tilde ~ expanded.

Return type:

Path

Parameters:
Returns:

Absolute directory path

Return type:

pathlib.Path

vcspull.config._validate_worktrees_config(worktrees_raw, repo_name)[source]

Validate and normalize worktrees configuration.

Return type:

list[WorktreeConfigDict]

Parameters:
  • worktrees_raw (Any) – Raw worktrees configuration from YAML/JSON.

  • repo_name (str) – Name of the parent repository (for error messages).

Returns:

Validated list of worktree configurations.

Return type:

list[WorktreeConfigDict]

Raises:

VCSPullException – If the worktrees configuration is invalid.

Examples

Valid configuration with a tag:

>>> from vcspull.config import _validate_worktrees_config
>>> config = [{"dir": "../v1", "tag": "v1.0.0"}]
>>> result = _validate_worktrees_config(config, "myrepo")
>>> len(result)
1
>>> result[0]["dir"]
'../v1'
>>> result[0]["tag"]
'v1.0.0'

Valid configuration with a branch:

>>> config = [{"dir": "../dev", "branch": "develop"}]
>>> result = _validate_worktrees_config(config, "myrepo")
>>> result[0]["branch"]
'develop'

Valid configuration with a commit:

>>> config = [{"dir": "../fix", "commit": "abc123"}]
>>> result = _validate_worktrees_config(config, "myrepo")
>>> result[0]["commit"]
'abc123'

Error: worktrees must be a list:

>>> _validate_worktrees_config("not-a-list", "myrepo")
Traceback (most recent call last):
    ...
vcspull.exc.VCSPullException: ...worktrees must be a list, got str

Error: worktree entry must be a dict:

>>> _validate_worktrees_config(["not-a-dict"], "myrepo")
Traceback (most recent call last):
    ...
vcspull.exc.VCSPullException: ...must be a dict, got str

Error: missing required ‘dir’ field:

>>> _validate_worktrees_config([{"tag": "v1.0.0"}], "myrepo")
Traceback (most recent call last):
    ...
vcspull.exc.VCSPullException: ...missing required 'dir' field

Error: no ref type specified:

>>> _validate_worktrees_config([{"dir": "../wt"}], "myrepo")
Traceback (most recent call last):
    ...
vcspull.exc.VCSPullException: ...must specify one of: tag, branch, or commit

Error: empty ref value:

>>> _validate_worktrees_config([{"dir": "../wt", "tag": ""}], "myrepo")
Traceback (most recent call last):
    ...
vcspull.exc.VCSPullException: ...empty ref value...

Error: multiple refs specified:

>>> _validate_worktrees_config(
...     [{"dir": "../wt", "tag": "v1", "branch": "main"}], "myrepo"
... )
Traceback (most recent call last):
    ...
vcspull.exc.VCSPullException: ...cannot specify multiple refs...
vcspull.config.extract_repos(config, cwd=pathlib.Path.cwd)[source]

Return expanded configuration.

end-user configuration permit inline configuration shortcuts, expand to identical format for parsing.

Return type:

list[ConfigDict]

Parameters:
  • config (dict) – the repo config in dict format.

  • cwd (pathlib.Path) – current working dir (for deciphering relative paths)

Returns:

list

Return type:

List of normalized repository information

vcspull.config.find_home_config_files(filetype=None)[source]

Return configs of .vcspull.{yaml,json} in user’s home directory.

Return type:

list[Path]

Parameters:

filetype (list[str] | None)

vcspull.config.find_config_files(path=None, match=None, filetype=None, include_home=False)[source]

Return repos from a directory and match. Not recursive.

Return type:

list[Path]

Parameters:
  • path (list) – list of paths to search

  • match (list) – list of globs to search against

  • filetype (list) – of filetypes to search against

  • include_home (bool) – Include home configuration files

Raises:

LoadConfigRepoConflict : – There are two configs that have same path and name with different repo urls.

Returns:

list of absolute paths to config files.

Return type:

list

vcspull.config.load_configs(files, cwd=pathlib.Path.cwd, *, merge_duplicates=True)[source]

Return repos from a list of files.

Return type:

list[ConfigDict]

Parameters:
Returns:

expanded config dict item

Return type:

list of dict

vcspull.config.detect_duplicate_repos(config1, config2)[source]

Return duplicate repos dict if repo_dir same and vcs different.

Return type:

list[tuple[ConfigDict, ConfigDict]]

Parameters:
Returns:

List of duplicate tuples

Return type:

list[tuple[ConfigDict, ConfigDict]]

vcspull.config.in_dir(config_dir=None, extensions=None)[source]

Return a list of configs in config_dir.

Return type:

list[str]

Parameters:
  • config_dir (str) – directory to search

  • extensions (list) – filetypes to check (e.g. ['.yaml', '.json']).

Return type:

list

vcspull.config.filter_repos(config, path=None, vcs_url=None, name=None)[source]

Return a list list of repos from (expanded) config file.

path, vcs_url and name all support fnmatch.

Return type:

list[ConfigDict]

Parameters:
  • config (dict) – the expanded repo config in dict format.

  • path (str, Optional) – directory of checkout location, fnmatch pattern supported

  • vcs_url (str, Optional) – url of vcs remote, fn match pattern supported

  • name (str, Optional) – project name, fnmatch pattern supported

Returns:

Repos

Return type:

list

vcspull.config.is_config_file(filename, extensions=None)[source]

Return True if file has a valid config file type.

Return type:

bool

Parameters:
  • filename (str) – filename to check (e.g. mysession.json).

  • extensions (list or str) – filetypes to check (e.g. ['.yaml', '.json']).

Returns:

bool

Return type:

True if is a valid config file type

vcspull.config._atomic_write(target, content)[source]

Write content to a file atomically via temp-file-then-rename.

Return type:

None

Parameters:
  • target (pathlib.Path) – Destination file path

  • content (str) – Content to write

vcspull.config.save_config_yaml(config_file_path, data)[source]

Save configuration data to a YAML file.

Return type:

None

Parameters:
  • config_file_path (pathlib.Path) – Path to the configuration file to write

  • data (dict) – Configuration data to save

vcspull.config.save_config_json(config_file_path, data)[source]

Save configuration data to a JSON file.

Return type:

None

Parameters:
  • config_file_path (pathlib.Path) – Path to the configuration file to write

  • data (dict) – Configuration data to save

vcspull.config.save_config(config_file_path, data)[source]

Save configuration data, dispatching by file extension.

Return type:

None

Parameters:
  • config_file_path (pathlib.Path) – Path to the configuration file to write

  • data (dict) – Configuration data to save

Examples

>>> import pathlib, tempfile, json
>>> with tempfile.TemporaryDirectory() as tmp:
...     p = pathlib.Path(tmp) / "test.json"
...     save_config(p, {"~/code/": {"repo": {"repo": "git+https://x"}}})
...     loaded = json.loads(p.read_text())
...     loaded["~/code/"]["repo"]["repo"]
'git+https://x'
>>> with tempfile.TemporaryDirectory() as tmp:
...     p = pathlib.Path(tmp) / "test.yaml"
...     save_config(p, {"~/code/": {"repo": {"repo": "git+https://x"}}})
...     "repo" in p.read_text()
True
vcspull.config.save_config_yaml_with_items(config_file_path, items)[source]

Persist configuration data while preserving duplicate top-level sections.

Return type:

None

Parameters:
vcspull.config._VALID_OPS: frozenset[str] = frozenset({'add', 'discover', 'fmt', 'import', 'merge'})

Valid operation names for pin checking.

vcspull.config.is_pinned_for_op(entry, op)[source]

Return True if the repo config entry is pinned for op.

Return type:

bool

Parameters:
  • entry (Any) – Raw repo config value (string, dict, or None).

  • op (str) – Operation name: "import", "add", "discover", "fmt", or "merge".

Examples

Global pin applies to all ops:

>>> is_pinned_for_op({"repo": "git+x", "options": {"pin": True}}, "import")
True
>>> is_pinned_for_op({"repo": "git+x", "options": {"pin": True}}, "fmt")
True

Per-op pin is scoped:

>>> entry = {"repo": "git+x", "options": {"pin": {"import": True}}}
>>> is_pinned_for_op(entry, "import")
True
>>> is_pinned_for_op(entry, "fmt")
False

allow_overwrite: false is shorthand for pin: {import: true} (guards against --sync):

>>> entry2 = {"repo": "git+x", "options": {"allow_overwrite": False}}
>>> is_pinned_for_op(entry2, "import")
True
>>> is_pinned_for_op(entry2, "add")
False

Plain string entries and entries without options are never pinned:

>>> is_pinned_for_op("git+x", "import")
False
>>> is_pinned_for_op({"repo": "git+x"}, "import")
False

Explicit false is not pinned:

>>> is_pinned_for_op({"repo": "git+x", "options": {"pin": False}}, "import")
False

String values for pin (not bool) are ignored — not pinned:

>>> is_pinned_for_op({"repo": "git+x", "options": {"pin": "true"}}, "import")
False

Invalid op raises ValueError:

>>> is_pinned_for_op(
...     {"repo": "git+x"}, "bogus"
... )
Traceback (most recent call last):
    ...
ValueError: Unknown op: 'bogus'
vcspull.config.get_pin_reason(entry)[source]

Return the human-readable pin reason from a repo config entry.

Non-string values are coerced to str() so callers can safely interpolate the result into log messages.

Return type:

str | None

Parameters:

entry (Any)

Examples

>>> entry = {"repo": "git+x", "options": {"pin": True, "pin_reason": "pinned"}}
>>> get_pin_reason(entry)
'pinned'
>>> get_pin_reason({"repo": "git+x"}) is None
True
>>> get_pin_reason("git+x") is None
True

Non-string pin_reason is coerced:

>>> get_pin_reason({"repo": "git+x", "options": {"pin_reason": 42}})
'42'
class vcspull.config.MergeAction[source]

Bases: Enum

Action for resolving a duplicate workspace-root repo conflict.

KEEP_EXISTING = 'keep_existing'

First occurrence wins (standard behavior).

KEEP_INCOMING = 'keep_incoming'

Incoming pinned entry displaces unpinned existing entry.

vcspull.config._classify_merge_action(existing_entry, incoming_entry)[source]

Classify the merge conflict resolution action.

Return type:

MergeAction

Parameters:
  • existing_entry (Any) – The entry already stored (first occurrence).

  • incoming_entry (Any) – The duplicate entry being merged in.

Examples

Neither pinned — keep existing (first-occurrence-wins):

>>> _classify_merge_action({"repo": "git+a"}, {"repo": "git+b"})
<MergeAction.KEEP_EXISTING: 'keep_existing'>

Incoming is pinned — incoming wins:

>>> _classify_merge_action(
...     {"repo": "git+a"},
...     {"repo": "git+b", "options": {"pin": True}},
... )
<MergeAction.KEEP_INCOMING: 'keep_incoming'>

Existing is pinned — existing wins regardless:

>>> _classify_merge_action(
...     {"repo": "git+a", "options": {"pin": True}},
...     {"repo": "git+b"},
... )
<MergeAction.KEEP_EXISTING: 'keep_existing'>

Both pinned — first-occurrence-wins:

>>> _classify_merge_action(
...     {"repo": "git+a", "options": {"pin": True}},
...     {"repo": "git+b", "options": {"pin": True}},
... )
<MergeAction.KEEP_EXISTING: 'keep_existing'>
vcspull.config.merge_duplicate_workspace_root_entries(label, occurrences)[source]

Merge duplicate entries for a single workspace root.

Return type:

tuple[Any, list[str], int]

Parameters:
vcspull.config.merge_duplicate_workspace_roots(config_data, duplicate_roots)[source]

Merge duplicate workspace root sections captured during load.

Return type:

tuple[dict[str, Any], list[str], int, list[tuple[str, int]]]

Parameters:
vcspull.config.canonicalize_workspace_path(label, *, cwd=None)[source]

Convert a workspace root label to an absolute canonical path.

Return type:

Path

Parameters:
vcspull.config.workspace_root_label(workspace_path, *, cwd=None, home=None, preserve_cwd_label=True)[source]

Create a normalized label for a workspace root path.

Return type:

str

Parameters:
  • workspace_path (Path)

  • cwd (Path | None)

  • home (Path | None)

  • preserve_cwd_label (bool)

vcspull.config.normalize_workspace_roots(config_data, *, cwd=None, home=None, preserve_cwd_label=True)[source]

Normalize workspace root labels and merge duplicate sections.

Return type:

tuple[dict[str, Any], dict[Path, str], list[str], int]

Parameters: