99import logging
1010import os
1111import pathlib
12+ import typing as t
1213from typing import Literal , Optional , Union
1314
1415import kaptan
1516
16- from libvcs .projects .git import GitRemote
17+ from libvcs ._internal .types import StrPath
18+ from libvcs .sync .git import GitRemote
1719
1820from . import exc
21+ from .types import ConfigDict , RawConfigDict
1922from .util import get_config_dir , update_dict
2023
2124log = logging .getLogger (__name__ )
2225
26+ if t .TYPE_CHECKING :
27+ from typing_extensions import TypeGuard
28+
2329
2430def expand_dir (
2531 _dir : pathlib .Path , cwd : pathlib .Path = pathlib .Path .cwd ()
@@ -45,7 +51,7 @@ def expand_dir(
4551 return _dir
4652
4753
48- def extract_repos (config : dict , cwd = pathlib .Path .cwd ()) -> list [dict ]:
54+ def extract_repos (config : RawConfigDict , cwd = pathlib .Path .cwd ()) -> list [ConfigDict ]:
4955 """Return expanded configuration.
5056
5157 end-user configuration permit inline configuration shortcuts, expand to
@@ -62,11 +68,11 @@ def extract_repos(config: dict, cwd=pathlib.Path.cwd()) -> list[dict]:
6268 -------
6369 list : List of normalized repository information
6470 """
65- configs = []
71+ configs : list [ ConfigDict ] = []
6672 for directory , repos in config .items ():
73+ assert isinstance (repos , dict )
6774 for repo , repo_data in repos .items ():
68-
69- conf = {}
75+ conf : dict = {}
7076
7177 """
7278 repo_name: http://myrepo.com/repo.git
@@ -91,21 +97,36 @@ def extract_repos(config: dict, cwd=pathlib.Path.cwd()) -> list[dict]:
9197
9298 if "name" not in conf :
9399 conf ["name" ] = repo
94- if "parent_dir" not in conf :
95- conf ["parent_dir" ] = expand_dir (directory , cwd = cwd )
96-
97- # repo_dir -> dir in libvcs 0.12.0b25
98- if "repo_dir" in conf and "dir" not in conf :
99- conf ["dir" ] = conf .pop ("repo_dir" )
100100
101101 if "dir" not in conf :
102- conf ["dir" ] = expand_dir (conf ["parent_dir" ] / conf ["name" ], cwd )
102+ conf ["dir" ] = expand_dir (
103+ pathlib .Path (expand_dir (pathlib .Path (directory ), cwd = cwd ))
104+ / conf ["name" ],
105+ cwd ,
106+ )
103107
104108 if "remotes" in conf :
109+ assert isinstance (conf ["remotes" ], dict )
105110 for remote_name , url in conf ["remotes" ].items ():
106- conf ["remotes" ][remote_name ] = GitRemote (
107- name = remote_name , fetch_url = url , push_url = url
108- )
111+ if isinstance (url , GitRemote ):
112+ continue
113+ if isinstance (url , str ):
114+ conf ["remotes" ][remote_name ] = GitRemote (
115+ name = remote_name , fetch_url = url , push_url = url
116+ )
117+ elif isinstance (url , dict ):
118+ assert "push_url" in url
119+ assert "fetch_url" in url
120+ conf ["remotes" ][remote_name ] = GitRemote (
121+ name = remote_name , ** url
122+ )
123+
124+ def is_valid_config_dict (val : t .Any ) -> "TypeGuard[ConfigDict]" :
125+ assert isinstance (val , dict )
126+ return True
127+
128+ assert is_valid_config_dict (conf )
129+
109130 configs .append (conf )
110131
111132 return configs
@@ -192,12 +213,12 @@ def find_config_files(
192213 configs .extend (find_config_files (path , match , f ))
193214 else :
194215 match = f"{ match } .{ filetype } "
195- configs = path .glob (match )
216+ configs = list ( path .glob (match ) )
196217
197218 return configs
198219
199220
200- def load_configs (files : list [Union [ str , pathlib . Path ] ], cwd = pathlib .Path .cwd ()):
221+ def load_configs (files : list [StrPath ], cwd = pathlib .Path .cwd ()):
201222 """Return repos from a list of files.
202223
203224 Parameters
@@ -216,10 +237,11 @@ def load_configs(files: list[Union[str, pathlib.Path]], cwd=pathlib.Path.cwd()):
216237 ----
217238 Validate scheme, check for duplicate destinations, VCS urls
218239 """
219- repos = []
240+ repos : list [ ConfigDict ] = []
220241 for file in files :
221242 if isinstance (file , str ):
222243 file = pathlib .Path (file )
244+ assert isinstance (file , pathlib .Path )
223245 ext = file .suffix .lstrip ("." )
224246 conf = kaptan .Kaptan (handler = ext ).import_config (str (file ))
225247 newrepos = extract_repos (conf .export ("dict" ), cwd = cwd )
@@ -230,51 +252,49 @@ def load_configs(files: list[Union[str, pathlib.Path]], cwd=pathlib.Path.cwd()):
230252
231253 dupes = detect_duplicate_repos (repos , newrepos )
232254
233- if dupes :
255+ if len ( dupes ) > 0 :
234256 msg = ("repos with same path + different VCS detected!" , dupes )
235257 raise exc .VCSPullException (msg )
236258 repos .extend (newrepos )
237259
238260 return repos
239261
240262
241- def detect_duplicate_repos (repos1 : list [dict ], repos2 : list [dict ]):
263+ ConfigDictTuple = tuple [ConfigDict , ConfigDict ]
264+
265+
266+ def detect_duplicate_repos (
267+ config1 : list [ConfigDict ], config2 : list [ConfigDict ]
268+ ) -> list [ConfigDictTuple ]:
242269 """Return duplicate repos dict if repo_dir same and vcs different.
243270
244271 Parameters
245272 ----------
246- repos1 : dict
247- list of repo expanded dicts
273+ config1 : list[ConfigDict]
248274
249- repos2 : dict
250- list of repo expanded dicts
275+ config2 : list[ConfigDict]
251276
252277 Returns
253278 -------
254- list of dict, or None
255- Duplicate repos
279+ list[ConfigDictTuple]
280+ List of duplicate tuples
256281 """
257- dupes = []
258- path_dupe_repos = []
282+ if not config1 :
283+ return []
259284
260- curpaths = [r ["dir" ] for r in repos1 ]
261- newpaths = [r ["dir" ] for r in repos2 ]
262- path_duplicates = list (set (curpaths ).intersection (newpaths ))
285+ dupes : list [ConfigDictTuple ] = []
263286
264- if not path_duplicates :
265- return None
287+ repo_dirs = {
288+ pathlib .Path (repo ["dir" ]).parent / repo ["name" ]: repo for repo in config1
289+ }
290+ repo_dirs_2 = {
291+ pathlib .Path (repo ["dir" ]).parent / repo ["name" ]: repo for repo in config2
292+ }
266293
267- path_dupe_repos . extend (
268- [ r for r in repos2 if any ( r [ "dir" ] == p for p in path_duplicates )]
269- )
294+ for repo_dir , repo in repo_dirs . items ():
295+ if repo_dir in repo_dirs_2 :
296+ dupes . append (( repo , repo_dirs_2 [ repo_dir ]) )
270297
271- if not path_dupe_repos :
272- return None
273-
274- for n in path_dupe_repos :
275- currepo = next ((r for r in repos1 if r ["dir" ] == n ["dir" ]), None )
276- if n ["url" ] != currepo ["url" ]:
277- dupes += (n , currepo )
278298 return dupes
279299
280300
@@ -304,11 +324,11 @@ def in_dir(config_dir=None, extensions: list[str] = [".yml", ".yaml", ".json"]):
304324
305325
306326def filter_repos (
307- config : dict ,
308- dir : Union [pathlib .Path , None ] = None ,
327+ config : list [ ConfigDict ] ,
328+ dir : Union [pathlib .Path , Literal [ "*" ], None ] = None ,
309329 vcs_url : Union [str , None ] = None ,
310330 name : Union [str , None ] = None ,
311- ):
331+ ) -> list [ ConfigDict ] :
312332 """Return a :py:obj:`list` list of repos from (expanded) config file.
313333
314334 dir, vcs_url and name all support fnmatch.
@@ -329,23 +349,35 @@ def filter_repos(
329349 list :
330350 Repos
331351 """
332- repo_list = []
352+ repo_list : list [ ConfigDict ] = []
333353
334354 if dir :
335- repo_list .extend ([r for r in config if fnmatch .fnmatch (r ["parent_dir" ], dir )])
355+ repo_list .extend (
356+ [
357+ r
358+ for r in config
359+ if fnmatch .fnmatch (str (pathlib .Path (r ["dir" ]).parent ), str (dir ))
360+ ]
361+ )
336362
337363 if vcs_url :
338364 repo_list .extend (
339- r for r in config if fnmatch .fnmatch (r .get ("url" , r .get ("repo" )), vcs_url )
365+ r
366+ for r in config
367+ if fnmatch .fnmatch (str (r .get ("url" , r .get ("repo" ))), vcs_url )
340368 )
341369
342370 if name :
343- repo_list .extend ([r for r in config if fnmatch .fnmatch (r .get ("name" ), name )])
371+ repo_list .extend (
372+ [r for r in config if fnmatch .fnmatch (str (r .get ("name" )), name )]
373+ )
344374
345375 return repo_list
346376
347377
348- def is_config_file (filename : str , extensions : list [str ] = [".yml" , ".yaml" , ".json" ]):
378+ def is_config_file (
379+ filename : str , extensions : Union [list [str ], str ] = [".yml" , ".yaml" , ".json" ]
380+ ):
349381 """Return True if file has a valid config file type.
350382
351383 Parameters
0 commit comments