Skip to content

Commit 6558173

Browse files
committed
refactor: Simplify add command argument parsing (Phase 2)
- Replace complex repo_name_or_url parsing with explicit name and url args - Remove 150+ lines of confusing comments and logic - Add clear --path and --dir options for specifying location - Make command usage intuitive: vcspull add <name> <url> - Reduce add_repo function from 270 to 163 lines - Improve help text and parameter documentation
1 parent 15264c8 commit 6558173

File tree

2 files changed

+71
-173
lines changed

2 files changed

+71
-173
lines changed

src/vcspull/cli/__init__.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -163,14 +163,11 @@ def cli(_args: list[str] | None = None) -> None:
163163
elif args.subparser_name == "add":
164164
all_parsers["add"]
165165
add_repo_kwargs = {
166-
"repo_name_or_url": args.repo_name_or_url,
166+
"name": args.name,
167+
"url": args.url,
167168
"config_file_path_str": args.config if hasattr(args, "config") else None,
168-
"target_path_str": args.target_path
169-
if hasattr(args, "target_path")
170-
else None,
171-
"base_dir_key": args.base_dir_key
172-
if hasattr(args, "base_dir_key")
173-
else None,
169+
"path": args.path if hasattr(args, "path") else None,
170+
"base_dir": args.base_dir if hasattr(args, "base_dir") else None,
174171
}
175172
add_repo(**add_repo_kwargs)
176173
elif args.subparser_name == "add-from-fs":

src/vcspull/cli/add.py

