Skip to content

Commit a1bc87f

Browse files
reckless: remove github API access
The shebang installer requires introspection of files, and the listavailable command reads the manifest for each plugin. Due to the need for file access, cloning all remote repositories is now simpler and faster, so it's time to rip out the github API access code.
1 parent 97fb02d commit a1bc87f

1 file changed

Lines changed: 50 additions & 134 deletions

File tree

tools/reckless

Lines changed: 50 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ class GithubRepository():
255255
class Source(Enum):
256256
DIRECTORY = 1
257257
LOCAL_REPO = 2
258-
GITHUB_REPO = 3
258+
REMOTE_GIT_REPO = 3
259259
OTHER_URL = 4
260260
UNKNOWN = 5
261261
# Cloned from remote source before searching (rather than github API)
@@ -309,11 +309,14 @@ class LoadedSource:
309309
self.content = SourceDir(source, self.type)
310310
self.local_clone = None
311311
self.local_clone_fetched = False
312-
if self.type == Source.GITHUB_REPO:
312+
if self.type == Source.REMOTE_GIT_REPO:
313313
local = _get_local_clone(source)
314314
if local:
315315
self.local_clone = SourceDir(local, Source.GIT_LOCAL_CLONE)
316-
self.local_clone.parent_source = self
316+
else:
317+
self.local_clone = copy_remote_git_source(InstInfo(None, source))
318+
self.content = self.local_clone
319+
self.local_clone.parent_source = self
317320

318321
def __repr__(self):
319322
return f'<Source {self.type}, {self.original_source}>'
@@ -344,8 +347,9 @@ class SourceDir():
344347
self.contents = populate_local_dir(self.location)
345348
elif self.srctype in [Source.LOCAL_REPO, Source.GIT_LOCAL_CLONE]:
346349
self.contents = populate_local_repo(self.location, parent=self, parent_source=self.parent_source)
347-
elif self.srctype == Source.GITHUB_REPO:
348-
self.contents = populate_github_repo(self.location)
350+
elif self.srctype == Source.REMOTE_GIT_REPO:
351+
self.contents = copy_remote_git_source(InstInfo(self.name, self.location)).contents
352+
349353
else:
350354
raise Exception("populate method undefined for {self.srctype}")
351355
# Ensure the relative path of the contents is inherited.
@@ -398,19 +402,18 @@ class SourceFile():
398402

399403

400404
class InstInfo:
401-
def __init__(self, name: str, location: str, git_url: str, source_dir: SourceDir=None):
405+
def __init__(self, name: str, location: str, source_dir: SourceDir=None):
402406
self.name = name
403407
self.source_loc = str(location) # Used for 'git clone'
404408
self.source_dir = source_dir # Use this insead of source_loc to only fetch once.
405-
self.git_url: str = git_url # API access for github repos
406409
self.srctype: Source = Source.get_type(location)
407410
self.entry: SourceFile = None # relative to source_loc or subdir
408411
self.deps: str = None
409412
self.subdir: str = None
410413
self.commit: str = None
411414

