@@ -233,6 +233,20 @@ def remove_dir(directory: str) -> bool:
233233 return False
234234
235235
236+ class GithubRepository ():
237+ """extract the github user account and repository name."""
238+ def __init__ (self , url : str ):
239+ assert 'github.com/' in url .lower ()
240+ url_parts = Path (str (url ).lower ().partition ('github.com/' )[2 ]).parts
241+ assert len (url_parts ) >= 2
242+ self .user = url_parts [0 ]
243+ self .name = url_parts [1 ].removesuffix ('.git' )
244+ self .url = url
245+
246+ def __repr__ (self ):
247+ return '<GithubRepository {self.user}/{self.repo}>'
248+
249+
236250class Source (Enum ):
237251 DIRECTORY = 1
238252 LOCAL_REPO = 2
@@ -262,12 +276,11 @@ class Source(Enum):
262276 @classmethod
263277 def get_github_user_repo (cls , source : str ) -> (str , str ):
264278 'extract a github username and repository name'
265- if 'github.com/' not in source . lower () :
266- return None , None
267- trailing = Path ( source . lower (). partition ( 'github.com/' )[ 2 ]). parts
268- if len ( trailing ) < 2 :
279+ try :
280+ repo = GithubRepository ( source )
281+ return repo . user , repo . name
282+ except :
269283 return None , None
270- return trailing [0 ], trailing [1 ].removesuffix ('.git' )
271284
272285
273286class SubmoduleSource :
@@ -325,7 +338,7 @@ class SourceDir():
325338 if self .srctype == Source .DIRECTORY :
326339 self .contents = populate_local_dir (self .location )
327340 elif self .srctype in [Source .LOCAL_REPO , Source .GIT_LOCAL_CLONE ]:
328- self .contents = populate_local_repo (self .location , parent_source = self .parent_source )
341+ self .contents = populate_local_repo (self .location , parent = self , parent_source = self .parent_source )
329342 elif self .srctype == Source .GITHUB_REPO :
330343 self .contents = populate_github_repo (self .location )
331344 else :
@@ -351,7 +364,7 @@ class SourceDir():
351364 return None
352365
353366 def __repr__ (self ):
354- return f"<SourceDir: { self .name } , { self .location } , { self .relative } >"
367+ return f"<SourceDir: { self .name } , { self .location } , { self .relative } , { self . parent_source } >"
355368
356369 def __eq__ (self , compared ):
357370 if isinstance (compared , str ):
@@ -576,7 +589,8 @@ def populate_local_repo(path: str, parent=None, parent_source=None) -> list:
576589 parentdir .name )
577590 else :
578591 relative_path = parentdir .name
579- child = SourceDir (p , srctype = Source .LOCAL_REPO ,
592+ child = SourceDir (p , srctype = parent .srctype ,
593+ parent_source = parent_source ,
580594 relative = relative_path )
581595 # ls-tree lists every file in the repo with full path.
582596 # No need to populate each directory individually.
@@ -611,8 +625,13 @@ def populate_local_repo(path: str, parent=None, parent_source=None) -> list:
611625 relative_path = filepath
612626 elif basedir .relative :
613627 relative_path = str (Path (basedir .relative ) / filepath )
614- assert relative_path
615- submodule_dir = SourceDir (filepath , srctype = Source .LOCAL_REPO ,
628+ else :
629+ relative_path = filepath
630+ if parent :
631+ srctype = parent .srctype
632+ else :
633+ srctype = Source .LOCAL_REPO
634+ submodule_dir = SourceDir (filepath , srctype = srctype ,
616635 relative = relative_path ,
617636 parent_source = parent_source )
618637 populate_local_repo (Path (path ) / filepath , parent = submodule_dir ,
@@ -710,7 +729,7 @@ def populate_github_repo(url: str) -> list:
710729 return contents
711730
712731
713- def copy_remote_git_source (github_source : InstInfo , verbose : bool = True ):
732+ def copy_remote_git_source (github_source : InstInfo , verbose : bool = True ) -> SourceDir :
714733 """clone or fetch & checkout a local copy of a remote git repo"""
715734 user , repo = Source .get_github_user_repo (github_source .source_loc )
716735 if not user or not repo :
@@ -2067,17 +2086,84 @@ def listinstalled():
20672086 return plugins
20682087
20692088
2070- def find_plugin_candidates (source : SourceDir , depth = 2 ) -> list :
2089+ def have_files (source : SourceDir ):
2090+ """Do we have direct access to the files in this directory?"""
2091+ if source .srctype in [Source .DIRECTORY , Source .LOCAL_REPO ,
2092+ Source .GIT_LOCAL_CLONE ]:
2093+ return True
2094+ log .info (f'no files in { source .name } ({ source .srctype } )' )
2095+ return False
2096+
2097+
2098+ def fetch_manifest (source : SourceDir ) -> dict :
2099+ """read and ingest a manifest from the provided source."""
2100+ log .debug (f'ingesting manifest from { source .name } : { source .location } /manifest.json ({ source .srctype } )' )
2101+ # local_path = RECKLESS_DIR / '.remote_sources' / user
2102+ if source .srctype not in [Source .GIT_LOCAL_CLONE , Source .LOCAL_REPO , Source .DIRECTORY ]:
2103+ log .info (f'oops! { source .srctype } ' )
2104+ return None
2105+ if source .srctype == Source .GIT_LOCAL_CLONE :
2106+ try :
2107+ repo = GithubRepository (source .parent_source .original_source )
2108+ path = RECKLESS_DIR / '.remote_sources' / repo .user / repo .name
2109+ except AssertionError :
2110+ log .info (f'could not parse github source { source .parent_source .original_source } ' )
2111+ return None
2112+ elif source .srctype in [Source .DIRECTORY , Source .LOCAL_REPO ]:
2113+ path = Path (source .location )
2114+ else :
2115+ raise Exception (f"cannot access manifest in { source .srctype } : { source } " )
2116+ if source .relative :
2117+ path = path / source .relative
2118+ path = path / 'manifest.json'
2119+ if not path .exists ():
2120+ return None
2121+ with open (path , 'r+' ) as manifest_file :
2122+ try :
2123+ manifest = json .loads (manifest_file .read ())
2124+ return manifest
2125+ except json .decoder .JSONDecodeError :
2126+ log .warning (f'{ source .name } contains malformed manifest ({ source .parent_source .original_source } )' )
2127+ return None
2128+
2129+
2130+ def find_plugin_candidates (source : Union [LoadedSource , SourceDir ], depth = 2 ) -> list :
20712131 """Filter through a source and return any candidates that appear to be
20722132 installable plugins with the registered installers."""
2133+ if isinstance (source , LoadedSource ):
2134+ if source .local_clone :
2135+ return find_plugin_candidates (source .local_clone )
2136+ return find_plugin_candidates (source .content )
2137+
20732138 candidates = []
20742139 assert isinstance (source , SourceDir )
20752140 if not source .contents and not source .prepopulated :
20762141 source .populate ()
2142+ for s in source .contents :
2143+ if isinstance (s , SourceDir ):
2144+ assert s .srctype == source .srctype , f'source dir { s .name } , { s .srctype } did not inherit { source .srctype } from { source .name } '
2145+ assert s .parent_source == source .parent_source , f'source dir { s .name } did not inherit parent { source .parent_source } from { source .name } '
20772146
20782147 guess = InstInfo (source .name , source .location , None , source_dir = source )
2148+ guess .srctype = source .srctype
2149+ manifest = None
20792150 if guess .get_inst_details ():
2080- candidates .append (source .name )
2151+ guess .srctype = source .srctype
2152+ guess .source_dir .srctype = source .srctype
2153+ if guess .source_dir .find ('manifest.json' ):
2154+ # FIXME: Handle github source case
2155+ if have_files (guess .source_dir ):
2156+ manifest = fetch_manifest (guess .source_dir )
2157+
2158+ if manifest :
2159+ candidate = manifest
2160+ else :
2161+ candidate = {'name' : source .name ,
2162+ 'short_description' : None ,
2163+ 'long_description' : None ,
2164+ 'entrypoint' : guess .entry ,
2165+ 'requirements' : []}
2166+ candidates .append (candidate )
20812167 if depth <= 1 :
20822168 return candidates
20832169
@@ -2106,21 +2192,27 @@ def available_plugins() -> list:
21062192 source .original_source ,
21072193 source_dir = source .content ),
21082194 verbose = False )
2195+ clone .srctype = Source .GIT_LOCAL_CLONE
2196+ clone .parent_source = source
21092197 if not clone :
21102198 log .warning (f"could not clone github source { source .original_source } " )
21112199 continue
21122200 source .local_clone = clone
21132201 source .local_clone .parent_source = source
21142202
2115- if source .local_clone :
2116- candidates .extend (find_plugin_candidates (source .local_clone ))
2117- else :
2118- candidates .extend (find_plugin_candidates (source .content ))
2203+ candidates .extend (find_plugin_candidates (source ))
2204+
2205+ # json output requested
2206+ if log .capture :
2207+ return candidates
2208+
2209+ for c in candidates :
2210+ log .info (c ['name' ])
2211+ if c ['short_description' ]:
2212+ log .info (f'\t description: { c ["short_description" ]} ' )
2213+ if c ['requirements' ]:
2214+ log .info (f'\t requirements: { c ["requirements" ]} ' )
21192215
2120- # Order and deduplicate results
2121- candidates = list (set (candidates ))
2122- candidates .sort ()
2123- log .info (' ' .join (candidates ))
21242216 return candidates
21252217
21262218
0 commit comments