Lines changed: 67 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""CLI functionality for vcspull add."""
1+
"""Simplified CLI functionality for vcspull add."""
22

33
from __future__ import annotations
44

@@ -9,7 +9,6 @@
99
import yaml
1010

1111
from vcspull.config import (
12-
expand_dir,
1312
find_home_config_files,
1413
save_config_yaml,
1514
)
@@ -30,56 +29,70 @@ def create_add_subparser(parser: argparse.ArgumentParser) -> None:
3029
help="path to custom config file (default: .vcspull.yaml or ~/.vcspull.yaml)",
3130
)
3231
parser.add_argument(
33-
"repo_name_or_url",
34-
help="Name of the repository or its URL. "
35-
"If only name, URL may be inferred or prompted.",
32+
"name",
33+
help="Name for the repository in the config",
3634
)
3735
parser.add_argument(
38-
"target_path",
39-
nargs="?",
40-
help="Optional local path for the repository (can be relative or absolute). "
41-
"Overrides default naming.",
36+
"url",
37+
help="Repository URL (e.g., https://github.com/user/repo.git)",
4238
)
4339
parser.add_argument(
44-
"--base-dir-key",
45-
help="Specify the top-level directory key from vcspull config "
46-
"(e.g., '~/study/python/') under which to add this repo. "
47-
"If not given, vcspull will try to infer or use the current working directory.",
40+
"--path",
41+
dest="path",
42+
help="Local directory path where repo will be cloned "
43+
"(determines base directory key if not specified with --dir)",
44+
)
45+
parser.add_argument(
46+
"--dir",
47+
dest="base_dir",
48+
help="Base directory key in config (e.g., '~/projects/'). "
49+
"If not specified, will be inferred from --path or use current directory.",
4850
)
4951

5052

5153
def add_repo(
52-
repo_name_or_url: str,
54+
name: str,
55+
url: str,
5356
config_file_path_str: str | None,
54-
target_path_str: str | None = None,
55-
base_dir_key: str | None = None,
57+
path: str | None,
58+
base_dir: str | None,
5659
) -> None:
57-
"""Add a repository to the vcspull configuration."""
60+
"""Add a repository to the vcspull configuration.
61+
62+
Parameters
63+
----------
64+
name : str
65+
Repository name for the config
66+
url : str
67+
Repository URL
68+
config_file_path_str : str | None
69+
Path to config file, or None to use default
70+
path : str | None
71+
Local path where repo will be cloned
72+
base_dir : str | None
73+
Base directory key to use in config
74+
"""
75+
# Determine config file
5876
config_file_path: pathlib.Path
5977
if config_file_path_str:
6078
config_file_path = pathlib.Path(config_file_path_str).expanduser().resolve()
6179
else:
62-
# Default to ~/.vcspull.yaml if no global --config is passed
63-
# Behavior of find_home_config_files might need adjustment if it returns a list
6480
home_configs = find_home_config_files(filetype=["yaml"])
6581
if not home_configs:
66-
# If no global --config and no home config, default to
67-
# creating .vcspull.yaml in CWD
6882
config_file_path = pathlib.Path.cwd() / ".vcspull.yaml"
6983
log.info(
7084
f"No config specified and no default found, will create at "
7185
f"{config_file_path}",
7286
)
7387
elif len(home_configs) > 1:
74-
# This case should ideally be handled by the main CLI arg parsing
75-
# for --config
7688
log.error(
77-
"Multiple home_config files found, please specify one with -c/--config",
89+
"Multiple home config files found, please specify one with -c/--config",
7890
)
7991
return
8092
else:
8193
config_file_path = home_configs[0]
8294

95+
# Load existing config
8396
raw_config: dict[str, t.Any] = {}
8497
if config_file_path.exists() and config_file_path.is_file():
8598
try:
@@ -99,163 +112,51 @@ def add_repo(
99112
f"Config file {config_file_path} not found. A new one will be created.",
100113
)
101114

102-
repo_name: str
103-
repo_url: str
104-
105-
# Simplistic parsing of repo_name_or_url, assuming it's a URL if it
106-
# contains '://' or 'git@'. A more robust solution would involve libvcs or regex
107-
if "://" in repo_name_or_url or repo_name_or_url.startswith(
108-
("git@", "git+", "hg+", "svn+"),
109-
):
110-
repo_url = repo_name_or_url
111-
# Try to infer name from URL
112-
repo_name = pathlib.Path(repo_url.split("/")[-1]).stem
113-
if target_path_str:
114-
repo_name = pathlib.Path(
115-
target_path_str,
116-
).name # if target_path is specified, use its name for repo_name
117-
else:
118-
repo_name = repo_name_or_url
119-
# URL is not provided, for now, we can make it an error or set a
120-
# placeholder. In future, could try to infer from a local git repo if
121-
# target_path_str points to one
122-
log.error(
123-
"Repository URL must be provided if repo_name_or_url is not a URL. "
124-
"Functionality to infer URL is not yet implemented.",
125-
)
126-
# For now, let's assume a placeholder or require URL to be part of
127-
# repo_name_or_url. This part needs a decision: either make URL mandatory
128-
# with name, or implement inference. Let's require the user to provide a
129-
# URL for now by just using repo_name_or_url as URL if it seems like one.
130-
# This means the first arg must be a URL if only one arg is given and it's
131-
# not just a name. To simplify, let's assume if not detected as URL, then
132-
# it's just a name and we need target_path to be a URL. Or, the user
133-
# *must* provide two arguments: name then url. The current subparser takes
134-
# one `repo_name_or_url` and optional `target_path`. This argument parsing
135-
# needs refinement in `create_add_subparser` to accept name and url
136-
# separately. For now, this will likely fail if a plain name is given
137-
# without a clear URL source. Let's adjust the expectation: repo_name_or_url
138-
# IS THE URL, and repo_name is derived or taken from target_path. This means
139-
# first arg is always URL-like or repo name. Second is target_path (which
140-
# becomes name)
141-
142-
# Re-evaluating argument strategy based on user request for
143-
# `vcspull add my-repo <url>`: `repo_name_or_url` will be `my-repo`
144-
# `target_path` will be `<url>`. This means `create_add_subparser` needs
145-
# adjustment. For now, let's proceed with the current subparser and assume
146-
# `repo_name_or_url` is the name, and `target_path` (if provided) is the
147-
# URL for this specific case. This is getting complicated. Let's simplify
148-
# the add command for now: `vcspull add <NAME> <URL>`
149-
# The current `repo_name_or_url` and `target_path` can be repurposed.
150-
151-
# Let's rename parser args for clarity: repo_name, repo_url
152-
# This requires changing create_add_subparser and the cli call in __init__.py
153-
# Given the current structure, repo_name_or_url = actual name,
154-
# target_path = actual_url
155-
if target_path_str is None:
156-
log.error("If first argument is a name, second argument (URL) is required.")
157-
return
158-
repo_name = repo_name_or_url
159-
repo_url = target_path_str
160-
161-
# Determine the base directory key for the config
162-
actual_base_dir_key: str
163-
if base_dir_key:
164-
actual_base_dir_key = (
165-
base_dir_key if base_dir_key.endswith("/") else base_dir_key + "/"
166-
)
167-
elif target_path_str and not (
168-
"://" in target_path_str or target_path_str.startswith("git@")
169-
): # if target_path is a path, not url
170-
# Infer from target_path_str if it's a local path. This assumes
171-
# target_path_str is the actual clone path if provided and is not the URL
172-
# itself. This logic is getting tangled due to the argument overloading.
173-
# Let's stick to the idea that repo_name is derived or is first arg, url
174-
# is second or inferred. For simplicity, let's assume if base_dir_key is
175-
# not given, we use a default or CWD based key. This needs to be more
176-
# robust. Example: if repo is to be cloned to /home/user/study/python/myrepo
177-
# and vcspull.yaml has '~/study/python/': ..., then this is the key.
178-
# We can use os.path.commonpath or iterate through existing keys.
179-
# For now, default to current working directory's representation or a new
180-
# top-level key if no match.
181-
cwd_path = pathlib.Path.cwd()
182-
resolved_target_dir_parent = (
183-
pathlib.Path(target_path_str).parent.resolve()
184-
if target_path_str
185-
and not ("://" in target_path_str or target_path_str.startswith("git@"))
186-
else cwd_path
187-
)
188-
189-
found_key = None
190-
for key_path_str in raw_config:
191-
expanded_key_path = expand_dir(pathlib.Path(key_path_str), cwd_path)
192-
if (
193-
resolved_target_dir_parent == expanded_key_path
194-
or resolved_target_dir_parent.is_relative_to(expanded_key_path)
195-
):
196-
# Check if resolved_target_dir_parent is a subdirectory or same as
197-
# an existing key path
198-
try:
199-
if resolved_target_dir_parent.relative_to(expanded_key_path):
200-
found_key = key_path_str
201-
break
202-
except ValueError: # Not a sub-path
203-
if resolved_target_dir_parent == expanded_key_path:
204-
found_key = key_path_str
205-
break
206-
if found_key:
207-
actual_base_dir_key = found_key
208-
else:
209-
# Default to a key based on the target repo's parent directory or CWD
210-
# Make it relative to home if possible, or absolute
211-
try:
212-
actual_base_dir_key = (
213-
"~/"
214-
+ str(resolved_target_dir_parent.relative_to(pathlib.Path.home()))
215-
+ "/"
216-
)
217-
except ValueError:
218-
actual_base_dir_key = str(resolved_target_dir_parent) + "/"
115+
# Determine base directory key
116+
if base_dir:
117+
# Use explicit base directory
118+
base_dir_key = base_dir if base_dir.endswith("/") else base_dir + "/"
119+
elif path:
120+
# Infer from provided path
121+
repo_path = pathlib.Path(path).expanduser().resolve()
122+
try:
123+
# Try to make it relative to home
124+
base_dir_key = "~/" + str(repo_path.relative_to(pathlib.Path.home())) + "/"
125+
except ValueError:
126+
# Use absolute path
127+
base_dir_key = str(repo_path) + "/"
219128
else:
220-
# Default base directory key if not specified and target_path is not a
221-
# local path
222-
# This could be "." or a user-configurable default
223-
actual_base_dir_key = "./" # Default to relative current directory key
224-
225-
# Ensure trailing slash for consistency
226-
if not actual_base_dir_key.endswith("/"):
227-
actual_base_dir_key += "/"
129+
# Default to current directory
130+
base_dir_key = "./"
228131

229-
# Ensure the base directory key exists in the config
230-
if actual_base_dir_key not in raw_config:
231-
raw_config[actual_base_dir_key] = {}
232-
elif not isinstance(raw_config[actual_base_dir_key], dict):
132+
# Ensure base directory key exists in config
133+
if base_dir_key not in raw_config:
134+
raw_config[base_dir_key] = {}
135+
elif not isinstance(raw_config[base_dir_key], dict):
233136
log.error(
234-
f"Configuration section '{actual_base_dir_key}' is not a dictionary. "
137+
f"Configuration section '{base_dir_key}' is not a dictionary. "
235138
"Aborting.",
236139
)
237140
return
238141

239-
# Check if repo already exists under this base key
240-
if repo_name in raw_config[actual_base_dir_key]:
142+
# Check if repo already exists
143+
if name in raw_config[base_dir_key]:
241144
log.warning(
242-
f"Repository '{repo_name}' already exists under '{actual_base_dir_key}'."
243-
f" Current URL: {raw_config[actual_base_dir_key][repo_name]}."
244-
f" To update, remove and re-add, or edit the YAML file manually.",
145+
f"Repository '{name}' already exists under '{base_dir_key}'. "
146+
f"Current URL: {raw_config[base_dir_key][name]}. "
147+
f"To update, remove and re-add, or edit the YAML file manually.",
245148
)
246149
return
247150

248151
# Add the repository
249-
# Simplest form: {repo_name: repo_url}
250-
# More complex form (if remotes, etc., were supported):
251-
# {repo_name: {"url": repo_url, ...}}
252-
raw_config[actual_base_dir_key][repo_name] = repo_url
152+
raw_config[base_dir_key][name] = url
253153

154+
# Save config
254155
try:
255156
save_config_yaml(config_file_path, raw_config)
256157
log.info(
257-
f"Successfully added '{repo_name}' ({repo_url}) to {config_file_path} "
258-
f"under '{actual_base_dir_key}'.",
158+
f"Successfully added '{name}' ({url}) to {config_file_path} "
159+
f"under '{base_dir_key}'.",
259160
)
260161
except Exception:
261-
log.exception(f"Error saving config to {config_file_path}")
162+
log.exception(f"Error saving config to {config_file_path}")

0 commit comments

Comments
 (0)