412415
def __repr__(self):
413-
return (f'InstInfo({self.name}, {self.source_loc}, {self.git_url}, '
416+
return (f'InstInfo({self.name}, {self.source_loc}, '
414417
f'{self.entry}, {self.deps}, {self.subdir})')
415418

416419
def get_repo_commit(self) -> Union[str, None]:
@@ -422,26 +425,9 @@ class InstInfo:
422425
return None
423426
return git.stdout.splitlines()[0]
424427

425-
if self.srctype == Source.GITHUB_REPO:
426-
parsed_url = urlparse(self.source_loc)
427-
if 'github.com' not in parsed_url.netloc:
428-
return None
429-
if len(parsed_url.path.split('/')) < 2:
430-
return None
431-
start = 1
432-
# Maybe we were passed an api.github.com/repo/<user> url
433-
if 'api' in parsed_url.netloc:
434-
start += 1
435-
repo_user = parsed_url.path.split('/')[start]
436-
repo_name = parsed_url.path.split('/')[start + 1]
437-
api_url = f'{API_GITHUB_COM}/repos/{repo_user}/{repo_name}/commits?ref=HEAD'
438-
r = urlopen(api_url, timeout=5)
439-
if r.status != 200:
440-
return None
441-
try:
442-
return json.loads(r.read().decode())['0']['sha']
443-
except:
444-
return None
428+
if self.srctype == Source.REMOTE_GIT_REPO:
429+
# The remote git source is not accessed directly. Use the local clone.
430+
assert False
445431

446432
def get_inst_details(self, permissive: bool=False) -> bool:
447433
"""Search the source_loc for plugin install details.
@@ -461,7 +447,7 @@ class InstInfo:
461447
if self.srctype in [Source.DIRECTORY, Source.LOCAL_REPO,
462448
Source.GIT_LOCAL_CLONE]:
463449
depth = 5
464-
elif self.srctype == Source.GITHUB_REPO:
450+
elif self.srctype == Source.REMOTE_GIT_REPO:
465451
depth = 1
466452

467453
def search_dir(self, sub: SourceDir, subdir: bool,
@@ -522,7 +508,7 @@ class InstInfo:
522508
# Fall back to cloning and searching the local copy instead.
523509
except HTTPError:
524510
result = None
525-
if self.srctype == Source.GITHUB_REPO:
511+
if self.srctype == Source.REMOTE_GIT_REPO:
526512
# clone source to reckless dir
527513
target = copy_remote_git_source(self)
528514
if not target:
@@ -654,91 +640,6 @@ def populate_local_repo(path: str, parent=None, parent_source=None) -> list:
654640
return basedir.contents
655641

656642

657-
def source_element_from_repo_api(member: dict):
658-
# api accessed via <repo>/contents/
659-
if 'type' in member and 'name' in member and 'git_url' in member:
660-
if member['type'] == 'dir':
661-
return SourceDir(member['git_url'], srctype=Source.GITHUB_REPO,
662-
name=member['name'])
663-
elif member['type'] == 'file':
664-
# Likely a submodule
665-
if member['size'] == 0:
666-
return SourceDir(None, srctype=Source.GITHUB_REPO,
667-
name=member['name'])
668-
return SourceFile(member['name'])
669-
elif member['type'] == 'commit':
670-
# No path is given by the api here
671-
return SourceDir(None, srctype=Source.GITHUB_REPO,
672-
name=member['name'])
673-
# git_url with <repo>/tree/ presents results a little differently
674-
elif 'type' in member and 'path' in member and 'url' in member:
675-
if member['type'] not in ['tree', 'blob']:
676-
log.debug(f' skipping {member["path"]} type={member["type"]}')
677-
if member['type'] == 'tree':
678-
return SourceDir(member['url'], srctype=Source.GITHUB_REPO,
679-
name=member['path'])
680-
elif member['type'] == 'blob':
681-
# This can be a submodule
682-
if member['size'] == 0:
683-
return SourceDir(member['git_url'], srctype=Source.GITHUB_REPO,
684-
name=member['name'])
685-
return SourceFile(member['path'])
686-
elif member['type'] == 'commit':
687-
# No path is given by the api here
688-
return SourceDir(None, srctype=Source.GITHUB_REPO,
689-
name=member['name'])
690-
return None
691-
692-
693-
def populate_github_repo(url: str) -> list:
694-
"""populate one level of a github repository via REST API"""
695-
# Forces search to clone remote repos (for blackbox testing)
696-
if GITHUB_API_FALLBACK:
697-
with tempfile.NamedTemporaryFile() as tmp:
698-
raise HTTPError(url, 403, 'simulated ratelimit', {}, tmp)
699-
# FIXME: This probably contains leftover cruft.
700-
repo = url.split('/')
701-
while '' in repo:
702-
repo.remove('')
703-
repo_name = None
704-
parsed_url = urlparse(url.removesuffix('.git'))
705-
if 'github.com' not in parsed_url.netloc:
706-
return None
707-
if len(parsed_url.path.split('/')) < 2:
708-
return None
709-
start = 1
710-
# Maybe we were passed an api.github.com/repo/<user> url
711-
if 'api' in parsed_url.netloc:
712-
start += 1
713-
repo_user = parsed_url.path.split('/')[start]
714-
repo_name = parsed_url.path.split('/')[start + 1]
715-
716-
# Get details from the github API.
717-
if API_GITHUB_COM in url:
718-
api_url = url
719-
else:
720-
api_url = f'{API_GITHUB_COM}/repos/{repo_user}/{repo_name}/contents/'
721-
722-
git_url = api_url
723-
if "api.github.com" in git_url:
724-
# This lets us redirect to handle blackbox testing
725-
log.debug(f'fetching from gh API: {git_url}')
726-
git_url = (API_GITHUB_COM + git_url.split("api.github.com")[-1])
727-
# Ratelimiting occurs for non-authenticated GH API calls at 60 in 1 hour.
728-
r = urlopen(git_url, timeout=5)
729-
if r.status != 200:
730-
return False
731-
if 'git/tree' in git_url:
732-
tree = json.loads(r.read().decode())['tree']
733-
else:
734-
tree = json.loads(r.read().decode())
735-
contents = []
736-
for sub in tree:
737-
if source_element_from_repo_api(sub):
738-
contents.append(source_element_from_repo_api(sub))
739-
return contents
740-
741-
742643
def copy_remote_git_source(github_source: InstInfo, verbose: bool=True) -> SourceDir:
743644
"""clone or fetch & checkout a local copy of a remote git repo"""
744645
user, repo = Source.get_github_user_repo(github_source.source_loc)
@@ -1329,22 +1230,30 @@ def _source_search(name: str, src: LoadedSource) -> Union[InstInfo, None]:
13291230
"""Identify source type, retrieve contents, and populate InstInfo
13301231
if the relevant contents are found."""
13311232
root_dir = src.content
1332-
source = InstInfo(name, root_dir.location, None)
1233+
source = InstInfo(name, root_dir.location)
13331234

13341235
# If a local clone of a github source already exists, prefer searching
13351236
# that instead of accessing the github API.
1336-
if src.type == Source.GITHUB_REPO:
1237+
if src.type == Source.REMOTE_GIT_REPO:
13371238
if src.local_clone:
13381239
if not src.local_clone_fetched:
13391240
# FIXME: Pass the LoadedSource here?
13401241
if _git_update(src.original_source, src.local_clone.location):
13411242
src.local_clone_fetched = True
13421243
log.debug(f'fetching local clone of {src.original_source}')
13431244
log.debug(f"Using local clone of {src}: {src.local_clone.location}")
1245+
1246+
# FIXME: ideally, the InstInfo object would have a concept of the
1247+
# original LoadedSource and get_inst_details would follow the local clone
13441248
source.source_loc = str(src.local_clone.location)
13451249
source.srctype = Source.GIT_LOCAL_CLONE
13461250

13471251
if source.get_inst_details(permissive=True):
1252+
# If we have a local clone, report back the original location and type,
1253+
# not the clone that was traversed.
1254+
if source.srctype is Source.GIT_LOCAL_CLONE:
1255+
source.source_loc = src.original_source
1256+
source.srctype = src.type
13481257
return source
13491258
return None
13501259

@@ -1354,9 +1263,11 @@ def _git_clone(src: InstInfo, dest: Union[PosixPath, str], verbose: bool=True) -
13541263
log.info(f'cloning {src.srctype} {src}')
13551264
else:
13561265
log.debug(f'cloning {src.srctype} {src}')
1357-
if src.srctype == Source.GITHUB_REPO:
1358-
assert 'github.com' in src.source_loc
1359-
source = f"{GITHUB_COM}" + src.source_loc.split("github.com")[-1]
1266+
if src.srctype == Source.REMOTE_GIT_REPO:
1267+
if 'github.com' in src.source_loc:
1268+
source = f"{GITHUB_COM}" + src.source_loc.split("github.com")[-1]
1269+
else:
1270+
source = src.source_loc
13601271
elif src.srctype in [Source.LOCAL_REPO, Source.OTHER_URL,
13611272
Source.GIT_LOCAL_CLONE]:
13621273
source = src.source_loc
@@ -1375,8 +1286,14 @@ def _git_clone(src: InstInfo, dest: Union[PosixPath, str], verbose: bool=True) -
13751286

13761287

13771288
def _git_update(github_source: str, local_copy: PosixPath):
1289+
1290+
if 'github.com' in github_source:
1291+
source = GITHUB_COM + github_source.split('github.com')[-1]
1292+
else:
1293+
source = github_source
1294+
13781295
# Ensure this is the correct source
1379-
git = run(['git', 'remote', 'set-url', 'origin', github_source],
1296+
git = run(['git', 'remote', 'set-url', 'origin', source],
13801297
cwd=str(local_copy), stdout=PIPE, stderr=PIPE, text=True,
13811298
check=False, timeout=60)
13821299
assert git.returncode == 0
@@ -1401,7 +1318,7 @@ def _git_update(github_source: str, local_copy: PosixPath):
14011318
default_branch = git.stdout.splitlines()[0]
14021319
if default_branch not in ['origin/master', 'origin/main']:
14031320
log.debug(f'UNUSUAL: fetched default branch {default_branch} for '
1404-
f'{github_source}')
1321+
f'{source}')
14051322

14061323
# Checkout default branch
14071324
git = run(['git', 'checkout', default_branch],
@@ -1448,7 +1365,7 @@ def _checkout_commit(orig_src: InstInfo,
14481365
cloned_src: InstInfo,
14491366
cloned_path: PosixPath):
14501367
# Check out and verify commit/tag if source was a repository
1451-
if orig_src.srctype in [Source.LOCAL_REPO, Source.GITHUB_REPO,
1368+
if orig_src.srctype in [Source.LOCAL_REPO, Source.REMOTE_GIT_REPO,
14521369
Source.OTHER_URL, Source.GIT_LOCAL_CLONE]:
14531370
if orig_src.commit:
14541371
log.debug(f"Checking out {orig_src.commit}")
@@ -1515,7 +1432,7 @@ def _install_plugin(src: InstInfo) -> Union[InstInfo, None]:
15151432
f" {full_source_path}"))
15161433
create_dir(clone_path)
15171434
shutil.copytree(full_source_path, plugin_path)
1518-
elif src.srctype in [Source.LOCAL_REPO, Source.GITHUB_REPO,
1435+
elif src.srctype in [Source.LOCAL_REPO, Source.REMOTE_GIT_REPO,
15191436
Source.OTHER_URL, Source.GIT_LOCAL_CLONE]:
15201437
# clone git repository to /tmp/reckless-...
15211438
if not _git_clone(src, plugin_path):
@@ -1548,7 +1465,7 @@ def _install_plugin(src: InstInfo) -> Union[InstInfo, None]:
15481465
inst_check_src.source_dir = clone.content
15491466
inst_check_src.source_dir.parent_source = clone
15501467

1551-
if src.srctype == Source.GITHUB_REPO:
1468+
if src.srctype == Source.REMOTE_GIT_REPO:
15521469
inst_check_src.srctype = Source.GIT_LOCAL_CLONE
15531470
else:
15541471
inst_check_src.srctype = clone.type
@@ -1727,7 +1644,7 @@ def install(plugin_name: str) -> Union[str, None]:
17271644
src = None
17281645
if direct_location:
17291646
log.debug(f"install of {name} requested from {direct_location}")
1730-
src = InstInfo(name, direct_location, name)
1647+
src = InstInfo(name, direct_location)
17311648
# Treating a local git repo as a directory allows testing
17321649
# uncommitted changes.
17331650
if src and src.srctype == Source.LOCAL_REPO:
@@ -1779,7 +1696,7 @@ def install(plugin_name: str) -> Union[str, None]:
17791696

17801697

17811698
def uninstall(plugin_name: str) -> str:
1782-
"""dDisables plugin and deletes the plugin's reckless dir. Returns the
1699+
"""Disables plugin and deletes the plugin's reckless dir. Returns the
17831700
status of the uninstall attempt."""
17841701
assert isinstance(plugin_name, str)
17851702
log.debug(f'Uninstalling plugin {plugin_name}')
@@ -1807,7 +1724,7 @@ def search(plugin_name: str) -> Union[InstInfo, None]:
18071724

18081725
for src in RECKLESS_SOURCES:
18091726
# Search repos named after the plugin before collections
1810-
if src.type == Source.GITHUB_REPO:
1727+
if src.type == Source.REMOTE_GIT_REPO:
18111728
if src.original_source.split('/')[-1].lower().removesuffix('.git') == plugin_name.lower():
18121729
ordered_sources.remove(src)
18131730
ordered_sources.insert(0, src)
@@ -1821,7 +1738,7 @@ def search(plugin_name: str) -> Union[InstInfo, None]:
18211738
log.debug(f'cannot search {source.type} {source.original_source}')
18221739
continue
18231740
if source.type in [Source.DIRECTORY, Source.LOCAL_REPO,
1824-
Source.GITHUB_REPO, Source.OTHER_URL]:
1741+
Source.REMOTE_GIT_REPO, Source.OTHER_URL]:
18251742
found = _source_search(plugin_name, source)
18261743
if found:
18271744
log.debug(f"{found}, {found.srctype}")
@@ -2101,7 +2018,7 @@ def update_plugin(plugin_name: str) -> tuple:
21012018
return (None, UpdateStatus.REFUSING_UPDATE)
21022019

