Skip to content

Commit a66af9b

Browse files
authored
Merge pull request #914 from isdaniel/smart-merge-settings.json
Smart JSON Merging for VS Code Settings `.vscode/settings.json`
2 parents a5fdd53 + 8130d98 commit a66af9b

File tree

1 file changed

+72
-1
lines changed

1 file changed

+72
-1
lines changed

src/specify_cli/__init__.py

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,73 @@ def init_git_repo(project_path: Path, quiet: bool = False) -> Tuple[bool, Option
485485
finally:
486486
os.chdir(original_cwd)
487487

488+
def handle_vscode_settings(sub_item, dest_file, rel_path, verbose=False, tracker=None) -> None:
489+
"""Handle merging or copying of .vscode/settings.json files."""
490+
def log(message, color="green"):
491+
if verbose and not tracker:
492+
console.print(f"[{color}]{message}[/] {rel_path}")
493+
494+
try:
495+
with open(sub_item, 'r', encoding='utf-8') as f:
496+
new_settings = json.load(f)
497+
498+
if dest_file.exists():
499+
merged = merge_json_files(dest_file, new_settings, verbose=verbose and not tracker)
500+
with open(dest_file, 'w', encoding='utf-8') as f:
501+
json.dump(merged, f, indent=4)
502+
f.write('\n')
503+
log("Merged:", "green")
504+
else:
505+
shutil.copy2(sub_item, dest_file)
506+
log("Copied (no existing settings.json):", "blue")
507+
508+
except Exception as e:
509+
log(f"Warning: Could not merge, copying instead: {e}", "yellow")
510+
shutil.copy2(sub_item, dest_file)
511+
512+
def merge_json_files(existing_path: Path, new_content: dict, verbose: bool = False) -> dict:
513+
"""Merge new JSON content into existing JSON file.
514+
515+
Performs a deep merge where:
516+
- New keys are added
517+
- Existing keys are preserved unless overwritten by new content
518+
- Nested dictionaries are merged recursively
519+
- Lists and other values are replaced (not merged)
520+
521+
Args:
522+
existing_path: Path to existing JSON file
523+
new_content: New JSON content to merge in
524+
verbose: Whether to print merge details
525+
526+
Returns:
527+
Merged JSON content as dict
528+
"""
529+
try:
530+
with open(existing_path, 'r', encoding='utf-8') as f:
531+
existing_content = json.load(f)
532+
except (FileNotFoundError, json.JSONDecodeError):
533+
# If file doesn't exist or is invalid, just use new content
534+
return new_content
535+
536+
def deep_merge(base: dict, update: dict) -> dict:
537+
"""Recursively merge update dict into base dict."""
538+
result = base.copy()
539+
for key, value in update.items():
540+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
541+
# Recursively merge nested dictionaries
542+
result[key] = deep_merge(result[key], value)
543+
else:
544+
# Add new key or replace existing value
545+
result[key] = value
546+
return result
547+
548+
merged = deep_merge(existing_content, new_content)
549+
550+
if verbose:
551+
console.print(f"[cyan]Merged JSON file:[/cyan] {existing_path.name}")
552+
553+
return merged
554+
488555
def download_template_from_github(ai_assistant: str, download_dir: Path, *, script_type: str = "sh", verbose: bool = True, show_progress: bool = True, client: httpx.Client = None, debug: bool = False, github_token: str = None) -> Tuple[Path, dict]:
489556
repo_owner = "github"
490557
repo_name = "spec-kit"
@@ -676,7 +743,11 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_
676743
rel_path = sub_item.relative_to(item)
677744
dest_file = dest_path / rel_path
678745
dest_file.parent.mkdir(parents=True, exist_ok=True)
679-
shutil.copy2(sub_item, dest_file)
746+
# Special handling for .vscode/settings.json - merge instead of overwrite
747+
if dest_file.name == "settings.json" and dest_file.parent.name == ".vscode":
748+
handle_vscode_settings(sub_item, dest_file, rel_path, verbose, tracker)
749+
else:
750+
shutil.copy2(sub_item, dest_file)
680751
else:
681752
shutil.copytree(item, dest_path)
682753
else:

0 commit comments

Comments
 (0)