Skip to content

Commit bfb29aa

Browse files
endothermicdevcdecker
authored andcommitted
reckless: Clone github sources when API access fails
Due to the API ratelimit, this allows cloning a github repo and searching the result rather than searching via the REST API. If a source has already been cloned, it is fetched and the default branch checked out. Fixes a failure reported by @farscapian Changelog-Fixed: Reckless no longer fails on github API ratelimit.
1 parent ba9ec41 commit bfb29aa

File tree

1 file changed

+113
-6
lines changed

1 file changed

+113
-6
lines changed

tools/reckless

Lines changed: 113 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import types
1717
from typing import Union
1818
from urllib.parse import urlparse
1919
from urllib.request import urlopen
20+
from urllib.error import HTTPError
2021
import venv
2122

2223

@@ -144,7 +145,8 @@ class InstInfo:
144145
target = SourceDir(self.source_loc, srctype=self.srctype)
145146
# Set recursion for how many directories deep we should search
146147
depth = 0
147-
if self.srctype in [Source.DIRECTORY, Source.LOCAL_REPO]:
148+
if self.srctype in [Source.DIRECTORY, Source.LOCAL_REPO,
149+
Source.GIT_LOCAL_CLONE]:
148150
depth = 5
149151
elif self.srctype == Source.GITHUB_REPO:
150152
depth = 1
@@ -193,7 +195,28 @@ class InstInfo:
193195
return success
194196
return None
195197

196-
result = search_dir(self, target, False, depth)
198+
try:
199+
result = search_dir(self, target, False, depth)
200+
# Using the rest API of github.com may result in a
201+
# "Error 403: rate limit exceeded" or other access issues.
202+
# Fall back to cloning and searching the local copy instead.
203+
except HTTPError:
204+
result = None
205+
if self.srctype == Source.GITHUB_REPO:
206+
# clone source to reckless dir
207+
target = copy_remote_git_source(self)
208+
if not target:
209+
logging.warning(f"could not clone github source {self}")
210+
return False
211+
logging.debug(f"falling back to cloning remote repo {self}")
212+
# Update to reflect use of a local clone
213+
self.source_loc = target.location
214+
self.srctype = target.srctype
215+
result = search_dir(self, target, False, 5)
216+
217+
if not result:
218+
return False
219+
197220
if result:
198221
if result != target:
199222
if result.relative:
@@ -235,6 +258,8 @@ class Source(Enum):
235258
GITHUB_REPO = 3
236259
OTHER_URL = 4
237260
UNKNOWN = 5
261+
# Cloned from remote source before searching (rather than github API)
262+
GIT_LOCAL_CLONE = 6
238263

239264
@classmethod
240265
def get_type(cls, source: str):
@@ -253,6 +278,16 @@ class Source(Enum):
253278
return cls(4)
254279
return cls(5)
255280

281+
@classmethod
282+
def get_github_user_repo(cls, source: str) -> (str, str):
283+
'extract a github username and repository name'
284+
if 'github.com/' not in source.lower():
285+
return None, None
286+
trailing = Path(source.lower().partition('github.com/')[2]).parts
287+
if len(trailing) < 2:
288+
return None, None
289+
return trailing[0], trailing[1]
290+
256291

257292
class SourceDir():
258293
"""Structure to search source contents."""
@@ -277,7 +312,7 @@ class SourceDir():
277312
# logging.debug(f"populating {self.srctype} {self.location}")
278313
if self.srctype == Source.DIRECTORY:
279314
self.contents = populate_local_dir(self.location)
280-
elif self.srctype == Source.LOCAL_REPO:
315+
elif self.srctype in [Source.LOCAL_REPO, Source.GIT_LOCAL_CLONE]:
281316
self.contents = populate_local_repo(self.location)
282317
elif self.srctype == Source.GITHUB_REPO:
283318
self.contents = populate_github_repo(self.location)
@@ -435,6 +470,11 @@ def source_element_from_repo_api(member: dict):
435470

436471

437472
def populate_github_repo(url: str) -> list:
473+
"""populate one level of a github repository via REST API"""
474+
# Forces search to clone remote repos (for blackbox testing)
475+
if GITHUB_API_FALLBACK:
476+
with tempfile.NamedTemporaryFile() as tmp:
477+
raise HTTPError(url, 403, 'simulated ratelimit', {}, tmp)
438478
# FIXME: This probably contains leftover cruft.
439479
repo = url.split('/')
440480
while '' in repo:
@@ -478,6 +518,28 @@ def populate_github_repo(url: str) -> list:
478518
return contents
479519

480520

