Skip to content

Commit 6d2c423

Browse files
committed
feat: add configuration migration function to handle backward compatibility for output style
1 parent 3d57418 commit 6d2c423

File tree

2 files changed

+130
-2
lines changed

2 files changed

+130
-2
lines changed

src/repomix/config/config_load.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,41 @@
1313
from .global_directory import get_global_directory
1414

1515

16+
def migrate_config_format(config_dict: Dict[str, Any]) -> Dict[str, Any]:
17+
"""Migrate old configuration format to new format
18+
19+
This function handles backward compatibility by:
20+
- Converting '_style' to 'style' in output configuration
21+
- Any other future migrations can be added here
22+
23+
Args:
24+
config_dict: Configuration dictionary
25+
26+
Returns:
27+
Migrated configuration dictionary
28+
"""
29+
# Make a deep copy to avoid modifying the original
30+
import copy
31+
32+
migrated_config = copy.deepcopy(config_dict)
33+
34+
# Handle output._style -> output.style migration
35+
if "output" in migrated_config and isinstance(migrated_config["output"], dict):
36+
output_config = migrated_config["output"]
37+
38+
# If we have _style but no style, migrate it
39+
if "_style" in output_config and "style" not in output_config:
40+
output_config["style"] = output_config["_style"]
41+
del output_config["_style"]
42+
logger.info("Migrated configuration: '_style' -> 'style'")
43+
# If we have both, remove the old _style
44+
elif "_style" in output_config and "style" in output_config:
45+
del output_config["_style"]
46+
logger.info("Removed deprecated '_style' parameter (using 'style' instead)")
47+
48+
return migrated_config
49+
50+
1651
def load_config(
1752
directory: str | Path,
1853
cwd: str | Path,
@@ -61,6 +96,19 @@ def load_global_config() -> Optional[RepomixConfig]:
6196

6297
try:
6398
config_dict = json.loads(global_config_path.read_text(encoding="utf-8"))
99+
100+
# Migrate old configuration format
101+
config_dict = migrate_config_format(config_dict)
102+
103+
# Try to update the global config file if migration was needed
104+
try:
105+
updated_content = json.dumps(config_dict, indent=2, ensure_ascii=False)
106+
if updated_content != global_config_path.read_text(encoding="utf-8"):
107+
global_config_path.write_text(updated_content, encoding="utf-8")
108+
logger.info("Updated global configuration file to new format")
109+
except Exception as e:
110+
logger.warn(f"Failed to update global configuration file: {e}")
111+
64112
return RepomixConfig(**config_dict)
65113
except Exception as error:
66114
logger.warn(f"Failed to load global configuration: {error}")
@@ -99,6 +147,10 @@ def load_local_config(directory: str | Path, cwd: str | Path, config_path: Optio
99147

100148
try:
101149
config_dict = json.loads(config_path_obj.read_text(encoding="utf-8"))
150+
151+
# Migrate old configuration format
152+
config_dict = migrate_config_format(config_dict)
153+
102154
return RepomixConfig(**config_dict)
103155
except json.JSONDecodeError as error:
104156
raise RepomixError(f"Invalid configuration file format: {config_path_obj}") from error

tests/test_config.py

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
RepomixConfigOutput,
1010
RepomixOutputStyle,
1111
)
12+
from src.repomix.config.config_load import migrate_config_format
1213

1314

1415
class TestRepomixConfigOutput:
@@ -281,7 +282,7 @@ def test_advanced_output_options_configuration(self):
281282
"truncate_base64": True,
282283
"include_empty_directories": True,
283284
"include_diffs": True,
284-
"style": "xml"
285+
"style": "xml",
285286
}
286287
}
287288

@@ -309,12 +310,13 @@ def test_advanced_output_options_json_serialization(self):
309310
"truncate_base64": True,
310311
"include_empty_directories": False,
311312
"include_diffs": True,
312-
"copy_to_clipboard": True
313+
"copy_to_clipboard": True,
313314
}
314315
}
315316

316317
# Convert to JSON and back to simulate config file loading
317318
import json
319+
318320
json_str = json.dumps(original_config)
319321
loaded_dict = json.loads(json_str)
320322
config = RepomixConfig(**loaded_dict)
@@ -332,5 +334,79 @@ def test_advanced_output_options_json_serialization(self):
332334
assert config.output.copy_to_clipboard is True
333335

334336

337+
class TestConfigMigration:
338+
"""Test cases for configuration migration functionality"""
339+
340+
def test_style_migration_from_underscore_style(self):
341+
"""Test that _style is properly migrated to style"""
342+
old_config = {
343+
"output": {"file_path": "repomix-output.md", "_style": "markdown", "header_text": "", "remove_comments": False},
344+
"security": {"enable_security_check": True},
345+
}
346+
347+
migrated = migrate_config_format(old_config)
348+
349+
# Check that _style was converted to style
350+
assert "_style" not in migrated["output"]
351+
assert migrated["output"]["style"] == "markdown"
352+
353+
# Verify we can create RepomixConfig with migrated data
354+
config_obj = RepomixConfig(**migrated)
355+
assert config_obj.output.style == "markdown"
356+
assert config_obj.output.style_enum == RepomixOutputStyle.MARKDOWN
357+
358+
def test_style_migration_with_both_style_and_underscore_style(self):
359+
"""Test that _style is removed when both _style and style are present"""
360+
config_with_both = {"output": {"_style": "xml", "style": "markdown"}}
361+
362+
migrated = migrate_config_format(config_with_both)
363+
assert "_style" not in migrated["output"]
364+
assert migrated["output"]["style"] == "markdown"
365+
366+
def test_migration_preserves_new_format(self):
367+
"""Test that new config format without _style is unchanged"""
368+
new_config = {"output": {"style": "xml", "file_path": "output.xml"}}
369+
370+
migrated = migrate_config_format(new_config)
371+
assert migrated["output"]["style"] == "xml"
372+
assert "_style" not in migrated["output"]
373+
374+
def test_migration_with_real_user_config(self):
375+
"""Test with the actual problematic config from user"""
376+
user_config = {
377+
"output": {
378+
"file_path": "repomix-output.md",
379+
"_style": "markdown",
380+
"header_text": "",
381+
"instruction_file_path": "",
382+
"remove_comments": False,
383+
"remove_empty_lines": False,
384+
"top_files_length": 5,
385+
"show_line_numbers": False,
386+
"copy_to_clipboard": False,
387+
"include_empty_directories": False,
388+
"calculate_tokens": False,
389+
"show_file_stats": False,
390+
"show_directory_structure": True,
391+
},
392+
"security": {"enable_security_check": True, "exclude_suspicious_files": True},
393+
"ignore": {"custom_patterns": [], "use_gitignore": True, "use_default_ignore": True},
394+
"include": [],
395+
}
396+
397+
migrated = migrate_config_format(user_config)
398+
399+
# Verify migration
400+
assert "_style" not in migrated["output"]
401+
assert migrated["output"]["style"] == "markdown"
402+
403+
# Test that we can create RepomixConfig with migrated data
404+
config_obj = RepomixConfig(**migrated)
405+
assert config_obj.output.style == "markdown"
406+
assert config_obj.output.style_enum == RepomixOutputStyle.MARKDOWN
407+
assert config_obj.output.top_files_length == 5
408+
assert config_obj.security.enable_security_check is True
409+
410+
335411
if __name__ == "__main__":
336412
pytest.main([__file__])

0 commit comments

Comments
 (0)