@@ -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,80 @@ 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 .info (f'ingesting manifest from { source .name } : { source .location } /manifest.json ({ source .srctype } )' )
2101+ # local_path = RECKLESS_DIR / '.remote_sources' / user
2102+ if source .srctype is Source .GIT_LOCAL_CLONE :
2103+ try :
2104+ repo = GithubRepository (source .parent_source .original_source )
2105+ path = RECKLESS_DIR / '.remote_sources' / repo .user / repo .name
2106+ if source .relative :
2107+ path = path / source .relative
2108+ path = path / 'manifest.json'
2109+ if not path .exists ():
2110+ return None
2111+ with open (path , 'r+' ) as manifest_file :
2112+ try :
2113+ manifest = json .loads (manifest_file .read ())
2114+ return manifest
2115+ except json .decoder .JSONDecodeError :
2116+ log .warning (f'{ source .name } contains malformed manifest ({ source .parent_source .original_source } )' )
2117+ return None
2118+ except AssertionError :
2119+ log .info (f'could not parse github source { source .parent_source .original_source } ' )
2120+ return None
2121+ else :
2122+ log .info (f'oops! { source .srctype } ' )
2123+ return None
2124+
2125+
2126+ def find_plugin_candidates (source : Union [LoadedSource , SourceDir ], depth = 2 ) -> list :
20712127 """Filter through a source and return any candidates that appear to be
20722128 installable plugins with the registered installers."""
2129+ if isinstance (source , LoadedSource ):
2130+ if source .local_clone :
2131+ return find_plugin_candidates (source .local_clone )
2132+ return find_plugin_candidates (source .content )
2133+
20732134 candidates = []
20742135 assert isinstance (source , SourceDir )
20752136 if not source .contents and not source .prepopulated :
20762137 source .populate ()
2138+ for s in source .contents :
2139+ if isinstance (s , SourceDir ):
2140+ assert s .srctype == source .srctype , f'source dir { s .name } , { s .srctype } did not inherit { source .srctype } from { source .name } '
2141+ assert s .parent_source == source .parent_source , f'source dir { s .name } did not inherit parent { source .parent_source } from { source .name } '
20772142
20782143 guess = InstInfo (source .name , source .location , None , source_dir = source )
2144+ guess .srctype = source .srctype
2145+ manifest = None
20792146 if guess .get_inst_details ():
2080- candidates .append (source .name )
2147+ guess .srctype = source .srctype
2148+ guess .source_dir .srctype = source .srctype
2149+ if guess .source_dir .find ('manifest.json' ):
2150+ # FIXME: Handle github source case
2151+ if have_files (guess .source_dir ):
2152+ manifest = fetch_manifest (guess .source_dir )
2153+
2154+ if manifest :
2155+ candidate = manifest
2156+ else :
2157+ candidate = {'name' : source .name ,
2158+ 'short_description' : None ,
2159+ 'long_description' : None ,
2160+ 'entrypoint' : guess .entry ,
2161+ 'requirements' : []}
2162+ candidates .append (candidate )
20812163 if depth <= 1 :
20822164 return candidates
20832165
@@ -2106,21 +2188,27 @@ def available_plugins() -> list:
21062188 source .original_source ,
21072189 source_dir = source .content ),
21082190 verbose = False )
2191+ clone .srctype = Source .GIT_LOCAL_CLONE
2192+ clone .parent_source = source
21092193 if not clone :
21102194 log .warning (f"could not clone github source { source .original_source } " )
21112195 continue
21122196 source .local_clone = clone
21132197 source .local_clone .parent_source = source
21142198
2115- if source .local_clone :
2116- candidates .extend (find_plugin_candidates (source .local_clone ))
2117- else :
2118- candidates .extend (find_plugin_candidates (source .content ))
2199+ candidates .extend (find_plugin_candidates (source ))
2200+
2201+ # json output requested
2202+ if log .capture :
2203+ return candidates
2204+
2205+ for c in candidates :
2206+ log .info (c ['name' ])
2207+ if c ['short_description' ]:
2208+ log .info (f' description: { c ["short_description" ]} ' )
2209+ if c ['requirements' ]:
2210+ log .info (f' requirements: { c ["short_description" ]} ' )
21192211
2120- # Order and deduplicate results
2121- candidates = list (set (candidates ))
2122- candidates .sort ()
2123- log .info (' ' .join (candidates ))
21242212 return candidates
21252213
21262214
0 commit comments