1414import typing as t
1515from collections .abc import Callable
1616
17+ from libvcs .cmd .git_filter import Combine , parse_filter
1718from libvcs .sync .git import GitRemote
1819
1920from vcspull .validator import (
2021 is_valid_config ,
22+ validate_legacy_options ,
23+ validate_metadata ,
2124 validate_repo_entry ,
2225 validate_working_copy ,
2326)
2932 config_format_from_path ,
3033)
3134from .types import ConfigDict , RawConfigDict , WorktreeConfigDict
32- from .util import get_config_dir , update_dict
35+ from .util import get_config_dir
3336
3437log = logging .getLogger (__name__ )
3538
@@ -269,22 +272,22 @@ def extract_repos(
269272 if isinstance (repo_data , str ):
270273 conf ["url" ] = repo_data
271274 else :
272- conf = update_dict (conf , repo_data )
273-
274- if "repo" in conf :
275- if "url" not in conf :
276- conf ["url" ] = conf .pop ("repo" )
277- else :
278- conf .pop ("repo" , None )
275+ conf = copy .deepcopy (repo_data )
279276
280277 location = f"{ directory !r} -> { repo !r} "
281278 try :
282- _ , conf = migrate_repo_entry (conf )
279+ _ , conf = migrate_repo_entry (conf , normalize_filters = False )
283280 except (TypeError , ValueError ) as error :
284281 msg = f"{ location } .{ error } "
285282 raise exc .VCSPullException (msg ) from error
286283 validate_repo_entry (conf , location = location )
287284
285+ if "repo" in conf :
286+ if "url" not in conf :
287+ conf ["url" ] = conf .pop ("repo" )
288+ else :
289+ conf .pop ("repo" , None )
290+
288291 if "name" not in conf :
289292 conf ["name" ] = repo
290293
@@ -969,9 +972,8 @@ def resolve_clone_depth(
969972
970973 1. ``explicit_depth`` (from ``--depth N``) → ``(False, explicit_depth)``.
971974 2. ``explicit_shallow`` (from ``--shallow``) → ``(True, None)``.
972- 3. Auto-detected depth (hybrid): a depth-1 checkout records ``shallow:
973- true`` (the common case), depth > 1 records ``depth: N``, and a full
974- checkout records neither.
975+ 3. Auto-detected depth: shallow checkouts record ``git.depth: N``;
976+ full checkouts omit the depth setting.
975977
976978 Parameters
977979 ----------
@@ -1079,7 +1081,47 @@ def build_repo_entry(
10791081LEGACY_REPO_OPTION_KEYS = ("rev" , "shallow" , "depth" )
10801082
10811083
1082- def migrate_repo_entry (entry : t .Any ) -> tuple [bool , t .Any ]:
1084+ def _migrate_filter_config (value : t .Any , * , depth : int = 0 ) -> t .Any :
1085+ """Express native combinations without nested percent encoding.
1086+
1087+ >>> _migrate_filter_config("combine:blob:none+tree:1")
1088+ {'kind': 'combine', 'filters': ['blob:none', 'tree:1']}
1089+ """
1090+ if depth > 33 :
1091+ msg = "filter nesting exceeds 32 levels"
1092+ raise ValueError (msg )
1093+ if isinstance (value , str ) and value .startswith ("combine:" ):
1094+ value = parse_filter (value )
1095+ if isinstance (value , Combine ):
1096+ return {
1097+ "kind" : "combine" ,
1098+ "filters" : [
1099+ _migrate_filter_config (child , depth = depth + 1 )
1100+ if isinstance (child , Combine )
1101+ else child .to_spec ()
1102+ for child in value .filters
1103+ ],
1104+ }
1105+ if isinstance (value , list ):
1106+ return [_migrate_filter_config (child , depth = depth + 1 ) for child in value ]
1107+ if (
1108+ isinstance (value , dict )
1109+ and value .get ("kind" ) == "combine"
1110+ and isinstance (value .get ("filters" ), list )
1111+ ):
1112+ return {
1113+ ** value ,
1114+ "filters" : [
1115+ _migrate_filter_config (child , depth = depth + 1 )
1116+ for child in value ["filters" ]
1117+ ],
1118+ }
1119+ return value
1120+
1121+
1122+ def migrate_repo_entry (
1123+ entry : t .Any , * , normalize_filters : bool = True
1124+ ) -> tuple [bool , t .Any ]:
10831125 """Separate checkout targets, backend settings, and entry policy.
10841126
10851127 Canonical values win over legacy values. Within the legacy layout,
@@ -1090,6 +1132,9 @@ def migrate_repo_entry(entry: t.Any) -> tuple[bool, t.Any]:
10901132 ----------
10911133 entry : Any
10921134 A raw repository entry (string shorthand or mapping).
1135+ normalize_filters : bool
1136+ Rewrite native combined filters for serialization. Loading disables
1137+ this conversion so unsupported syntax fails at its source.
10931138
10941139 Returns
10951140 -------
@@ -1121,17 +1166,31 @@ def migrate_repo_entry(entry: t.Any) -> tuple[bool, t.Any]:
11211166 if not isinstance (entry , dict ):
11221167 return False , entry
11231168
1169+ if "metadata" in entry :
1170+ try :
1171+ validate_metadata (entry ["metadata" ])
1172+ except exc .VCSPullException as error :
1173+ raise ValueError (str (error )) from error
1174+
11241175 legacy_keys = (
11251176 * LEGACY_REPO_OPTION_KEYS ,
11261177 "options" ,
11271178 "git_options" ,
11281179 "hg_options" ,
11291180 "svn_options" ,
11301181 )
1131- if not any (key in entry for key in legacy_keys ):
1132- return False , entry
1133-
11341182 new_entry = copy .deepcopy (entry )
1183+ if normalize_filters :
1184+ for key in ("git" , "git_options" ):
1185+ options = new_entry .get (key )
1186+ if isinstance (options , dict ) and "filter" in options :
1187+ options ["filter" ] = _migrate_filter_config (options ["filter" ])
1188+ if not any (key in new_entry for key in legacy_keys ):
1189+ return new_entry != entry , new_entry
1190+ try :
1191+ validate_legacy_options (new_entry )
1192+ except exc .VCSPullException as error :
1193+ raise ValueError (str (error )) from error
11351194 options = new_entry .pop ("options" , {})
11361195 if not isinstance (options , dict ):
11371196 msg = "options: expected a mapping"
0 commit comments