mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-06 11:18:54 -05:00
[Downloader] Catch and handle erorr in update_all when target repository/branch is missing (#3080)
* [Downloader] Catch and handle erorr in update_all when target repository/branch is removed from remote * Rewrite fix, remove ctx from repo_manager, edit docstring, add annotations * Text formatting * Group failed repo messages into padded table, catch single updated repo fails * Error catching v2; repo_manager design change * Docstrings, typos and changelog * Add Optional to update_repos annotatition * Wrong logic * Clear-er log message. * add format_failed_repos, change _repo_update for failed messages * Merge cog updating with fail repo logic; Filter out failed repos * Merge cog updating with fail repo logic; Cog updating logic shuffled to support sending fails at the end * Docstring typo * format_failed_repos - proper docstring * repo_manager.update_repos argument name fix * downloader._cog_checkforupdates added missed failed message * downloader._cog_update_logic place back return on some errors * Purge unused stuff from downloader._repo_update * downloader._cog_update_logic Change exception catching * _cog_update_logic purging obsolete * Remove obsolete 'message' from _cog_checkforupdates * Fix forgotten ctx.send * Wording * Removed obsolete 'message' * Fix wrong type hint in , update docstring * repo update logic fix * format_failed_repos type hint and docstring repair * Extend _get_cogs_to_check with 'update_repos' * Fix type mangling in _get_cogs_to_check * fix: typo Co-Authored-By: jack1142 <6032823+jack1142@users.noreply.github.com> * _repo_update; Added single repo up-to-date message
This commit is contained in:
parent
9a051ef2c6
commit
064d97f87b
1
changelog.d/downloader/2936.bugfix.rst
Normal file
1
changelog.d/downloader/2936.bugfix.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Catch errors if remote repository or branch is deleted, notify user which repository failed and continue updating others.
|
||||||
1
changelog.d/downloader/3080.misc.1.rst
Normal file
1
changelog.d/downloader/3080.misc.1.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
`RepoManager.update_all_repos` replaced by new method `update_repos` which additionally handles failing repositories.
|
||||||
1
changelog.d/downloader/3080.misc.2.rst
Normal file
1
changelog.d/downloader/3080.misc.2.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Added `Downloader.format_failed_repos` for formatting error message of repos failing to update.
|
||||||
@ -5,7 +5,7 @@ import re
|
|||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Tuple, Union, Iterable, Optional, Dict, Set, List, cast
|
from typing import Tuple, Union, Iterable, Collection, Optional, Dict, Set, List, cast
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
@ -499,24 +499,24 @@ class Downloader(commands.Cog):
|
|||||||
"""Update all repos, or ones of your choosing."""
|
"""Update all repos, or ones of your choosing."""
|
||||||
async with ctx.typing():
|
async with ctx.typing():
|
||||||
updated: Set[str]
|
updated: Set[str]
|
||||||
if not repos:
|
|
||||||
updated = {repo.name for repo in await self._repo_manager.update_all_repos()}
|
updated_repos, failed = await self._repo_manager.update_repos(repos)
|
||||||
else:
|
updated = {repo.name for repo in updated_repos}
|
||||||
updated = set()
|
|
||||||
for repo in repos:
|
|
||||||
old, new = await repo.update()
|
|
||||||
if old != new:
|
|
||||||
updated.add(repo.name)
|
|
||||||
|
|
||||||
if updated:
|
if updated:
|
||||||
message = _("Repo update completed successfully.")
|
message = _("Repo update completed successfully.")
|
||||||
message += _("\nUpdated: ") + humanize_list(tuple(map(inline, updated)))
|
message += _("\nUpdated: ") + humanize_list(tuple(map(inline, updated)))
|
||||||
elif repos is None:
|
elif not repos:
|
||||||
await ctx.send(_("All installed repos are already up to date."))
|
message = _("All installed repos are already up to date.")
|
||||||
return
|
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("These repos are already up to date."))
|
if len(updated_repos) > 1:
|
||||||
return
|
message = _("These repos are already up to date.")
|
||||||
|
else:
|
||||||
|
message = _("This repo is already up to date.")
|
||||||
|
|
||||||
|
if failed:
|
||||||
|
message += "\n" + self.format_failed_repos(failed)
|
||||||
|
|
||||||
await ctx.send(message)
|
await ctx.send(message)
|
||||||
|
|
||||||
@commands.group()
|
@commands.group()
|
||||||
@ -721,8 +721,9 @@ class Downloader(commands.Cog):
|
|||||||
This command doesn't update cogs, it only checks for updates.
|
This command doesn't update cogs, it only checks for updates.
|
||||||
Use `[p]cog update` to update cogs.
|
Use `[p]cog update` to update cogs.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async with ctx.typing():
|
async with ctx.typing():
|
||||||
cogs_to_check = await self._get_cogs_to_check()
|
cogs_to_check, failed = await self._get_cogs_to_check()
|
||||||
cogs_to_update, libs_to_update = await self._available_updates(cogs_to_check)
|
cogs_to_update, libs_to_update = await self._available_updates(cogs_to_check)
|
||||||
message = ""
|
message = ""
|
||||||
if cogs_to_update:
|
if cogs_to_update:
|
||||||
@ -735,10 +736,14 @@ class Downloader(commands.Cog):
|
|||||||
message += _("\nThese shared libraries can be updated: ") + humanize_list(
|
message += _("\nThese shared libraries can be updated: ") + humanize_list(
|
||||||
tuple(map(inline, libnames))
|
tuple(map(inline, libnames))
|
||||||
)
|
)
|
||||||
if message:
|
|
||||||
|
if not message:
|
||||||
|
message = _("All installed cogs are up to date.")
|
||||||
|
|
||||||
|
if failed:
|
||||||
|
message += "\n" + self.format_failed_repos(failed)
|
||||||
|
|
||||||
await ctx.send(message)
|
await ctx.send(message)
|
||||||
else:
|
|
||||||
await ctx.send(_("All installed cogs are up to date."))
|
|
||||||
|
|
||||||
@cog.command(name="update")
|
@cog.command(name="update")
|
||||||
async def _cog_update(self, ctx: commands.Context, *cogs: InstalledCog) -> None:
|
async def _cog_update(self, ctx: commands.Context, *cogs: InstalledCog) -> None:
|
||||||
@ -774,11 +779,22 @@ class Downloader(commands.Cog):
|
|||||||
rev: Optional[str] = None,
|
rev: Optional[str] = None,
|
||||||
cogs: Optional[List[InstalledModule]] = None,
|
cogs: Optional[List[InstalledModule]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
message = ""
|
||||||
|
failed_repos = set()
|
||||||
|
updates_available = set()
|
||||||
|
|
||||||
async with ctx.typing():
|
async with ctx.typing():
|
||||||
# this is enough to be sure that `rev` is not None (based on calls to this method)
|
# this is enough to be sure that `rev` is not None (based on calls to this method)
|
||||||
if repo is not None:
|
if repo is not None:
|
||||||
rev = cast(str, rev)
|
rev = cast(str, rev)
|
||||||
|
|
||||||
|
try:
|
||||||
await repo.update()
|
await repo.update()
|
||||||
|
except errors.UpdateError:
|
||||||
|
message = self.format_failed_repos([repo.name])
|
||||||
|
await ctx.send(message)
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
commit = await repo.get_full_sha1(rev)
|
commit = await repo.get_full_sha1(rev)
|
||||||
except errors.AmbiguousRevision as e:
|
except errors.AmbiguousRevision as e:
|
||||||
@ -794,52 +810,57 @@ class Downloader(commands.Cog):
|
|||||||
await ctx.send(msg)
|
await ctx.send(msg)
|
||||||
return
|
return
|
||||||
except errors.UnknownRevision:
|
except errors.UnknownRevision:
|
||||||
await ctx.send(
|
message += _(
|
||||||
_("Error: there is no revision `{rev}` in repo `{repo.name}`").format(
|
"Error: there is no revision `{rev}` in repo `{repo.name}`"
|
||||||
rev=rev, repo=repo
|
).format(rev=rev, repo=repo)
|
||||||
)
|
await ctx.send(message)
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
await repo.checkout(commit)
|
await repo.checkout(commit)
|
||||||
cogs_to_check = await self._get_cogs_to_check(repos=[repo], cogs=cogs)
|
cogs_to_check, __ = await self._get_cogs_to_check(
|
||||||
|
repos=[repo], cogs=cogs, update_repos=False
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
cogs_to_check = await self._get_cogs_to_check(repos=repos, cogs=cogs)
|
cogs_to_check, check_failed = await self._get_cogs_to_check(repos=repos, cogs=cogs)
|
||||||
|
failed_repos.update(check_failed)
|
||||||
|
|
||||||
pinned_cogs = {cog for cog in cogs_to_check if cog.pinned}
|
pinned_cogs = {cog for cog in cogs_to_check if cog.pinned}
|
||||||
cogs_to_check -= pinned_cogs
|
cogs_to_check -= pinned_cogs
|
||||||
if not cogs_to_check:
|
if not cogs_to_check:
|
||||||
message = _("There were no cogs to check.")
|
message += _("There were no cogs to check.")
|
||||||
if pinned_cogs:
|
if pinned_cogs:
|
||||||
cognames = [cog.name for cog in pinned_cogs]
|
cognames = [cog.name for cog in pinned_cogs]
|
||||||
message += _(
|
message += _(
|
||||||
"\nThese cogs are pinned and therefore weren't checked: "
|
"\nThese cogs are pinned and therefore weren't checked: "
|
||||||
) + humanize_list(tuple(map(inline, cognames)))
|
) + humanize_list(tuple(map(inline, cognames)))
|
||||||
await ctx.send(message)
|
else:
|
||||||
return
|
|
||||||
cogs_to_update, libs_to_update = await self._available_updates(cogs_to_check)
|
cogs_to_update, libs_to_update = await self._available_updates(cogs_to_check)
|
||||||
|
|
||||||
updates_available = cogs_to_update or libs_to_update
|
updates_available = cogs_to_update or libs_to_update
|
||||||
cogs_to_update, filter_message = self._filter_incorrect_cogs(cogs_to_update)
|
cogs_to_update, filter_message = self._filter_incorrect_cogs(cogs_to_update)
|
||||||
message = ""
|
|
||||||
if updates_available:
|
if updates_available:
|
||||||
updated_cognames, message = await self._update_cogs_and_libs(
|
updated_cognames, message = await self._update_cogs_and_libs(
|
||||||
cogs_to_update, libs_to_update
|
cogs_to_update, libs_to_update
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
if repos:
|
if repos:
|
||||||
message = _("Cogs from provided repos are already up to date.")
|
message += _("Cogs from provided repos are already up to date.")
|
||||||
elif repo:
|
elif repo:
|
||||||
if cogs:
|
if cogs:
|
||||||
message = _("Provided cogs are already up to date with this revision.")
|
message += _(
|
||||||
|
"Provided cogs are already up to date with this revision."
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
message = _(
|
message += _(
|
||||||
"Cogs from provided repo are already up to date with this revision."
|
"Cogs from provided repo are already up to date with this revision."
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
if cogs:
|
if cogs:
|
||||||
message = _("Provided cogs are already up to date.")
|
message += _("Provided cogs are already up to date.")
|
||||||
else:
|
else:
|
||||||
message = _("All installed cogs are already up to date.")
|
message += _("All installed cogs are already up to date.")
|
||||||
if repo is not None:
|
if repo is not None:
|
||||||
await repo.checkout(repo.branch)
|
await repo.checkout(repo.branch)
|
||||||
if pinned_cogs:
|
if pinned_cogs:
|
||||||
@ -848,7 +869,12 @@ class Downloader(commands.Cog):
|
|||||||
"\nThese cogs are pinned and therefore weren't checked: "
|
"\nThese cogs are pinned and therefore weren't checked: "
|
||||||
) + humanize_list(tuple(map(inline, cognames)))
|
) + humanize_list(tuple(map(inline, cognames)))
|
||||||
message += filter_message
|
message += filter_message
|
||||||
|
|
||||||
|
if failed_repos:
|
||||||
|
message += "\n" + self.format_failed_repos(failed_repos)
|
||||||
|
|
||||||
await ctx.send(message)
|
await ctx.send(message)
|
||||||
|
|
||||||
if updates_available and updated_cognames:
|
if updates_available and updated_cognames:
|
||||||
await self._ask_for_cog_reload(ctx, updated_cognames)
|
await self._ask_for_cog_reload(ctx, updated_cognames)
|
||||||
|
|
||||||
@ -1024,23 +1050,31 @@ class Downloader(commands.Cog):
|
|||||||
*,
|
*,
|
||||||
repos: Optional[Iterable[Repo]] = None,
|
repos: Optional[Iterable[Repo]] = None,
|
||||||
cogs: Optional[Iterable[InstalledModule]] = None,
|
cogs: Optional[Iterable[InstalledModule]] = None,
|
||||||
) -> Set[InstalledModule]:
|
update_repos: bool = True,
|
||||||
|
) -> Tuple[Set[InstalledModule], List[str]]:
|
||||||
|
failed = []
|
||||||
if not (cogs or repos):
|
if not (cogs or repos):
|
||||||
await self._repo_manager.update_all_repos()
|
if update_repos:
|
||||||
cogs_to_check = {cog for cog in await self.installed_cogs() if cog.repo is not None}
|
__, failed = await self._repo_manager.update_repos()
|
||||||
|
|
||||||
|
cogs_to_check = {
|
||||||
|
cog
|
||||||
|
for cog in await self.installed_cogs()
|
||||||
|
if cog.repo is not None and cog.repo.name not in failed
|
||||||
|
}
|
||||||
else:
|
else:
|
||||||
# this is enough to be sure that `cogs` is not None (based on if above)
|
# this is enough to be sure that `cogs` is not None (based on if above)
|
||||||
if not repos:
|
if not repos:
|
||||||
cogs = cast(Iterable[InstalledModule], cogs)
|
cogs = cast(Iterable[InstalledModule], cogs)
|
||||||
repos = {cog.repo for cog in cogs if cog.repo is not None}
|
repos = {cog.repo for cog in cogs if cog.repo is not None}
|
||||||
|
|
||||||
for repo in repos:
|
if update_repos:
|
||||||
if await repo.is_on_branch():
|
__, failed = await self._repo_manager.update_repos(repos)
|
||||||
exit_to_commit = None
|
|
||||||
else:
|
if failed:
|
||||||
exit_to_commit = repo.commit
|
# remove failed repos
|
||||||
await repo.update()
|
repos = {repo for repo in repos if repo.name not in failed}
|
||||||
await repo.checkout(exit_to_commit)
|
|
||||||
if cogs:
|
if cogs:
|
||||||
cogs_to_check = {cog for cog in cogs if cog.repo is not None and cog.repo in repos}
|
cogs_to_check = {cog for cog in cogs if cog.repo is not None and cog.repo in repos}
|
||||||
else:
|
else:
|
||||||
@ -1050,7 +1084,7 @@ class Downloader(commands.Cog):
|
|||||||
if cog.repo is not None and cog.repo in repos
|
if cog.repo is not None and cog.repo in repos
|
||||||
}
|
}
|
||||||
|
|
||||||
return cogs_to_check
|
return (cogs_to_check, failed)
|
||||||
|
|
||||||
async def _update_cogs_and_libs(
|
async def _update_cogs_and_libs(
|
||||||
self, cogs_to_update: Iterable[Installable], libs_to_update: Iterable[Installable]
|
self, cogs_to_update: Iterable[Installable], libs_to_update: Iterable[Installable]
|
||||||
@ -1207,3 +1241,31 @@ class Downloader(commands.Cog):
|
|||||||
msg = _("This command is not provided by a cog.")
|
msg = _("This command is not provided by a cog.")
|
||||||
|
|
||||||
await ctx.send(box(msg))
|
await ctx.send(box(msg))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def format_failed_repos(failed: Collection[str]) -> str:
|
||||||
|
"""Format collection of ``Repo.name``'s into failed message.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
failed : Collection
|
||||||
|
Collection of ``Repo.name``
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
formatted message
|
||||||
|
"""
|
||||||
|
|
||||||
|
message = (
|
||||||
|
_("Failed to update the following repositories:")
|
||||||
|
if len(failed) > 1
|
||||||
|
else _("Failed to update the following repository:")
|
||||||
|
)
|
||||||
|
message += " " + humanize_list(tuple(map(inline, failed))) + "\n"
|
||||||
|
message += _(
|
||||||
|
"The repository's branch might have been removed or"
|
||||||
|
" the repository is no longer accessible at set url."
|
||||||
|
" See logs for more information."
|
||||||
|
)
|
||||||
|
return message
|
||||||
|
|||||||
@ -796,6 +796,9 @@ class Repo(RepoJSONMixin):
|
|||||||
`tuple` of `str`
|
`tuple` of `str`
|
||||||
:py:code`(old commit hash, new commit hash)`
|
:py:code`(old commit hash, new commit hash)`
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
`UpdateError` - if git pull results with non-zero exit code
|
||||||
"""
|
"""
|
||||||
old_commit = await self.latest_commit()
|
old_commit = await self.latest_commit()
|
||||||
|
|
||||||
@ -1134,28 +1137,58 @@ class RepoManager:
|
|||||||
Tuple[Repo, Tuple[str, str]]
|
Tuple[Repo, Tuple[str, str]]
|
||||||
A 2-`tuple` with Repo object and a 2-`tuple` of `str`
|
A 2-`tuple` with Repo object and a 2-`tuple` of `str`
|
||||||
containing old and new commit hashes.
|
containing old and new commit hashes.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
repo = self._repos[repo_name]
|
repo = self._repos[repo_name]
|
||||||
old, new = await repo.update()
|
old, new = await repo.update()
|
||||||
return (repo, (old, new))
|
return (repo, (old, new))
|
||||||
|
|
||||||
async def update_all_repos(self) -> Dict[Repo, Tuple[str, str]]:
|
async def update_repos(
|
||||||
"""Call `Repo.update` on all repositories.
|
self, repos: Optional[Iterable[Repo]] = None
|
||||||
|
) -> Tuple[Dict[Repo, Tuple[str, str]], List[str]]:
|
||||||
|
"""Calls `Repo.update` on passed repositories and
|
||||||
|
catches failing ones.
|
||||||
|
|
||||||
|
Calling without params updates all currently installed repos.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
repos: Iterable
|
||||||
|
Iterable of Repos, None to update all
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
Dict[Repo, Tuple[str, str]]
|
tuple of Dict and list
|
||||||
A mapping of `Repo` objects that received new commits to
|
A mapping of `Repo` objects that received new commits to
|
||||||
a 2-`tuple` of `str` containing old and new commit hashes.
|
a 2-`tuple` of `str` containing old and new commit hashes.
|
||||||
|
|
||||||
|
`list` of failed `Repo` names
|
||||||
"""
|
"""
|
||||||
|
failed = []
|
||||||
ret = {}
|
ret = {}
|
||||||
for repo_name, __ in self._repos.items():
|
|
||||||
repo, (old, new) = await self.update_repo(repo_name)
|
# select all repos if not specified
|
||||||
|
if not repos:
|
||||||
|
repos = self.repos
|
||||||
|
|
||||||
|
for repo in repos:
|
||||||
|
try:
|
||||||
|
updated_repo, (old, new) = await self.update_repo(repo.name)
|
||||||
|
except errors.UpdateError as err:
|
||||||
|
log.error(
|
||||||
|
"Repository '%s' failed to update. URL: '%s' on branch '%s'",
|
||||||
|
repo.name,
|
||||||
|
repo.url,
|
||||||
|
repo.branch,
|
||||||
|
exc_info=err,
|
||||||
|
)
|
||||||
|
|
||||||
|
failed.append(repo.name)
|
||||||
|
continue
|
||||||
|
|
||||||
if old != new:
|
if old != new:
|
||||||
ret[repo] = (old, new)
|
ret[updated_repo] = (old, new)
|
||||||
return ret
|
|
||||||
|
return ret, failed
|
||||||
|
|
||||||
async def _load_repos(self, set_repos: bool = False) -> Dict[str, Repo]:
|
async def _load_repos(self, set_repos: bool = False) -> Dict[str, Repo]:
|
||||||
ret = {}
|
ret = {}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user