21032020
src = InstInfo(plugin_name,
2104-
metadata['original source'], None)
2021+
metadata['original source'])
21052022
if not src.get_inst_details():
21062023
log.error(f'cannot locate {plugin_name} in original source {metadata["original_source"]}')
21072024
return (None, UpdateStatus.ERROR)
@@ -2252,7 +2169,7 @@ def find_plugin_candidates(source: Union[LoadedSource, SourceDir], depth=2) -> l
22522169
assert s.srctype == source.srctype, f'source dir {s.name}, {s.srctype} did not inherit {source.srctype} from {source.name}'
22532170
assert s.parent_source == source.parent_source, f'source dir {s.name} did not inherit parent {source.parent_source} from {source.name}'
22542171

2255-
guess = InstInfo(source.name, source.location, None, source_dir=source)
2172+
guess = InstInfo(source.name, source.location, source_dir=source)
22562173
guess.srctype = source.srctype
22572174
manifest = None
22582175
if guess.get_inst_details():
@@ -2292,11 +2209,10 @@ def available_plugins() -> list:
22922209
log.debug(f'confusing source: {source.type}')
22932210
continue
22942211
# It takes too many API calls to query for installable plugins accurately.
2295-
if source.type == Source.GITHUB_REPO and not source.local_clone:
2212+
if source.type == Source.REMOTE_GIT_REPO and not source.local_clone:
22962213
# FIXME: ignoring non-cloned repos for now.
22972214
log.debug(f'cloning {source.original_source} in order to search')
22982215
clone = copy_remote_git_source(InstInfo(None,
2299-
source.original_source,
23002216
source.original_source,
23012217
source_dir=source.content),
23022218
verbose=False)
@@ -2538,7 +2454,6 @@ if __name__ == '__main__':
25382454
LIGHTNING_CONFIG = args.conf
25392455
RECKLESS_CONFIG = load_config(reckless_dir=str(RECKLESS_DIR),
25402456
network=NETWORK)
2541-
RECKLESS_SOURCES = load_sources()
25422457
API_GITHUB_COM = 'https://api.github.com'
25432458
GITHUB_COM = 'https://github.com'
25442459
# Used for blackbox testing to avoid hitting github servers
@@ -2551,6 +2466,7 @@ if __name__ == '__main__':
25512466
if 'GITHUB_API_FALLBACK' in os.environ:
25522467
GITHUB_API_FALLBACK = os.environ['GITHUB_API_FALLBACK']
25532468

2469+
RECKLESS_SOURCES = load_sources()
25542470
if 'targets' in args: # and len(args.targets) > 0:
25552471
if args.func.__name__ == 'help_alias':
25562472
log.add_result(args.func(args.targets))

0 commit comments

Comments
 (0)