521+
def copy_remote_git_source(github_source: InstInfo):
522+
"""clone or fetch & checkout a local copy of a remote git repo"""
523+
user, repo = Source.get_github_user_repo(github_source.source_loc)
524+
if not user or not repo:
525+
logging.warning('could not extract github user and repo '
526+
f'name for {github_source.source_loc}')
527+
return None
528+
local_path = RECKLESS_DIR / '.remote_sources' / user
529+
create_dir(RECKLESS_DIR / '.remote_sources')
530+
if not create_dir(local_path):
531+
logging.warning(f'could not provision dir {local_path} to '
532+
f'clone remote source {github_source.source_loc}')
533+
return None
534+
local_path = local_path / repo
535+
if local_path.exists():
536+
# Fetch the latest
537+
assert _git_update(github_source, local_path)
538+
else:
539+
_git_clone(github_source, local_path)
540+
return SourceDir(local_path, srctype=Source.GIT_LOCAL_CLONE)
541+
542+
481543
class Config():
482544
"""A generic class for procuring, reading and editing config files"""
483545
def obtain_config(self,
@@ -803,7 +865,8 @@ def _git_clone(src: InstInfo, dest: Union[PosixPath, str]) -> bool:
803865
if src.srctype == Source.GITHUB_REPO:
804866
assert 'github.com' in src.source_loc
805867
source = f"{GITHUB_COM}" + src.source_loc.split("github.com")[-1]
806-
elif src.srctype in [Source.LOCAL_REPO, Source.OTHER_URL]:
868+
elif src.srctype in [Source.LOCAL_REPO, Source.OTHER_URL,
869+
Source.GIT_LOCAL_CLONE]:
807870
source = src.source_loc
808871
else:
809872
return False
@@ -819,6 +882,46 @@ def _git_clone(src: InstInfo, dest: Union[PosixPath, str]) -> bool:
819882
return True
820883

821884

885+
def _git_update(github_source: InstInfo, local_copy: PosixPath):
886+
# Ensure this is the correct source
887+
git = run(['git', 'remote', 'set-url', 'origin', github_source.source_loc],
888+
cwd=str(local_copy), stdout=PIPE, stderr=PIPE, text=True,
889+
check=False, timeout=60)
890+
assert git.returncode == 0
891+
if git.returncode != 0:
892+
return False
893+
894+
# Fetch the latest from the remote
895+
git = run(['git', 'fetch', 'origin', '--recurse-submodules=on-demand'],
896+
cwd=str(local_copy), stdout=PIPE, stderr=PIPE, text=True,
897+
check=False, timeout=60)
898+
assert git.returncode == 0
899+
if git.returncode != 0:
900+
return False
901+
902+
# Find default branch
903+
git = run(['git', 'symbolic-ref', 'refs/remotes/origin/HEAD', '--short'],
904+
cwd=str(local_copy), stdout=PIPE, stderr=PIPE, text=True,
905+
check=False, timeout=60)
906+
assert git.returncode == 0
907+
if git.returncode != 0:
908+
return False
909+
default_branch = git.stdout.splitlines()[0]
910+
if default_branch != 'origin/master':
911+
logging.debug(f'UNUSUAL: fetched default branch {default_branch} for '
912+
f'{github_source.source_loc}')
913+
914+
# Checkout default branch
915+
git = run(['git', 'checkout', default_branch],
916+
cwd=str(local_copy), stdout=PIPE, stderr=PIPE, text=True,
917+
check=False, timeout=60)
918+
assert git.returncode == 0
919+
if git.returncode != 0:
920+
return False
921+
922+
return True
923+
924+
822925
def get_temp_reckless_dir() -> PosixPath:
823926
random_dir = 'reckless-{}'.format(str(hash(os.times()))[-9:])
824927
new_path = Path(tempfile.gettempdir()) / random_dir
@@ -850,7 +953,7 @@ def _checkout_commit(orig_src: InstInfo,
850953
cloned_path: PosixPath):
851954
# Check out and verify commit/tag if source was a repository
852955
if orig_src.srctype in [Source.LOCAL_REPO, Source.GITHUB_REPO,
853-
Source.OTHER_URL]:
956+
Source.OTHER_URL, Source.GIT_LOCAL_CLONE]:
854957
if orig_src.commit:
855958
logging.debug(f"Checking out {orig_src.commit}")
856959
checkout = Popen(['git', 'checkout', orig_src.commit],
@@ -912,7 +1015,7 @@ def _install_plugin(src: InstInfo) -> Union[InstInfo, None]:
9121015
create_dir(clone_path)
9131016
shutil.copytree(src.source_loc, plugin_path)
9141017
elif src.srctype in [Source.LOCAL_REPO, Source.GITHUB_REPO,
915-
Source.OTHER_URL]:
1018+
Source.OTHER_URL, Source.GIT_LOCAL_CLONE]:
9161019
# clone git repository to /tmp/reckless-...
9171020
if not _git_clone(src, plugin_path):
9181021
return None
@@ -1401,6 +1504,10 @@ if __name__ == '__main__':
14011504
GITHUB_COM = os.environ['REDIR_GITHUB']
14021505
logging.root.setLevel(args.loglevel)
14031506

1507+
GITHUB_API_FALLBACK = False
1508+
if 'GITHUB_API_FALLBACK' in os.environ:
1509+
GITHUB_API_FALLBACK = os.environ['GITHUB_API_FALLBACK']
1510+
14041511
if 'targets' in args:
14051512
# FIXME: Catch missing argument
14061513
if args.func.__name__ == 'help_alias':

0 commit comments

Comments
 (0)